[QUIC] API QuicListener - #71579

Merged
ManickaP merged 14 commits into
dotnet:mainfrom
ManickaP:mapichov/quic-listener
Jul 10, 2022
Merged

[QUIC] API QuicListener#71579
ManickaP merged 14 commits into
dotnet:mainfrom
ManickaP:mapichov/quic-listener

Conversation

@ManickaP

Copy link
Copy Markdown
Member

Fixes#67560
Contributes to #68902

@ghost

ghost commented Jul 2, 2022

Copy link
Copy Markdown

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

ghost commented Jul 2, 2022

Copy link
Copy Markdown

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
Copy Markdown
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?

@ManickaPManickaPJul 2, 2022

Copy link
Copy Markdown
MemberAuthor

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 (listenerEvent.Type == QUIC_LISTENER_EVENT_TYPE.NEW_CONNECTION)
{
if (!IsSupported)
ref var data = ref listenerEvent.NEW_CONNECTION;

Copy link
Copy Markdown
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
Copy Markdown
MemberAuthor

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.

Comment threadsrc/libraries/System.Net.Quic/src/System/Net/Quic/QuicListener.cs Outdated
@ManickaP
ManickaPforce-pushed the mapichov/quic-listener branch 2 times, most recently from 97ad0ce to 1329933CompareJuly 4, 2022 10:13
@ManickaP
ManickaPforce-pushed the mapichov/quic-listener branch from 1329933 to 035a47aCompareJuly 5, 2022 09:18
@ManickaP

Copy link
Copy Markdown
MemberAuthor

Failing test is #45868

@ManickaP

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-libraries-coreclr outerloop

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@ManickaP

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-libraries stress-http

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@ManickaP
ManickaP marked this pull request as ready for review July 5, 2022 16:17
@ManickaPManickaP mentioned this pull request Jul 7, 2022
Comment threadsrc/libraries/System.Net.Quic/src/Resources/Strings.resx Outdated
{
// 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
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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.

@ManickaPManickaPJul 8, 2022

Copy link
Copy Markdown
MemberAuthor

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
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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.

@ManickaPManickaPJul 8, 2022

Copy link
Copy Markdown
MemberAuthor

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
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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.

@ManickaPManickaPJul 8, 2022

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Addressed in #71783.

Comment threadsrc/libraries/System.Net.Quic/src/System/Net/Quic/QuicListener.cs Outdated
@ManickaP
ManickaPforce-pushed the mapichov/quic-listener branch from 6fbd597 to 762ec71CompareJuly 8, 2022 10:04
@ManickaP
ManickaPforce-pushed the mapichov/quic-listener branch 2 times, most recently from 5e558d3 to 25276edCompareJuly 8, 2022 13:16
@ManickaP
ManickaPforce-pushed the mapichov/quic-listener branch from 25276ed to 4e5b919CompareJuly 8, 2022 13:25

@CarnaViireCarnaViire left a comment

Copy link
Copy Markdown
Member

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?)

Comment threadsrc/libraries/System.Net.Quic/src/System.Net.Quic.csproj Outdated
_cancellationTokenSource = new CancellationTokenSource();
}

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

Copy link
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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);

Comment threadsrc/libraries/System.Net.Quic/src/System/Net/Quic/QuicListener.cs Outdated
@stephentoub

Copy link
Copy Markdown
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).

@ManickaP
ManickaPforce-pushed the mapichov/quic-listener branch from 453f6aa to 61ce63fCompareJuly 8, 2022 17:46

@CarnaViireCarnaViire left a comment

Copy link
Copy Markdown
Member

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:mainJul 10, 2022
@ManickaP
ManickaP deleted the mapichov/quic-listener branch July 10, 2022 20:13
@ManickaP

Copy link
Copy Markdown
MemberAuthor

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

@karelzkarelz added this to the 7.0.0 milestone Jul 19, 2022
@ghostghost locked as resolved and limited conversation to collaborators Aug 18, 2022
Sign up for freeto 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

@ManickaP@stephentoub@CarnaViire@gfoidl@karelz
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

