-
Notifications
You must be signed in to change notification settings - Fork 5.3k
[QUIC] Stream write cancellation #53304
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
Changes from all commits
10d77d2
eb17b63
62895f6
fe84604
2c5f97d
d443a76
9af76d1
b7b2f85
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,6 +6,7 @@ | |
| using System.Collections.Generic; | ||
| using System.Linq; | ||
| using System.Text; | ||
| using System.Threading; | ||
| using System.Threading.Tasks; | ||
| using Xunit; | ||
|
|
||
|
|
@@ -434,6 +435,138 @@ await Task.Run(async () => | |
| Assert.Equal(ExpectedErrorCode, ex.ErrorCode); | ||
| }).WaitAsync(TimeSpan.FromSeconds(15)); | ||
| } | ||
|
|
||
| [ActiveIssue("https://github.com/dotnet/runtime/issues/53530")] | ||
| [Fact] | ||
| public async Task StreamAbortedWithoutWriting_ReadThrows() | ||
| { | ||
| long expectedErrorCode = 1234; | ||
|
|
||
| await RunClientServer( | ||
| clientFunction: async connection => | ||
| { | ||
| await using QuicStream stream = connection.OpenUnidirectionalStream(); | ||
| stream.AbortWrite(expectedErrorCode); | ||
|
|
||
| await stream.ShutdownCompleted(); | ||
| }, | ||
| serverFunction: async connection => | ||
| { | ||
| await using QuicStream stream = await connection.AcceptStreamAsync(); | ||
|
|
||
| byte[] buffer = new byte[1]; | ||
|
|
||
| QuicStreamAbortedException ex = await Assert.ThrowsAsync<QuicStreamAbortedException>(() => ReadAll(stream, buffer)); | ||
| Assert.Equal(expectedErrorCode, ex.ErrorCode); | ||
|
|
||
| await stream.ShutdownCompleted(); | ||
| } | ||
| ); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task WritePreCanceled_Throws() | ||
| { | ||
| long expectedErrorCode = 1234; | ||
|
|
||
| await RunClientServer( | ||
| clientFunction: async connection => | ||
| { | ||
| await using QuicStream stream = connection.OpenUnidirectionalStream(); | ||
|
|
||
| CancellationTokenSource cts = new CancellationTokenSource(); | ||
| cts.Cancel(); | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This way of testing cancellation is not reliable. Sometimes it seems that task finishes before cancellation occurs. Does anyone have an idea on how to make sure write is not over before cancellation? Pre-cancelling before calling WriteAsync is not appealing to me...
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can only think of ways which touch
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sending 64M instead of 1M helped, at least I didn't get any failures on my setup while running it ~20 times. If someone believes it is not reliable enough, the only option I see here is to remove this test completely. I've added a second one that checks pre-cancelled token.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rewrote to an infinite loop until canceled, as per @geoffkizer suggestion
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could that infinite loop be somehow finite, but still big enough for the test? I already see hanging tests in CI :) |
||
|
|
||
| await Assert.ThrowsAsync<OperationCanceledException>(() => stream.WriteAsync(new byte[1], cts.Token).AsTask()); | ||
|
|
||
| // next write would also throw | ||
| await Assert.ThrowsAsync<OperationCanceledException>(() => stream.WriteAsync(new byte[1]).AsTask()); | ||
|
|
||
| // manual write abort is still required | ||
| stream.AbortWrite(expectedErrorCode); | ||
|
|
||
| await stream.ShutdownCompleted(); | ||
| }, | ||
| serverFunction: async connection => | ||
| { | ||
| await using QuicStream stream = await connection.AcceptStreamAsync(); | ||
|
|
||
| byte[] buffer = new byte[1024 * 1024]; | ||
|
|
||
| // TODO: it should always throw QuicStreamAbortedException, but sometimes it does not https://github.com/dotnet/runtime/issues/53530 | ||
| //QuicStreamAbortedException ex = await Assert.ThrowsAsync<QuicStreamAbortedException>(() => ReadAll(stream, buffer)); | ||
| try | ||
| { | ||
| await ReadAll(stream, buffer); | ||
| } | ||
| catch (QuicStreamAbortedException) { } | ||
|
|
||
| await stream.ShutdownCompleted(); | ||
| } | ||
| ); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task WriteCanceled_NextWriteThrows() | ||
| { | ||
| long expectedErrorCode = 1234; | ||
|
|
||
| await RunClientServer( | ||
| clientFunction: async connection => | ||
| { | ||
| await using QuicStream stream = connection.OpenUnidirectionalStream(); | ||
|
|
||
| CancellationTokenSource cts = new CancellationTokenSource(500); | ||
|
|
||
| async Task WriteUntilCanceled() | ||
| { | ||
| var buffer = new byte[64 * 1024]; | ||
| while (true) | ||
| { | ||
| await stream.WriteAsync(buffer, cancellationToken: cts.Token); | ||
| } | ||
| } | ||
|
|
||
| // a write would eventually be canceled | ||
| await Assert.ThrowsAsync<OperationCanceledException>(() => WriteUntilCanceled().WaitAsync(TimeSpan.FromSeconds(3))); | ||
|
|
||
| // next write would also throw | ||
| await Assert.ThrowsAsync<OperationCanceledException>(() => stream.WriteAsync(new byte[1]).AsTask()); | ||
|
|
||
| // manual write abort is still required | ||
| stream.AbortWrite(expectedErrorCode); | ||
|
|
||
| await stream.ShutdownCompleted(); | ||
| }, | ||
| serverFunction: async connection => | ||
| { | ||
| await using QuicStream stream = await connection.AcceptStreamAsync(); | ||
|
|
||
| async Task ReadUntilAborted() | ||
| { | ||
| var buffer = new byte[1024]; | ||
| while (true) | ||
| { | ||
| int res = await stream.ReadAsync(buffer); | ||
| if (res == 0) | ||
| { | ||
| break; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // TODO: it should always throw QuicStreamAbortedException, but sometimes it does not https://github.com/dotnet/runtime/issues/53530 | ||
| //QuicStreamAbortedException ex = await Assert.ThrowsAsync<QuicStreamAbortedException>(() => ReadUntilAborted()); | ||
| try | ||
| { | ||
| await ReadUntilAborted().WaitAsync(TimeSpan.FromSeconds(3)); | ||
| } | ||
| catch (QuicStreamAbortedException) { } | ||
|
|
||
| await stream.ShutdownCompleted(); | ||
| } | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| public sealed class QuicStreamTests_MockProvider : QuicStreamTests<MockProviderFactory> { } | ||
|
|
||
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 I don't wait for it before creating CancellationTokenRegistration, in case of pre-cancelled token, SendResettableCompletionSource.CompleteException fails with
InvalidOperationException: Operation is not valid due to the current state of the object.🤔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.
Yeah, we guard the
ResettableCompletionSourcecompletion withSendState, but it "breaks" here since we're (ab)usingSendResettableCompletionSourceforSTART_COMPLETEas well.I don't have any better suggestions than what you did here.