-
Notifications
You must be signed in to change notification settings - Fork 10k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Delay socket receive/send until first read/flush #34458
Conversation
- Today when the socket connection is accepted or connection, we implicitly start reading and writing from the socket. This can prevent certain scenarios where users want to get access to the raw socket before any operations happen (like in hand off scenarios). This change defers the reads and writes until read or flush is called on the transport's IDuplexPipe.
- Make sure we don't start multiple instances of reading and writing tasks because of thread safety issues.
{ | ||
try | ||
if (_connectionStarted == 1 || Interlocked.CompareExchange(ref _connectionStarted, 1, 0) == 1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For the "fast-check" if (_connectionStarted == 1
should this be a volatile read?
Or from different view: EnsureStarted
is only called in DisposeAsync
so is it worth to have a fast-check? Just the compare & exchange should be enough?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, it's called in more than 2 places. The fast check is in the hot path (every read/write/flush)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, it's a partial class. Thanks.
But is still safe without a volatile read? Now I believe it's safe, as in the case of _connectionStarted = 0
the CompareExchange is hit which has the proper memory-fences.
using Microsoft.AspNetCore.Server.Kestrel.FunctionalTests; | ||
using Microsoft.AspNetCore.Testing; | ||
using Microsoft.Extensions.Hosting; | ||
using Xunit; | ||
|
||
using KestrelHttpVersion = Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpVersion; | ||
using KestrelHttpMethod = Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpMethod; | ||
|
||
namespace Sockets.FunctionalTests | ||
{ | ||
public class SocketTranspotTests : LoggedTestBase |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not introduced in this change, but file and type are misnamed. Should be SocketTransportTests .
|
||
// Offload these to avoid potentially blocking the first read/write/flush | ||
_receivingTask = Task.Run(DoReceive); | ||
_sendingTask = Task.Run(DoSend); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Task.Run causes an extra Task allocation. The compiler generated Task returned by the DoReceive method, and the wrapping Task created by Task.Run. If you just place await Task.Yield().ConfigureAwait(false);
as the first statement in DoReceive, you will avoid this extra allocation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can live with an extra allocation 😄. There's now 5 more here per connection. These are also long lived so I'm not concerned.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
2 allocations because of DoSend, but fair enough.
|
That's how it started but tests failed because they are dependent on both running. Specifically, the max request buffer size tests depend on the speed of the abort being received during the receive loop. So that can be fixed but I don't want to risk any subtle behavior changes here |
Application = pair.Application; | ||
|
||
Transport = new SocketDuplexPipe(this); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit:
Transport = new SocketDuplexPipe(this); | |
Transport = new SocketDuplexPipe(this, pair.Transport); |
And remove InnerTransport
. Edit: Or just reference _originalTransport
directly in SocketDuplexPipe since it's nested.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I kind prefer using properties rather than a private field, even if it's internal. This is purely a stylistic thing though.
@@ -106,6 +108,9 @@ public override void Abort(ConnectionAbortedException abortReason) | |||
// Only called after connection middleware is complete which means the ConnectionClosed token has fired. | |||
public override async ValueTask DisposeAsync() | |||
{ | |||
// Just in case we haven't started the connection, start it here so we can clean up properly. | |||
EnsureStarted(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess we can remove the _receivingTask != null
and _sendingTask != null
checks now. Technically these could still be null if some background thread flipped _connectionStarted
and hasn't set these yet, but that would indicate some misusage of the ConnectionContext during dispose.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe it wouldn't be a misusage if the read or write was canceled before disposing.
I didn't run a performance profile, so we should look out for regressions here. EnsureStarted should be optimized but it's in every call to @BrennanConroy @Pilchie @DamianEdwards @halter73 @sebastienros |
I'm more concerned with the extra struct copies than EnsureStarted, but I'm guessing both will be insignificant. Good to keep an eye on though. |
Struct copies? |
You mean the wrapped |
This reverts commit e2acbb9.
This change was reverted 952ccb4 |
Since it was reverted, next possibility is this PR: |
Fixes #34344
PS: I'd like to write a test that does theDuplicateAndClose
but I need to figure out how involved that is (and how flaky the test would end up being...).I added a test that does DuplicateAndClose to the same process to make sure the scenario works.