[QUIC] API QuicListener - #71579

Merged
ManickaP merged 14 commits into
dotnet:mainfrom
ManickaP:mapichov/quic-listener
Jul 10, 2022
Merged

[QUIC] API QuicListener#71579
ManickaP merged 14 commits into
dotnet:mainfrom
ManickaP:mapichov/quic-listener

Conversation

@ManickaP

Copy link
Copy Markdown
Member

Fixes#67560
Contributes to #68902

@ghost

ghost commented Jul 2, 2022

Copy link
Copy Markdown

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

ghost commented Jul 2, 2022

Copy link
Copy Markdown

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
Copy Markdown
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?

@ManickaPManickaPJul 2, 2022

Copy link
Copy Markdown
MemberAuthor

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 (listenerEvent.Type == QUIC_LISTENER_EVENT_TYPE.NEW_CONNECTION)
{
if (!IsSupported)
ref var data = ref listenerEvent.NEW_CONNECTION;

Copy link
Copy Markdown
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
Copy Markdown
MemberAuthor

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.

Comment threadsrc/libraries/System.Net.Quic/src/System/Net/Quic/QuicListener.cs Outdated
@ManickaP
ManickaPforce-pushed the mapichov/quic-listener branch 2 times, most recently from 97ad0ce to 1329933CompareJuly 4, 2022 10:13
@ManickaP
ManickaPforce-pushed the mapichov/quic-listener branch from 1329933 to 035a47aCompareJuly 5, 2022 09:18
@ManickaP

Copy link
Copy Markdown
MemberAuthor

Failing test is #45868

@ManickaP

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-libraries-coreclr outerloop

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@ManickaP

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-libraries stress-http

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@ManickaP
ManickaP marked this pull request as ready for review July 5, 2022 16:17
@ManickaPManickaP mentioned this pull request Jul 7, 2022
Comment threadsrc/libraries/System.Net.Quic/src/Resources/Strings.resx Outdated
{
// 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
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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.

@ManickaPManickaPJul 8, 2022

Copy link
Copy Markdown
MemberAuthor

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
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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.

@ManickaPManickaPJul 8, 2022

Copy link
Copy Markdown
MemberAuthor

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
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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.

@ManickaPManickaPJul 8, 2022

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Addressed in #71783.

Comment threadsrc/libraries/System.Net.Quic/src/System/Net/Quic/QuicListener.cs Outdated
@ManickaP
ManickaPforce-pushed the mapichov/quic-listener branch from 6fbd597 to 762ec71CompareJuly 8, 2022 10:04
@ManickaP
ManickaPforce-pushed the mapichov/quic-listener branch 2 times, most recently from 5e558d3 to 25276edCompareJuly 8, 2022 13:16
@ManickaP
ManickaPforce-pushed the mapichov/quic-listener branch from 25276ed to 4e5b919CompareJuly 8, 2022 13:25

@CarnaViireCarnaViire left a comment

Copy link
Copy Markdown
Member

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?)

Comment threadsrc/libraries/System.Net.Quic/src/System.Net.Quic.csproj Outdated
_cancellationTokenSource = new CancellationTokenSource();
}

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

Copy link
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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);

Comment threadsrc/libraries/System.Net.Quic/src/System/Net/Quic/QuicListener.cs Outdated
@stephentoub

Copy link
Copy Markdown
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).

@ManickaP
ManickaPforce-pushed the mapichov/quic-listener branch from 453f6aa to 61ce63fCompareJuly 8, 2022 17:46

@CarnaViireCarnaViire left a comment

Copy link
Copy Markdown
Member

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:mainJul 10, 2022
@ManickaP
ManickaP deleted the mapichov/quic-listener branch July 10, 2022 20:13
@ManickaP

Copy link
Copy Markdown
MemberAuthor

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

@karelzkarelz added this to the 7.0.0 milestone Jul 19, 2022
@ghostghost locked as resolved and limited conversation to collaborators Aug 18, 2022
Sign up for freeto 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

@ManickaP@stephentoub@CarnaViire@gfoidl@karelz
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[QUIC] API QuicListener - #71579

