Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[QUIC] QuicStream reading/writing work #90253

Merged
merged 24 commits into from
Sep 13, 2023

Conversation

ManickaP
Copy link
Member

@ManickaP ManickaP commented Aug 9, 2023

So far addressed:

Remain as-is:

Closes #77216
Fixes #79911
Fixes #82704
Closes #79818
Fixes #82523

Also fixed problem with GC eating QuicStream when only either ReadsClosed or WritesClosed is awaited. Improved some logging. Added bunch of tests.

Affected tests were locally ran with iteration count 100, repro cases ran for 5+ minutes, ran ASP.NET Core tests locally and running stress test in docker locally now (result will be later).

cc @CarnaViire @rzikm @wfurt

@ghost
Copy link

ghost commented Aug 9, 2023

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

Issue Details

So far address:

Remain as-is:

Still working on:

Fixes #77216, #79911, #82704
Closes #79818

Also fixed problem with GC eating QuicStream when only either ReadsClosed or WritesClosed is awaited. Improved some logging. Added bunch of tests.

Affected tests were locally ran with iteration count 100 before committing the fix.

Putting up as draft to get early feedback and CI results.

cc @CarnaViire @rzikm @wfurt

Author: ManickaP
Assignees: -
Labels:

area-System.Net.Quic

Milestone: -

@ManickaP
Copy link
Member Author

So far: outerloop unrelated failures, stress H/3 clear.


unsafe
{
_sendBuffers.Initialize(buffer);
int status = MsQuicApi.Api.StreamSend(
Copy link
Member Author

Choose a reason for hiding this comment

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

Take into account that abort might have happened in parallel or in the meantime and StreamSend might return an error.

Copy link
Member Author

Choose a reason for hiding this comment

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

It'll return QUIC_STATUS_ABORTED which is conveniently ignored by TryGetStreamExceptionForMsQuicStatus, leading to a correct path of throwing exception from Abort and overriding it with OperationCanceled in finally block.

@ManickaP ManickaP force-pushed the mapichov/quic-stream branch from 6d1b71f to 978abf7 Compare August 12, 2023 15:51
/// It remembers the result from <see cref="TryComplete"/> and propagates it to <see cref="_finalTaskSource"/> only after <see cref="TrySignal"/> is called.
/// Effectively allowing to separate setting of the result from task completion, which is necessary when the resettable portion of the value task source needs to consumed first.
/// </summary>
private struct FinalTaskSource
Copy link
Member Author

Choose a reason for hiding this comment

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

This is the original implementation for FinalTaskSource, I only brought it back.
It solves the problem when Writes/ReadsClosed tasks were signaled long before ValueTask. Now, they are always signaled afterwards.

_valueTaskSource.SetException(_finalTaskSource.Task.Exception?.InnerException!);
}

// In case the _valueTaskSource was successful, we want the potential error from _finalTaskSource to surface immediately.
Copy link
Member Author

Choose a reason for hiding this comment

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

Removed this part as it was causing #82523

}
// The task was non-finally completed without having anyone awaiting on it.
// In such case, discard the temporary result and replace it with this final completion.
if (state == State.Ready && !_hasWaiter && final)
Copy link
Member Author

Choose a reason for hiding this comment

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

This allows to ignore RECEIVE event notification that no ReadAsync is waiting on, allowing AbortRead to override them.


private bool TryComplete(Exception? exception, bool final)
{
// Dispose the cancellation registration before completing the task, so that it cannot run after the awaiting method returned.
Copy link
Member Author

Choose a reason for hiding this comment

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

Solves #82704.
As the registration was disposed after completing the tasks, they continuation was allowed to run, which reset this value task source while cancellation registration was still alive and could fire before the disposal.

{
thisRef._cancellationAction?.Invoke(target);
thisRef._cancelledToken = cancellationToken;
Copy link
Member Author

Choose a reason for hiding this comment

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

We don't set the task from cancellation, now it's part of cancellation action. But we remember that it was cancelled via this token and throw appropriate error in GetResult, effectively overriding any result set to the task.
This helps with waiting on SEND_COMPLETE in WriteAsync where we don't want cancellation to unblock the task, but we still want to propagate it to the user when SEND_COMPLETE arrives.

_finalTaskSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
if (!TrySignal(out _))
{
GCHandle handle = GCHandle.Alloc(keepAlive);
Copy link
Member Author

Choose a reason for hiding this comment

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

Solves GC eating the Stream when the only thing awaited was Reads/WritesClosed.

@@ -109,7 +110,8 @@ public sealed partial class QuicStream
}
};
private MsQuicBuffers _sendBuffers = new MsQuicBuffers();
private readonly object _sendBuffersLock = new object();
private int _sendLocked;
Copy link
Member Author

@ManickaP ManickaP Aug 12, 2023

Choose a reason for hiding this comment

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

When taken, the owner has the exclusive right to unblock _sendTcs. Used to make sure that when StreamSend is called and succeeds (WriteAsync takes the lock), we will always wait for SEND_COMPLETE (event handle returns it). If Abort happens in the meantime, it will store the exception in _sendException instead.

@@ -462,6 +461,21 @@ public void Abort(QuicAbortDirection abortDirection, long errorCode)
(ulong)errorCode),
"StreamShutdown failed");
}

