Skip to content

Added a physical open callback#3678

Merged
vonzshik merged 17 commits intonpgsql:mainfrom
vonzshik:3032-physical-open-setup
Apr 23, 2021
Merged

Added a physical open callback#3678
vonzshik merged 17 commits intonpgsql:mainfrom
vonzshik:3032-physical-open-setup

Conversation

@vonzshik
Copy link
Contributor

Closes #3032

@vonzshik vonzshik requested a review from roji as a code owner April 21, 2021 12:46
@vonzshik vonzshik requested a review from NinoFloris April 21, 2021 12:52
Copy link
Member

@NinoFloris NinoFloris left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks a lot!

One thing you might add is a test for this callback + multiplexing. Today as you wrote it everything works ok, tomorrow it could be moved around by more pooling code and end up running after the connection is already available for use by other multiplexing callers.

Copy link
Member

@roji roji left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! See mainly nits below.

I wonder how many people will ever use this aside from @NinoFloris 🤣

@vonzshik
Copy link
Contributor Author

I wonder how many people will ever use this aside from @NinoFloris 🤣

The main thing is, it makes sense, there is almost no perf downside and it's easy to implement, so why not 😛

@vonzshik vonzshik requested a review from roji April 23, 2021 11:20
throw new InvalidOperationException($"Asynchronous {nameof(PhysicalOpenCallback)} isn't supported with synchronous {nameof(NpgsqlConnection.Open)}");
}

await physicalOpenTask;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small nit: if we're in sync mode, we still await the task - this is OK since we've checked above that it's completed. But we could just skip that altogether. So, if the task is complete, no need to do anything (in either sync or async), if it's incomplete we await it in async and throw in sync.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We actually should for ValueTask ;)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's true, but it's a Task now :)

Another reason why I prefer using Task rather than ValueTask everywhere by default. The contract for Task is much simpler and less bug-prone, and when there's really no perf justification for it...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I hadn't seen that change, yes for task it can really just short circuit!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Totally off-topic, but I never understood why they chose to integrate IValueTaskSource into ValueTask as opposed to create a new awaitable. That would have made it typingly obvious what a method returns and what you're allowed to do to it. The non-IValueTaskSource ValueTask (which either contains the value or wraps a Task) is pretty simple and can also be reused without danger, but you can never be sure what's actually behind a ValueTask...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bit of the issue with the .NET type system is you have no nice existential return type support (without lifting it visibly into the type as a universal type). So you can either return a concrete struct or always pay for an interface. GetAwaiter for task already returned the concrete TaskAwaitable struct so I don't see they had much choice.

If you want to be sure you can do whatever with a valuetask you can always call Preserve, and it's nicely short circuited when it already wraps a task.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But I mean, why could they not have created a public ReusableValueTask (or whatever name), which is the awaitable wrapping IValueTaskSource, instead of folding that functionality into the existing ValueTask, whose purpose at that time was only to optimize synchronous completion?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know Stephen Toub really doesn't like having too many user visible types, and I can imagine ValueTask & Task is already concept heavy for many users.

Copy link
Member

@roji roji left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Super small nit if you feel like it.. LGTM, no need for another review from me.

@NinoFloris
Copy link
Member

Final question, how would the PhysicalOpenCallback make sure it can support both sync and async open? Should it just do blocking queries in that case? As it does not know when called if awaiting anything is actually supported.

@roji
Copy link
Member

roji commented Apr 23, 2021

Final question, how would the PhysicalOpenCallback make sure it can support both sync and async open? Should it just do blocking queries in that case? As it does not know when called if awaiting anything is actually supported.

I think the idea is that in a single application, you either use sync or async, and so you provide the appropriate callback. Do you have another scenario in mind? Otherwise we could have two callbacks - though that may be a bit heavy...

@vonzshik
Copy link
Contributor Author

Final question, how would the PhysicalOpenCallback make sure it can support both sync and async open? Should it just do blocking queries in that case? As it does not know when called if awaiting anything is actually supported.

I think the idea is that in a single application, you either use sync or async, and so you provide the appropriate callback. Do you have another scenario in mind? Otherwise we could have two callbacks - though that may be a bit heavy...

Or we can pass the boolean async value.

@NinoFloris
Copy link
Member

Yeah for us it's completely fine but you don't always control the callers of DbConnection methods, say in health checks.

Just posed it as a question so we have at least thought about it.

@roji
Copy link
Member

roji commented Apr 23, 2021

Or we can pass the boolean async value.

We could, but the boolean async value is an internal implementation detail of Npgsql (in order to unify sync/async paths)... for public APIs, we always expose two entry points as is standard in .NET.

Thinking about it a bit more, it does seem more standard-conformant to just expose two callbacks... This would also obviate the sync-over-async issue altogether.

@vonzshik
Copy link
Contributor Author

@roji going to ping you yet again as I've added another callback.

@vonzshik vonzshik requested a review from roji April 23, 2021 14:37
@vonzshik vonzshik enabled auto-merge (squash) April 23, 2021 14:51
@vonzshik vonzshik merged commit 8a1c9c7 into npgsql:main Apr 23, 2021
@vonzshik vonzshik deleted the 3032-physical-open-setup branch April 23, 2021 15:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Physical connection setup handler

4 participants