Merged
ManickaP merged 14 commits into
dotnet:mainfrom
ManickaP:mapichov/quic-listener
Jul 10, 2022
Merged

[QUIC] API QuicListener#71579
ManickaP merged 14 commits into
dotnet:mainfrom
ManickaP:mapichov/quic-listener

Conversation

@ManickaP

Copy link
Copy Markdown
Member

Fixes#67560
Contributes to #68902

@ghost

ghost commented Jul 2, 2022

Copy link
Copy Markdown

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

ghost commented Jul 2, 2022

Copy link
Copy Markdown

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
Copy Markdown
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?

@ManickaPManickaPJul 2, 2022

Copy link
Copy Markdown
MemberAuthor

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 (listenerEvent.Type == QUIC_LISTENER_EVENT_TYPE.NEW_CONNECTION)
{
if (!IsSupported)
ref var data = ref listenerEvent.NEW_CONNECTION;

Copy link
Copy Markdown
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
Copy Markdown
MemberAuthor

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.

Comment threadsrc/libraries/System.Net.Quic/src/System/Net/Quic/QuicListener.cs Outdated
@ManickaP
ManickaPforce-pushed the mapichov/quic-listener branch 2 times, most recently from 97ad0ce to 1329933CompareJuly 4, 2022 10:13
@ManickaP
ManickaPforce-pushed the mapichov/quic-listener branch from 1329933 to 035a47aCompareJuly 5, 2022 09:18
@ManickaP

Copy link
Copy Markdown
MemberAuthor

Failing test is #45868

@ManickaP

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-libraries-coreclr outerloop

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@ManickaP

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-libraries stress-http

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@ManickaP
ManickaP marked this pull request as ready for review July 5, 2022 16:17
@ManickaPManickaP mentioned this pull request Jul 7, 2022
Comment threadsrc/libraries/System.Net.Quic/src/Resources/Strings.resx Outdated
{
// 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
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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.

@ManickaPManickaPJul 8, 2022

Copy link
Copy Markdown
MemberAuthor

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
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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.

@ManickaPManickaPJul 8, 2022

Copy link
Copy Markdown
MemberAuthor

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
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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.

@ManickaPManickaPJul 8, 2022

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Addressed in #71783.

Comment threadsrc/libraries/System.Net.Quic/src/System/Net/Quic/QuicListener.cs Outdated
@ManickaP
ManickaPforce-pushed the mapichov/quic-listener branch from 6fbd597 to 762ec71CompareJuly 8, 2022 10:04
@ManickaP
ManickaPforce-pushed the mapichov/quic-listener branch 2 times, most recently from 5e558d3 to 25276edCompareJuly 8, 2022 13:16
@ManickaP
ManickaPforce-pushed the mapichov/quic-listener branch from 25276ed to 4e5b919CompareJuly 8, 2022 13:25

@CarnaViireCarnaViire left a comment

Copy link
Copy Markdown
Member

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?)

Comment threadsrc/libraries/System.Net.Quic/src/System.Net.Quic.csproj Outdated
_cancellationTokenSource = new CancellationTokenSource();
}

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

Copy link
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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);

Comment threadsrc/libraries/System.Net.Quic/src/System/Net/Quic/QuicListener.cs Outdated
@stephentoub

Copy link
Copy Markdown
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).

@ManickaP
ManickaPforce-pushed the mapichov/quic-listener branch from 453f6aa to 61ce63fCompareJuly 8, 2022 17:46

@CarnaViireCarnaViire left a comment

Copy link
Copy Markdown
Member

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:mainJul 10, 2022
@ManickaP
ManickaP deleted the mapichov/quic-listener branch July 10, 2022 20:13
@ManickaP

Copy link
Copy Markdown
MemberAuthor

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

@karelzkarelz added this to the 7.0.0 milestone Jul 19, 2022
@ghostghost locked as resolved and limited conversation to collaborators Aug 18, 2022
Sign up for freeto 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

@ManickaP@stephentoub@CarnaViire@gfoidl@karelz
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[QUIC] API QuicListener - #71579

