Repository navigation
Made multiplexing bootstrap threadsafe - #3257
Conversation
roji
left a comment
There was a problem hiding this comment.
@vonzshik this looks good, but can you give a short explanation on how the lack of thread-safety here generates an error? Is this because of the idle channel perf issue you mention in #3248 (comment)? I'm still not sure exactly how it's all connected.
The only thing missing here, is the semaphore disposing, I'm not sure where to put it.
I think we need to make the pool disposable. We could dispose from PoolManager (see Clear/ClearAll) but we'd have to take care of race conditions. Not sure this is critical to do right away - there are generally only a few global pools in a typical application.
I've done more testing. The problems seems to be coming from the transaction logic. Or to be more clear, how multiplexing works, when we attemp to get a connection from the pool synchronously. npgsql/src/Npgsql/ConnectorPool.cs Lines 203 to 209 in e23d23d For some reason, it breaks the ChannelReader, so it takes around a second to read an idle connector.This could be fixed by using an async version for opening of a transaction... That is, if there was one npgsql/src/Npgsql/NpgsqlConnection.cs Lines 615 to 628 in e23d23d I was busy for the last few days, and will take a look at everything else later today (or tomorrow). |
Oh great point... Before multiplexing, beginning a transaction is indeed always synchronous - we just write some bytes into our write buffer, but don't actually send. But with multiplexing, starting a transaction means opening a connection 🤯 We should definitely fix this, it should be easy. |
Honestly, I'm not sure myself. Also, I've found another 'fun' thing: npgsql/src/Npgsql/NpgsqlConnector.cs Lines 782 to 787 in e23d23d |
|
Okaaay, I've managed to repeat the same issue without npgsql. @roji do you want to take a look? |
Absolutely... are you saying there's an issue with Channels? If you share the repro I can take a look, and if needed you can post it in runtime too... |
Here you go. For now, I would like for you to take a look, as it's possible I've missed something. But it should be more or less equal to how npgsql works. The main thing seems to be:
After that, we essentially work in a single thread mode, and the channel returns something every 1 second. |
|
@vonzshik I'm investigating your repro and I think I understand what's going on. The problem is (not surprisingly) not with Channels, but more with us insisting on trying to build a sync API (NpgsqlConnection.Open) over an async one (Channels). I'm looking for a solution. |
So it's more of a "channels were not designed to work like this, so don't do that" kind of thing? |
Not quite - or maybe only in a limited sense :) Doing sync-over-async in general (regardless of Channels) is problematic and risky, and I wish we didn't have to do it. However, our (new) pooling implementation is based on Channels - which is async only - and yet we do not want to break the entire world by saying that Npgsql doesn't support sync any more. I was worried about this when I first adopted Channels (as a way of significantly reducing pool lock-free complexity). The good news is that I think I found a reasonable fix; I've done a bit of tweaking on your repro, take a look at https://github.com/roji/Test/tree/VonzshikChannelFun. I'll assume you understand SingleThreadSynchronizationContext - it's about making sure that we can execute an async operation without its continuation running on the TP thread (but running instead on some other thread). This allows us to block synchronously on the operation, because if that happens on a TP thread, it doesn't trigger a deadlock where all TP threads are synchronously blocking on the operation, but its continuation is waiting to be executed on the TP. Issue 1The problem is that ValueTask.AsTask schedules a callback on the IValueTaskSource - the one which completes the Task - without regards to the synchronization context. In effect, us using AsTask introduces exactly the dependency on the TP which we need to avoid. I don't think this can be considered an issue in AsTask itself: the synchronization context is supposed to affect continuations introduced by await, and it will indeed affect continuations on the Task returned by AsTask, but will not affect the continuation on ValueTask which transitions the Task. My fix for this is to replace var mre = new ManualResetEventSlim();
reader.WaitToReadAsync().GetAwaiter().OnCompleted(() => mre.Set());
mre.Wait();Here we set up the callback on the IValueTaskSource ourselves (similar to what AsTask does under the hood), but GetAwaiter().OnCompleted() does respect the synchronization context. I've submitted #3268 to fix this. Issue 2In https://github.com/roji/Test/tree/VonzshikChannelFun, the first async rent/return pair is commented out, and only the second sync pair is executed. If you uncomment the first pair, you will still see a starvation regardless of the AsTask issue above. This is "normal", and is a result of mixing sync and async (never a good idea), coupled with a thundering herd effect.
To summarize, this is an issue in user code, rather than something that we can fix in Npgsql. /cc @Brar @YohDeadfall |
|
Wow. Thank you @roji for an explanation! Now this does make a lot of sense. |
Not entirely unnecessary. As you've said, the second issue is mixing of sync and async code. And this pr makes the |
Ah yes, that part of it - for sure. But not multiplexing bootstrap thread safety, right? |
Yep - it's mostly an optimization. |
|
Have approved, you can merge when ready. Note that we usually squash PRs here, and almost never do merge commits (why complicated the git history...). |
Closes #3248
The only thing missing here, is the semaphore disposing, I'm not sure where to put it.
@roji thoughts?