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

[QUIC] API QuicListener #71579

Merged
merged 14 commits into from
Jul 10, 2022
Merged

[QUIC] API QuicListener #71579

merged 14 commits into from
Jul 10, 2022

Conversation

ManickaP
Copy link
Member

@ManickaP ManickaP commented Jul 2, 2022

Fixes #67560
Contributes to #68902

@dotnet-issue-labeler
Copy link

Note regarding the new-api-needs-documentation label:

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.

@ghost
Copy link

ghost commented Jul 2, 2022

Tagging subscribers to this area: @dotnet/ncl
See info in area-owners.md if you want to be subscribed.

Issue Details

Fixes #67560
Contributes to #68902

Author: ManickaP
Assignees: -
Labels:

new-api-needs-documentation, area-System.Net.Quic

Milestone: -

// 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",
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
" reg",
"reg",

Is the space a typo?

Copy link
Member Author

@ManickaP ManickaP Jul 2, 2022

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.

{
if (!IsSupported)
ref var data = ref listenerEvent.NEW_CONNECTION;
Copy link
Member

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.

Copy link
Member Author

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.

@ManickaP ManickaP force-pushed the mapichov/quic-listener branch 2 times, most recently from 97ad0ce to 1329933 Compare July 4, 2022 10:13
@ManickaP
Copy link
Member Author

ManickaP commented Jul 5, 2022

Failing test is #45868

@ManickaP
Copy link
Member Author

ManickaP commented Jul 5, 2022

/azp run runtime-libraries-coreclr outerloop

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@ManickaP
Copy link
Member Author

ManickaP commented Jul 5, 2022

/azp run runtime-libraries stress-http

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@ManickaP ManickaP marked this pull request as ready for review July 5, 2022 16:17
{
// 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);
Copy link
Member

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.

Copy link
Member Author

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

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.

Copy link
Member Author

@ManickaP ManickaP Jul 8, 2022

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

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.

Copy link
Member Author

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

Choose a reason for hiding this comment

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

Here and elsewhere, .resx.

Copy link
Member Author

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

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?

Copy link
Member Author

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

Choose a reason for hiding this comment

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

Why is this needed?

Copy link
Member Author

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

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.

Copy link
Member Author

@ManickaP ManickaP Jul 8, 2022

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.

Comment on lines +63 to +64
MaxBidirectionalStreams = 0;
MaxUnidirectionalStreams = 0;
Copy link
Member

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.

Copy link
Member Author

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

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.

Copy link
Member Author

@ManickaP ManickaP Jul 8, 2022

Choose a reason for hiding this comment

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

Addressed in #71783.

@ManickaP ManickaP force-pushed the mapichov/quic-listener branch 2 times, most recently from 5e558d3 to 25276ed Compare July 8, 2022 13:16
Copy link
Member

@CarnaViire CarnaViire left a 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?)

_cancellationTokenSource = new CancellationTokenSource();
}

public async void StartHandshake(QuicConnection connection, SslClientHelloInfo clientHello, Func<QuicConnection, SslClientHelloInfo, CancellationToken, ValueTask<QuicServerConnectionOptions>> connectionOptionsCallback)
Copy link
Member

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?

Copy link
Member Author

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 😅

Copy link
Member

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

// 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);

@stephentoub
Copy link
Member

I also wonder why we don't have a default implementation for the interface, the implementation seems default enough

That's what ManualResetValueTaskSourceCore<T> provides, the core logic for implementing an IValueTaskSource and IValueTaskSource<T>. The most basic implementation of the interfaces can effectively just delegate to members of the struct. More complicated implementations can use the MRVTSC as part of the implementation but layer on additional functionality (as this one does, with additional logic around GCHandles and cancellation and whatnot).

Copy link
Member

@CarnaViire CarnaViire 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! :shipit:

@ManickaP ManickaP merged commit 60af631 into dotnet:main Jul 10, 2022
@ManickaP ManickaP deleted the mapichov/quic-listener branch July 10, 2022 20:13
@ManickaP
Copy link
Member Author

@JamesNK you'll need to react to this, especially around the QuicListenerOptions where we added the callback.

@karelz karelz added this to the 7.0.0 milestone Jul 19, 2022
@ghost ghost locked as resolved and limited conversation to collaborators Aug 18, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[API Proposal]: [QUIC] QuicListener
5 participants