Merged
ManickaP merged 14 commits into
dotnet:mainfrom
ManickaP:mapichov/quic-listener
Jul 10, 2022
Merged

[QUIC] API QuicListener#71579
ManickaP merged 14 commits into
dotnet:mainfrom
ManickaP:mapichov/quic-listener

Conversation

@ManickaP

Copy link
Copy Markdown
Member

Fixes#67560
Contributes to #68902

@ghost

ghost commented Jul 2, 2022

Copy link
Copy Markdown

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

ghost commented Jul 2, 2022

Copy link
Copy Markdown

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
Copy Markdown
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?

@ManickaPManickaPJul 2, 2022

Copy link
Copy Markdown
MemberAuthor

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 (listenerEvent.Type == QUIC_LISTENER_EVENT_TYPE.NEW_CONNECTION)
{
if (!IsSupported)
ref var data = ref listenerEvent.NEW_CONNECTION;

Copy link
Copy Markdown
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
Copy Markdown
MemberAuthor

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.

Comment threadsrc/libraries/System.Net.Quic/src/System/Net/Quic/QuicListener.cs Outdated
@ManickaP
ManickaPforce-pushed the mapichov/quic-listener branch 2 times, most recently from 97ad0ce to 1329933CompareJuly 4, 2022 10:13
@ManickaP
ManickaPforce-pushed the mapichov/quic-listener branch from 1329933 to 035a47aCompareJuly 5, 2022 09:18
@ManickaP

Copy link
Copy Markdown
MemberAuthor

Failing test is #45868

@ManickaP

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-libraries-coreclr outerloop

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@ManickaP

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-libraries stress-http

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@ManickaP
ManickaP marked this pull request as ready for review July 5, 2022 16:17
@ManickaPManickaP mentioned this pull request Jul 7, 2022
Comment threadsrc/libraries/System.Net.Quic/src/Resources/Strings.resx Outdated
{
// 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
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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.

@ManickaPManickaPJul 8, 2022

Copy link
Copy Markdown
MemberAuthor

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
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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.

@ManickaPManickaPJul 8, 2022

Copy link
Copy Markdown
MemberAuthor

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
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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.

@ManickaPManickaPJul 8, 2022

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Addressed in #71783.

Comment threadsrc/libraries/System.Net.Quic/src/System/Net/Quic/QuicListener.cs Outdated
@ManickaP
ManickaPforce-pushed the mapichov/quic-listener branch from 6fbd597 to 762ec71CompareJuly 8, 2022 10:04
@ManickaP
ManickaPforce-pushed the mapichov/quic-listener branch 2 times, most recently from 5e558d3 to 25276edCompareJuly 8, 2022 13:16
@ManickaP
ManickaPforce-pushed the mapichov/quic-listener branch from 25276ed to 4e5b919CompareJuly 8, 2022 13:25

@CarnaViireCarnaViire left a comment

Copy link
Copy Markdown
Member

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?)

Comment threadsrc/libraries/System.Net.Quic/src/System.Net.Quic.csproj Outdated
_cancellationTokenSource = new CancellationTokenSource();
}

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

Copy link
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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);

Comment threadsrc/libraries/System.Net.Quic/src/System/Net/Quic/QuicListener.cs Outdated
@stephentoub

Copy link
Copy Markdown
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).

@ManickaP
ManickaPforce-pushed the mapichov/quic-listener branch from 453f6aa to 61ce63fCompareJuly 8, 2022 17:46

@CarnaViireCarnaViire left a comment

Copy link
Copy Markdown
Member

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:mainJul 10, 2022
@ManickaP
ManickaP deleted the mapichov/quic-listener branch July 10, 2022 20:13
@ManickaP

Copy link
Copy Markdown
MemberAuthor

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

@karelzkarelz added this to the 7.0.0 milestone Jul 19, 2022
@ghostghost locked as resolved and limited conversation to collaborators Aug 18, 2022
Sign up for freeto 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

@ManickaP@stephentoub@CarnaViire@gfoidl@karelz
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

[QUIC] API QuicListener - #71579

