-
Notifications
You must be signed in to change notification settings - Fork 4.9k
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
QuicStream shutdown #52929
QuicStream shutdown #52929
Conversation
Note regarding the This serves as a reminder for when your PR is modifying a ref *.cs file and adding/modifying public APIs, to please make sure the API implementation in the src *.cs file is documented with triple slash comments, so the PR reviewers can sign off that change. |
Tagging subscribers to this area: @dotnet/ncl Issue DetailsThis implements #756, #32075, and #42651. It adds:
Behaviors introducedLocally aborting stream, then calling ReadAsync(): Abort();
ReadAsync(); // throws InvalidOperationException("Stream is no longer readable.") ReadAsync() pending, then locally aborting stream: (note: we initially wanted this to throw ValueTask<int> t = ReadAsync();
Abort();
await t; // throws QuicOperationAbortedException Peer aborting stream: ReadAsync(); // throws QuicStreamAbortedException(int errorCode) Concurrent reads: ValueTask<int> t = ReadAsync();
ReadAsync(); // throws InvalidOperationException("Only one read is supported at a time.") Our recommended disposal pattern is now: await using QuicStream stream = ...;
try
{
// ... all the usage of the stream.
await CloseAsync(CancellationToken); // implicitly completes writes. waits for peer shutdown and ACK our shutdown.
}
catch
{
Abort();
// now that sends are aborted, the `DisposeAsync` call from the `using` above will close the stream immediately without waiting for peer to shutdown or ACK.
throw;
}
|
CC @ManickaP @CarnaViire @wfurt to avoid too many conflicts! |
422d718
to
732b944
Compare
@@ -122,8 +122,7 @@ public async Task WriteTests(int[][] writes, WriteType writeType) | |||
} | |||
} | |||
|
|||
stream.Shutdown(); | |||
await stream.ShutdownCompleted(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CompleteWrites will not wait for any completion, right? AFAIK we can't remove waiting for completion here and in the other tests. Both sides should receive SHUTDOWN_COMPLETED on app level before Connection.Close is called, otherwise we may get INVALID_STATE exceptions from msquic, because Connection.Close is happening immediately without waiting for anything.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will work because stream.DisposeAsync()
now calls shutdown and waits for peer.
} | ||
else | ||
{ | ||
_state.ShutdownCompletionSource.Task.GetAwaiter().GetResult(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If it was a bidi stream and peer did not send their shutdown, we'll wait here forever?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. The "recommended disposal behavior" in top comment is how we get around this right now.
The alternative is to make dispose be abortive, which we previously didn't like because other streams treat dispose as graceful.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking at the "recommended disposal behavior" - should we then check if CloseAsync was called, which would mean we are on a recommended path -- and if not, perform an abortive dispose?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reading through the code again, I see that CloseAsync is just DisposeAsync, so the "recommended disposal behavior" is actually just putting a timeout on that wait via CancellationToken that you can't specify otherwise.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The problem is that everyone else using Stream
-- any stream, not just QuicStream
or NetworkStream
-- will assume that Dispose()
will gracefully close the stream.
We'd be the odd one that made it abortive. So this is a workaround for now to keep consistent. I expect a long deliberation on this one once it gets into API review.
{ | ||
if (_disposed) | ||
{ | ||
return; | ||
} | ||
|
||
QUIC_STREAM_SHUTDOWN_FLAGS flags = immediate | ||
? (QUIC_STREAM_SHUTDOWN_FLAGS.GRACEFUL | QUIC_STREAM_SHUTDOWN_FLAGS.IMMEDIATE) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This combination seems invalid... judging by the code, IMMEDIATE is only allowed in combination with both ABORT_SEND and ABORT_RECEIVE
https://github.com/microsoft/msquic/blob/c643c6275dfc506ec47c2067b676f26ba2368568/src/core/api.c#L909-L916
https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-4.1-15
Would this situation use the new abort API? I'm guessing the server would abort the read side of an incoming bidirectional stream with H3_NO_ERROR. |
# Conflicts: # src/libraries/System.Net.Quic/ref/System.Net.Quic.cs # src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/Mock/MockStream.cs # src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/Interop/MsQuicStatusCodes.cs # src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/MsQuicStream.cs # src/libraries/System.Net.Quic/tests/FunctionalTests/QuicTestBase.cs
This brings changes to read states and behavior done initially in #52929 with my fixes to it to make all tests work
Triage: We used most of this PR in merged PRs and some leftovers tracked by bugs and new APIs. Closing. |
This implements #756, #32075, #42651, and the QuicStream part of #32142.
It adds:
QuicStream.CompleteWrites()
.QuicStream.Abort(int errorCode, direction)
.QuicStream.CloseAsync()
implicitly callsQuicStream.CompleteWrites()
then, if writes haven't been aborted, waits for graceful (both sides have reached FIN/abort on their write stream) shutdown.QuicStream.CloseAsync(CancellationToken)
to allow cancellation of waiting for that graceful shutdown.ReadAsync()
andDisposeAsync
/CloseAsync
is pending.Behaviors introduced
Locally aborting stream, then calling ReadAsync():
ReadAsync() pending, then locally aborting stream:
(note: we initially wanted this to throw
ObjectDisposedException
, but because one direction can be aborted without disposing, we need to differentiate the two states)Peer aborting stream:
Concurrent reads:
Our recommended disposal pattern is now very complicated... we initially agreed on a design like this but in actual use it's kinda crazy. A sample of it is here:
runtime/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicStreamTests.cs
Lines 646 to 676 in d9e42a7