Skip to content
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

Merged
merged 5 commits into from
Jul 20, 2021

Conversation

davidfowl
Copy link
Member

@davidfowl davidfowl commented Jul 17, 2021

  • Today when the socket connection is accepted or connected, 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.

Fixes #34344

PS: I'd like to write a test that does the DuplicateAndClose 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.

- 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.
{
try
if (_connectionStarted == 1 || Interlocked.CompareExchange(ref _connectionStarted, 1, 0) == 1)
Copy link
Member

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?

Copy link
Member Author

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)

Copy link
Member

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

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);
Copy link
Member

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.

Copy link
Member Author

@davidfowl davidfowl Jul 19, 2021

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.

Copy link
Member

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.

@halter73
Copy link
Member

halter73 commented Jul 20, 2021

Any reason not to start send and receive loops independently? Edit. The opposite is the better question. It's not worth it.

@davidfowl
Copy link
Member Author

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);
Copy link
Member

@halter73 halter73 Jul 20, 2021

Choose a reason for hiding this comment

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

Nit:

Suggested change
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.

Copy link
Member Author

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();
Copy link
Member

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.

Copy link
Member

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.

@davidfowl davidfowl merged commit e2acbb9 into main Jul 20, 2021
@davidfowl davidfowl deleted the davidfowl/lazy-socket-connection branch July 20, 2021 04:30
@ghost ghost added this to the 6.0-rc1 milestone Jul 20, 2021
@davidfowl
Copy link
Member Author

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 ReadAsync which could affect benchmarks...

@BrennanConroy @Pilchie @DamianEdwards @halter73 @sebastienros

@halter73
Copy link
Member

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.

@davidfowl
Copy link
Member Author

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?

@davidfowl
Copy link
Member Author

You mean the wrapped PipeReader.ReadAsync() calls?

davidfowl added a commit that referenced this pull request Aug 12, 2021
@sebastienros
Copy link
Member

Might have had some impact on PlaintextMapAction

image

@davidfowl
Copy link
Member Author

This change was reverted 952ccb4

@sebastienros
Copy link
Member

Since it was reverted, next possibility is this PR:
#34285

@amcasey amcasey added area-networking Includes servers, yarp, json patch, bedrock, websockets, http client factory, and http abstractions and removed area-runtime labels Jun 2, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-networking Includes servers, yarp, json patch, bedrock, websockets, http client factory, and http abstractions feature-kestrel
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Allow customizing Socket.AcceptAsync() call
7 participants