Merged
ManickaP merged 14 commits into
dotnet:mainfrom
ManickaP:mapichov/quic-listener
Jul 10, 2022
Merged

[QUIC] API QuicListener#71579
ManickaP merged 14 commits into
dotnet:mainfrom
ManickaP:mapichov/quic-listener

Conversation

@ManickaP

Copy link
Copy Markdown
Member

Fixes#67560
Contributes to #68902

@ghost

ghost commented Jul 2, 2022

Copy link
Copy Markdown

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

ghost commented Jul 2, 2022

Copy link
Copy Markdown

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
Copy Markdown
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?

@ManickaPManickaPJul 2, 2022

Copy link
Copy Markdown
MemberAuthor

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 (listenerEvent.Type == QUIC_LISTENER_EVENT_TYPE.NEW_CONNECTION)
{
if (!IsSupported)
ref var data = ref listenerEvent.NEW_CONNECTION;

Copy link
Copy Markdown
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
Copy Markdown
MemberAuthor

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.

Comment threadsrc/libraries/System.Net.Quic/src/System/Net/Quic/QuicListener.cs Outdated
@ManickaP
ManickaPforce-pushed the mapichov/quic-listener branch 2 times, most recently from 97ad0ce to 1329933CompareJuly 4, 2022 10:13
@ManickaP
ManickaPforce-pushed the mapichov/quic-listener branch from 1329933 to 035a47aCompareJuly 5, 2022 09:18
@ManickaP

Copy link
Copy Markdown
MemberAuthor

Failing test is #45868

@ManickaP

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-libraries-coreclr outerloop

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@ManickaP

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-libraries stress-http

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@ManickaP
ManickaP marked this pull request as ready for review July 5, 2022 16:17
@ManickaPManickaP mentioned this pull request Jul 7, 2022
Comment threadsrc/libraries/System.Net.Quic/src/Resources/Strings.resx Outdated
{
// 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
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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.

@ManickaPManickaPJul 8, 2022

Copy link
Copy Markdown
MemberAuthor

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
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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.

@ManickaPManickaPJul 8, 2022

Copy link
Copy Markdown
MemberAuthor

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
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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.

@ManickaPManickaPJul 8, 2022

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Addressed in #71783.

Comment threadsrc/libraries/System.Net.Quic/src/System/Net/Quic/QuicListener.cs Outdated
@ManickaP
ManickaPforce-pushed the mapichov/quic-listener branch from 6fbd597 to 762ec71CompareJuly 8, 2022 10:04
@ManickaP
ManickaPforce-pushed the mapichov/quic-listener branch 2 times, most recently from 5e558d3 to 25276edCompareJuly 8, 2022 13:16
@ManickaP
ManickaPforce-pushed the mapichov/quic-listener branch from 25276ed to 4e5b919CompareJuly 8, 2022 13:25

@CarnaViireCarnaViire left a comment

Copy link
Copy Markdown
Member

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?)

Comment threadsrc/libraries/System.Net.Quic/src/System.Net.Quic.csproj Outdated
_cancellationTokenSource = new CancellationTokenSource();
}

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

Copy link
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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);

Comment threadsrc/libraries/System.Net.Quic/src/System/Net/Quic/QuicListener.cs Outdated
@stephentoub

Copy link
Copy Markdown
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).

@ManickaP
ManickaPforce-pushed the mapichov/quic-listener branch from 453f6aa to 61ce63fCompareJuly 8, 2022 17:46

@CarnaViireCarnaViire left a comment

Copy link
Copy Markdown
Member

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:mainJul 10, 2022
@ManickaP
ManickaP deleted the mapichov/quic-listener branch July 10, 2022 20:13
@ManickaP

Copy link
Copy Markdown
MemberAuthor

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

@karelzkarelz added this to the 7.0.0 milestone Jul 19, 2022
@ghostghost locked as resolved and limited conversation to collaborators Aug 18, 2022
Sign up for freeto 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

@ManickaP@stephentoub@CarnaViire@gfoidl@karelz
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[QUIC] API QuicListener - #71579