if (abortDirection.HasFlag(QuicAbortDirection.Read))
Copy link
Member Author

Choose a reason for hiding this comment

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

This reordering solves #79911.

@dotnet dotnet deleted a comment from azure-pipelines bot Aug 12, 2023
@dotnet dotnet deleted a comment from azure-pipelines bot Aug 12, 2023
@dotnet dotnet deleted a comment from azure-pipelines bot Aug 12, 2023
@dotnet dotnet deleted a comment from azure-pipelines bot Aug 12, 2023
@ManickaP ManickaP force-pushed the mapichov/quic-stream branch 2 times, most recently from 5c74b9c to d5f8381 Compare August 12, 2023 16:50
@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@ManickaP
Copy link
Member Author

/azp run runtime-libraries-coreclr outerloop

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@@ -150,6 +151,7 @@ private void CheckForShutdown()

if (_clientControl != null)
{
await _sendSettingsTask.ConfigureAwait(false);
Copy link
Member

Choose a reason for hiding this comment

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

Is it possible this task throws? E.g. connection closed before we were able to send settings? Or it is impossible at this point in time?

Copy link
Member Author

Choose a reason for hiding this comment

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

No, SendSettingsAsync catches all exceptions and calls Abort on them:

else if (closeType == CloseOutboundControlStream.Abort)
{
int iterations = 5;
while (iterations-- > 0)
Copy link
Member

Choose a reason for hiding this comment

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

why are iterations and delay needed?

Copy link
Member Author

Choose a reason for hiding this comment

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

Depending on timing, the client can receive the control stream abort, i.e. RESET_STREAM, before it even read the HTTP/3 stream type from it, see

private async Task ProcessServerStreamAsync(QuicStream stream)
for details.

QUIC spec says, RESET can discard data, which can happen in this case. Add to it H/3 spec, which says that unrecognized or aborted stream should be ignored:

try
{
bytesRead = await stream.ReadAsync(buffer.AvailableMemory, CancellationToken.None).ConfigureAwait(false);
}
catch (QuicException ex) when (ex.QuicError == QuicError.StreamAborted)
{
// Treat identical to receiving 0. See below comment.
bytesRead = 0;
}

Leading to ignoring the incoming control stream, because client doesn't know it's control, instead of reacting to the abort and closing the connection.

So this fix attempts to do the again, but with slight delay between sending the data and abort, line 1710.

And why this worked before: we would return data for one QuicStream.ReadAsync before propagating the error, not the whole buffer, just one call to read succeed, which was enough to get that 1 byte saying it's control. Also, MsQuic could always discard the data, we just never observed it (timing, too fast on the same machine...)

Copy link
Member

@CarnaViire CarnaViire left a comment

Choose a reason for hiding this comment

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

While the change seems LGTM, but I see in the latest CI run that there was an assert firing on dispose, might be worth investigating...

The assert in question is a new one added to QuicStream's DisposeAsync (Line 717): Debug.Assert(_sendTcs.IsCompleted);

Full console log

Process terminated. Assertion failed.
   at System.Net.Quic.QuicStream.DisposeAsync() in /_/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicStream.cs:line 717
   at System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start[TStateMachine](TStateMachine& stateMachine) in /_/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncMethodBuilderCore.cs:line 38
   at System.Net.Quic.QuicStream.DisposeAsync()
   at System.Net.Quic.QuicStream.Dispose(Boolean disposing) in /_/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicStream.Stream.cs:line 222
   at System.IO.Stream.Close() in /_/src/libraries/System.Private.CoreLib/src/System/IO/Stream.cs:line 165
   at System.Net.Http.Http3RequestStream.Dispose() in /_/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3RequestStream.cs:line 93
   at System.Net.Http.Http3RequestStream.Http3ReadStream.Dispose(Boolean disposing) in /_/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3RequestStream.cs:line 1395
   at System.IO.Stream.Close() in /_/src/libraries/System.Private.CoreLib/src/System/IO/Stream.cs:line 165
   at System.Net.Http.HttpConnectionResponseContent.Dispose(Boolean disposing) in /_/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionResponseContent.cs:line 89
   at System.Net.Http.HttpContent.Dispose() in /_/src/libraries/System.Net.Http/src/System/Net/Http/HttpContent.cs:line 679
   at System.Net.Http.HttpResponseMessage.Dispose(Boolean disposing) in /_/src/libraries/System.Net.Http/src/System/Net/Http/HttpResponseMessage.cs:line 215
   at System.Net.Http.HttpResponseMessage.Dispose() in /_/src/libraries/System.Net.Http/src/System/Net/Http/HttpResponseMessage.cs:line 221
   at System.Net.Http.Functional.Tests.HttpClientHandlerTest_Http3.<>c__DisplayClass14_0.<<RequestSendingResponseDisposed_ThrowsOnServer>b__1>d.MoveNext() in /_/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Http3.cs:line 573
...

@ManickaP
Copy link
Member Author

Failure is: #91308

@ManickaP
Copy link
Member Author

/azp run runtime-libraries-coreclr outerloop

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

Copy link
Member

@CarnaViire CarnaViire left a comment

Choose a reason for hiding this comment

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

Tests seem fine now, so :shipit:

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.