-
Notifications
You must be signed in to change notification settings - Fork 4.8k
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
[QUIC] API QuicListener #71579
[QUIC] API QuicListener #71579
Conversation
Note regarding the This serves as a reminder for when your PR is modifying a ref *.cs file and adding/modifying public APIs, to please make sure the API implementation in the src *.cs file is documented with triple slash comments, so the PR reviewers can sign off that change. |
Tagging subscribers to this area: @dotnet/ncl |
src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/ValueTaskSource.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/ValueTaskSource.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/ValueTaskSource.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Net.Quic/src/System/Net/Quic/Interop/MsQuicSafeHandle.cs
Outdated
Show resolved
Hide resolved
// This is used for our logging that uses the same format of object identification as MsQuic to easily correlate log events. | ||
private static readonly string[] TypeName = new string[] | ||
{ | ||
" reg", |
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.
" reg", | |
"reg", |
Is the space a typo?
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 intentional. The strings correspond to msquic logging strings which use the space to align everything in the log file.
src/libraries/System.Net.Quic/src/System/Net/Quic/QuicListener.PendingConnection.cs
Outdated
Show resolved
Hide resolved
{ | ||
if (!IsSupported) | ||
ref var data = ref listenerEvent.NEW_CONNECTION; |
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.
Concrete type instead of var
.
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.
This is the only place where it's not reasonable since the type name is generated from an anonymous union and looks like this: _Anonymous_e__Union._NEW_CONNECTION_e__Struct
. So I'd prefer to keep the var
here.
src/libraries/System.Net.Quic/src/System/Net/Quic/QuicListener.cs
Outdated
Show resolved
Hide resolved
97ad0ce
to
1329933
Compare
1329933
to
035a47a
Compare
Failing test is #45868 |
/azp run runtime-libraries-coreclr outerloop |
Azure Pipelines successfully started running 1 pipeline(s). |
/azp run runtime-libraries stress-http |
Azure Pipelines successfully started running 1 pipeline(s). |
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectHelper.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectHelper.cs
Outdated
Show resolved
Hide resolved
{ | ||
// MsQuic always uses storage size as if IPv6 was used | ||
Span<byte> addressBytes = new Span<byte>((byte*)pInetAddress, Internals.SocketAddress.IPv6AddressSize); | ||
Span<byte> addressBytes = new Span<byte>((byte*)Unsafe.AsPointer(ref quicAddress), Internals.SocketAddress.IPv6AddressSize); |
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.
This is potential corruption if quicAddress isn't fixed in memory. I assume it is, in which case it's worth a comment.
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.
It's native memory, so it cannot be moved by GC. I'll add a comment.
settings.IsSet.IdleTimeoutMs = 1; | ||
if (options.IdleTimeout != Timeout.InfiniteTimeSpan) | ||
{ | ||
if (options.IdleTimeout <= TimeSpan.Zero) throw new Exception("IdleTimeout must not be negative."); |
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.
This should throw something derived from Exception, and the message should be in the resx.
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.
Addressed in #71783 apart from the message.
_state.ListenerState = listenerState; | ||
_remoteEndPoint = info->RemoteAddress->ToIPEndPoint(); | ||
_localEndPoint = info->LocalAddress->ToIPEndPoint(); | ||
_negotiatedAlpnProtocol = new SslApplicationProtocol(new Span<byte>(info->NegotiatedAlpn, info->NegotiatedAlpnLength).ToArray()); |
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.
Is NegotiatedAlpn here likely one of only a few common values? If yes, you could special-case the most common ones in order to avoid the array 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.
We might. We do this when passing ALPN from HttpClient to QuicConnection. On the QUIC level we'd have to be opinionated, but we could be biased towards H/3. At the moment, I will leave it as-is. We had it like this till now, it's not gonna move the needle significantly. We can improve it in the future.
Exception ex = new MsQuicException(connectionEvent.SHUTDOWN_INITIATED_BY_TRANSPORT.Status, "Connection has been shutdown by transport"); | ||
state.ConnectTcs!.SetException(ExceptionDispatchInfo.SetCurrentStackTrace(ex)); | ||
state.ConnectTcs = null; | ||
state.ConnectTcs.TrySetException(new MsQuicException(connectionEvent.SHUTDOWN_INITIATED_BY_TRANSPORT.Status, "Connection has been shutdown by transport")); |
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.
Here and elsewhere, .resx.
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.
We should address this in #71432 which touches all the exceptions. I'm merely shuffling code here.
} | ||
else | ||
{ | ||
IPAddress[] addresses = Dns.GetHostAddressesAsync(dnsHost, cancellationToken).GetAwaiter().GetResult(); |
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.
We shouldn't be doing I/O synchronously like this. This is in a ConnectAsync
method... why can't it be async and this be awaited?
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.
Only code shuffle, came from #71428, see #71428 (comment). Addressed in #71783
else | ||
{ | ||
IPAddress[] addresses = Dns.GetHostAddressesAsync(dnsHost, cancellationToken).GetAwaiter().GetResult(); | ||
cancellationToken.ThrowIfCancellationRequested(); |
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.
Why is this needed?
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.
Based on @wfurt comment: #71428 (comment), on some platforms (e.g. Linux) the token is not processed by the resolution. I'm only shuffling code here.
/// Idle timeout for connections, after which the connection will be closed. | ||
/// Default <see cref="TimeSpan.Zero"/> means underlying implementation default idle timeout. | ||
/// </summary> | ||
public TimeSpan IdleTimeout { get; set; } = TimeSpan.Zero; |
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.
Zero seems a bit inconsistent for this. With SocketsHttpHandler, if you set its idle timeout to 0, that means connections won't be pooled since they'll be torn down the moment they're added to the pool.
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.
We should discuss this in API review next Tuesday. I summarized the reasoning here: #68902 (comment), but I'm open to changes. The zero meaning here is trying to be consistent with ListenBacklog = 0
meaning use whatever is the best for the current platform.
Apart from this, 0 in msquic means infinite (for which we already have -1), and we don't have a mechanism to automatically close the connection after ... I don't even know what would be single use in this space, one outbound stream?
Moreover, there's no explicit pooling, it's a timeout to automatically tear down a connection if no data flow on it. So it's slightly different concept than SocketsHttpHandler.
MaxBidirectionalStreams = 0; | ||
MaxUnidirectionalStreams = 0; |
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.
These look to be zero'ing values that are already zero.
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.
Yes, I wanted to be explicit and consistent with QuicServerConnectionOptions
ctor. Showing the defaults so that you can see them and understand them in the same manner as for the server connection options.
/// <summary> | ||
/// Initializes a new instance of the <see cref="QuicClientConnectionOptions"/> class. | ||
/// </summary> | ||
public QuicClientConnectionOptions() |
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.
Our style generally has us putting ctors before properties.
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.
Addressed in #71783.
src/libraries/System.Net.Quic/src/System/Net/Quic/QuicListener.PendingConnection.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Net.Quic/src/System/Net/Quic/QuicListener.cs
Outdated
Show resolved
Hide resolved
6fbd597
to
762ec71
Compare
5e558d3
to
25276ed
Compare
25276ed
to
4e5b919
Compare
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.
Generally LGTM. I would love if @stephentoub could take a closer look at ValueTaskSource
though (I also wonder why we don't have a default implementation for the interface, the implementation seems default enough, but now it's hidden inside Quic?)
src/libraries/System.Net.Quic/src/System/Net/Quic/Interop/MsQuicSafeHandle.cs
Show resolved
Hide resolved
src/libraries/System.Net.Quic/src/System/Net/Quic/Interop/MsQuicSafeHandle.cs
Show resolved
Hide resolved
_cancellationTokenSource = new CancellationTokenSource(); | ||
} | ||
|
||
public async void StartHandshake(QuicConnection connection, SslClientHelloInfo clientHello, Func<QuicConnection, SslClientHelloInfo, CancellationToken, ValueTask<QuicServerConnectionOptions>> connectionOptionsCallback) |
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'm a bit confused at the naming. While the name is "StartHandshake", the method actually not only starts, but completes the whole handshake as well (it awaits connection.FinishHandshakeAsync).
I understand that the method is never awaited (judging by the (only?) usage in QuicListener.cs)
// Kicks off the rest of the handshake in the background.
pendingConnection.StartHandshake(connection, clientHello, _connectionOptionsCallback);
so I wonder whether this method should either be synchronous (and hide kicking off the whole operation inside), or somehow not await FinishHandshakeAsync
, or be named without "Start" prefix?
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.
The method is async void
and is like that on purpose. When a new connection arrives, we want to kick off the handshake process and let it finish on the background. Then, we need a vehicle to actually get the handshake result (ie: the connected connection), for which we use _finishHandshakeTask
exposed by PendingConnection.FinishHandshakeAsync
and used in AcceptConnectionAsync
.
In another words, we don't want to and cannot await it from NEW_CONNECTION
, but we also need to react to the result of QuicConnection.FinishHandshakeAsync
--> async void
. Other option would be to do the same thing with Task.ContinueWith
, but everything returns ValueTask
here, so this seemed more straightforward.
Does this help? Do you want me to improve the comments around this? If this helped you to understand it better, I can move it into the code comments, just let me know which part makes it more clear. It's hard for me to see which explanation helps since I wrote the code 😅
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 did understand that you don't ever want to await it (though I missed it's async void
), what was strange to me is that method named "Start..." waits until finish 😅 Maybe adding a comment highlighting that it is called Start.. because it is async void so it is never awaited will be better 😅
P.S.: I expected to see something like this
runtime/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs
Lines 502 to 506 in e55c908
// Queue the creation of the connection to escape the held lock | |
ThreadPool.QueueUserWorkItem(static state => | |
{ | |
_ = state.thisRef.AddHttp11ConnectionAsync(state.queueItem); // ignore returned task | |
}, (thisRef: this, queueItem), preferLocal: true); |
src/libraries/System.Net.Quic/src/System/Net/Quic/QuicListener.cs
Outdated
Show resolved
Hide resolved
That's what |
453f6aa
to
61ce63f
Compare
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.
LGTM, thanks!
@JamesNK you'll need to react to this, especially around the |
Fixes #67560
Contributes to #68902