Merged
ManickaP merged 14 commits into
dotnet:mainfrom
ManickaP:mapichov/quic-listener
Jul 10, 2022
Merged

[QUIC] API QuicListener#71579
ManickaP merged 14 commits into
dotnet:mainfrom
ManickaP:mapichov/quic-listener

Conversation

@ManickaP

Copy link
Copy Markdown
Member

Fixes#67560
Contributes to #68902

@ghost

ghost commented Jul 2, 2022

Copy link
Copy Markdown

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

ghost commented Jul 2, 2022

Copy link
Copy Markdown

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
Copy Markdown
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?

@ManickaPManickaPJul 2, 2022

Copy link
Copy Markdown
MemberAuthor

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 (listenerEvent.Type == QUIC_LISTENER_EVENT_TYPE.NEW_CONNECTION)
{
if (!IsSupported)
ref var data = ref listenerEvent.NEW_CONNECTION;

Copy link
Copy Markdown
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
Copy Markdown
MemberAuthor

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.

Comment threadsrc/libraries/System.Net.Quic/src/System/Net/Quic/QuicListener.cs Outdated
@ManickaP
ManickaPforce-pushed the mapichov/quic-listener branch 2 times, most recently from 97ad0ce to 1329933CompareJuly 4, 2022 10:13
@ManickaP
ManickaPforce-pushed the mapichov/quic-listener branch from 1329933 to 035a47aCompareJuly 5, 2022 09:18
@ManickaP

Copy link
Copy Markdown
MemberAuthor

Failing test is #45868

@ManickaP

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-libraries-coreclr outerloop

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@ManickaP

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-libraries stress-http

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@ManickaP
ManickaP marked this pull request as ready for review July 5, 2022 16:17
@ManickaPManickaP mentioned this pull request Jul 7, 2022
Comment threadsrc/libraries/System.Net.Quic/src/Resources/Strings.resx Outdated
{
// 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
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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.

@ManickaPManickaPJul 8, 2022

Copy link
Copy Markdown
MemberAuthor

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
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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.

@ManickaPManickaPJul 8, 2022

Copy link
Copy Markdown
MemberAuthor

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
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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.

@ManickaPManickaPJul 8, 2022

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Addressed in #71783.

Comment threadsrc/libraries/System.Net.Quic/src/System/Net/Quic/QuicListener.cs Outdated
@ManickaP
ManickaPforce-pushed the mapichov/quic-listener branch from 6fbd597 to 762ec71CompareJuly 8, 2022 10:04
@ManickaP
ManickaPforce-pushed the mapichov/quic-listener branch 2 times, most recently from 5e558d3 to 25276edCompareJuly 8, 2022 13:16
@ManickaP
ManickaPforce-pushed the mapichov/quic-listener branch from 25276ed to 4e5b919CompareJuly 8, 2022 13:25

@CarnaViireCarnaViire left a comment

Copy link
Copy Markdown
Member

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?)

Comment threadsrc/libraries/System.Net.Quic/src/System.Net.Quic.csproj Outdated
_cancellationTokenSource = new CancellationTokenSource();
}

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

Copy link
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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);

Comment threadsrc/libraries/System.Net.Quic/src/System/Net/Quic/QuicListener.cs Outdated
@stephentoub

Copy link
Copy Markdown
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).

@ManickaP
ManickaPforce-pushed the mapichov/quic-listener branch from 453f6aa to 61ce63fCompareJuly 8, 2022 17:46

@CarnaViireCarnaViire left a comment

Copy link
Copy Markdown
Member

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:mainJul 10, 2022
@ManickaP
ManickaP deleted the mapichov/quic-listener branch July 10, 2022 20:13
@ManickaP

Copy link
Copy Markdown
MemberAuthor

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

@karelzkarelz added this to the 7.0.0 milestone Jul 19, 2022
@ghostghost locked as resolved and limited conversation to collaborators Aug 18, 2022
Sign up for freeto 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

@ManickaP@stephentoub@CarnaViire@gfoidl@karelz
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[QUIC] API QuicListener - #71579

Merged
ManickaP merged 14 commits into
dotnet:mainfrom
ManickaP:mapichov/quic-listener
Jul 10, 2022
Merged

[QUIC] API QuicListener#71579
ManickaP merged 14 commits into
dotnet:mainfrom
ManickaP:mapichov/quic-listener

Conversation

@ManickaP

Copy link
Copy Markdown
Member

Fixes#67560
Contributes to #68902

@ghost

ghost commented Jul 2, 2022

Copy link
Copy Markdown

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

ghost commented Jul 2, 2022

Copy link
Copy Markdown

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
Copy Markdown
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?

@ManickaPManickaPJul 2, 2022

Copy link
Copy Markdown
MemberAuthor

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 (listenerEvent.Type == QUIC_LISTENER_EVENT_TYPE.NEW_CONNECTION)
{
if (!IsSupported)
ref var data = ref listenerEvent.NEW_CONNECTION;

Copy link
Copy Markdown
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
Copy Markdown
MemberAuthor

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.

Comment threadsrc/libraries/System.Net.Quic/src/System/Net/Quic/QuicListener.cs Outdated
@ManickaP
ManickaPforce-pushed the mapichov/quic-listener branch 2 times, most recently from 97ad0ce to 1329933CompareJuly 4, 2022 10:13
@ManickaP
ManickaPforce-pushed the mapichov/quic-listener branch from 1329933 to 035a47aCompareJuly 5, 2022 09:18
@ManickaP

Copy link
Copy Markdown
MemberAuthor

Failing test is #45868

@ManickaP

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-libraries-coreclr outerloop

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@ManickaP

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-libraries stress-http

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@ManickaP
ManickaP marked this pull request as ready for review July 5, 2022 16:17
@ManickaPManickaP mentioned this pull request Jul 7, 2022
Comment threadsrc/libraries/System.Net.Quic/src/Resources/Strings.resx Outdated
{
// 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
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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.

@ManickaPManickaPJul 8, 2022

Copy link
Copy Markdown
MemberAuthor

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
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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.

@ManickaPManickaPJul 8, 2022

Copy link
Copy Markdown
MemberAuthor

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
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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.

@ManickaPManickaPJul 8, 2022

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Addressed in #71783.

Comment threadsrc/libraries/System.Net.Quic/src/System/Net/Quic/QuicListener.cs Outdated
@ManickaP
ManickaPforce-pushed the mapichov/quic-listener branch from 6fbd597 to 762ec71CompareJuly 8, 2022 10:04
@ManickaP
ManickaPforce-pushed the mapichov/quic-listener branch 2 times, most recently from 5e558d3 to 25276edCompareJuly 8, 2022 13:16
@ManickaP
ManickaPforce-pushed the mapichov/quic-listener branch from 25276ed to 4e5b919CompareJuly 8, 2022 13:25

@CarnaViireCarnaViire left a comment

Copy link
Copy Markdown
Member

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?)

Comment threadsrc/libraries/System.Net.Quic/src/System.Net.Quic.csproj Outdated
_cancellationTokenSource = new CancellationTokenSource();
}

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

Copy link
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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);

Comment threadsrc/libraries/System.Net.Quic/src/System/Net/Quic/QuicListener.cs Outdated
@stephentoub

Copy link
Copy Markdown
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).

@ManickaP
ManickaPforce-pushed the mapichov/quic-listener branch from 453f6aa to 61ce63fCompareJuly 8, 2022 17:46

@CarnaViireCarnaViire left a comment

Copy link
Copy Markdown
Member

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:mainJul 10, 2022
@ManickaP
ManickaP deleted the mapichov/quic-listener branch July 10, 2022 20:13
@ManickaP

Copy link
Copy Markdown
MemberAuthor

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

@karelzkarelz added this to the 7.0.0 milestone Jul 19, 2022
@ghostghost locked as resolved and limited conversation to collaborators Aug 18, 2022
Sign up for freeto 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

@ManickaP@stephentoub@CarnaViire@gfoidl@karelz
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

[QUIC] API QuicListener - #71579

Merged
ManickaP merged 14 commits into
dotnet:mainfrom
ManickaP:mapichov/quic-listener
Jul 10, 2022
Merged

[QUIC] API QuicListener#71579
ManickaP merged 14 commits into
dotnet:mainfrom
ManickaP:mapichov/quic-listener

Conversation

@ManickaP

Copy link
Copy Markdown
Member

Fixes#67560
Contributes to #68902

@ghost

ghost commented Jul 2, 2022

Copy link
Copy Markdown

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

ghost commented Jul 2, 2022

Copy link
Copy Markdown

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
Copy Markdown
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?

@ManickaPManickaPJul 2, 2022

Copy link
Copy Markdown
MemberAuthor

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 (listenerEvent.Type == QUIC_LISTENER_EVENT_TYPE.NEW_CONNECTION)
{
if (!IsSupported)
ref var data = ref listenerEvent.NEW_CONNECTION;

Copy link
Copy Markdown
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
Copy Markdown
MemberAuthor

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.

Comment threadsrc/libraries/System.Net.Quic/src/System/Net/Quic/QuicListener.cs Outdated
@ManickaP
ManickaPforce-pushed the mapichov/quic-listener branch 2 times, most recently from 97ad0ce to 1329933CompareJuly 4, 2022 10:13
@ManickaP
ManickaPforce-pushed the mapichov/quic-listener branch from 1329933 to 035a47aCompareJuly 5, 2022 09:18
@ManickaP

Copy link
Copy Markdown
MemberAuthor

Failing test is #45868

@ManickaP

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-libraries-coreclr outerloop

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@ManickaP

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-libraries stress-http

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@ManickaP
ManickaP marked this pull request as ready for review July 5, 2022 16:17
@ManickaPManickaP mentioned this pull request Jul 7, 2022
Comment threadsrc/libraries/System.Net.Quic/src/Resources/Strings.resx Outdated
{
// 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
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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.

@ManickaPManickaPJul 8, 2022

Copy link
Copy Markdown
MemberAuthor

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
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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.

@ManickaPManickaPJul 8, 2022

Copy link
Copy Markdown
MemberAuthor

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
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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.

@ManickaPManickaPJul 8, 2022

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Addressed in #71783.

Comment threadsrc/libraries/System.Net.Quic/src/System/Net/Quic/QuicListener.cs Outdated
@ManickaP
ManickaPforce-pushed the mapichov/quic-listener branch from 6fbd597 to 762ec71CompareJuly 8, 2022 10:04
@ManickaP
ManickaPforce-pushed the mapichov/quic-listener branch 2 times, most recently from 5e558d3 to 25276edCompareJuly 8, 2022 13:16
@ManickaP
ManickaPforce-pushed the mapichov/quic-listener branch from 25276ed to 4e5b919CompareJuly 8, 2022 13:25

@CarnaViireCarnaViire left a comment

Copy link
Copy Markdown
Member

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?)

Comment threadsrc/libraries/System.Net.Quic/src/System.Net.Quic.csproj Outdated
_cancellationTokenSource = new CancellationTokenSource();
}

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

Copy link
Copy Markdown
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
Copy Markdown
MemberAuthor

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
Copy Markdown
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);

Comment threadsrc/libraries/System.Net.Quic/src/System/Net/Quic/QuicListener.cs Outdated
@stephentoub

Copy link
Copy Markdown
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).

@ManickaP
ManickaPforce-pushed the mapichov/quic-listener branch from 453f6aa to 61ce63fCompareJuly 8, 2022 17:46

@CarnaViireCarnaViire left a comment

Copy link
Copy Markdown
Member

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:mainJul 10, 2022
@ManickaP
ManickaP deleted the mapichov/quic-listener branch July 10, 2022 20:13
@ManickaP

Copy link
Copy Markdown
MemberAuthor

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

@karelzkarelz added this to the 7.0.0 milestone Jul 19, 2022
@ghostghost locked as resolved and limited conversation to collaborators Aug 18, 2022
Sign up for freeto 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

@ManickaP@stephentoub@CarnaViire@gfoidl@karelz