Skip to content

[QUIC] Abort after Dispose is no-op #57318

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

Merged
merged 1 commit into from
Aug 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -669,7 +669,7 @@ public async Task ResponseCancellation_BothCancellationTokenAndDispose_Success()
Exception ex = await Assert.ThrowsAnyAsync<Exception>(() => stream.ReadAsync(new byte[1024], cancellationToken: cts.Token).AsTask());

// exact exception depends on who won the race
if (ex is not OperationCanceledException and not ObjectDisposedException)
if (ex is not OperationCanceledException)
{
var ioe = Assert.IsType<IOException>(ex);
var hre = Assert.IsType<HttpRequestException>(ioe.InnerException);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -493,7 +493,11 @@ private static unsafe int CopyMsQuicBuffersToUserBuffer(ReadOnlySpan<QuicBuffer>

internal override void AbortRead(long errorCode)
{
ThrowIfDisposed();
if (_disposed == 1)
{
// Dispose called AbortRead already
return;
}

bool shouldComplete = false;
lock (_state)
Expand Down Expand Up @@ -521,7 +525,13 @@ internal override void AbortRead(long errorCode)

internal override void AbortWrite(long errorCode)
{
ThrowIfDisposed();
if (_disposed == 1)
{
// Dispose already triggered graceful shutdown
// It is unsafe to try to trigger abortive shutdown now, because final event arriving after Dispose releases SafeHandle
// so if it arrives after our check but before we call msquic, me might end up with access violation
Copy link
Member

Choose a reason for hiding this comment

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

Would it help if we send the abort only if the SendState was < Aborted? Few lines bellow we test the state, we could as well use similar pattern we do elsewhere with shouldAbort, set it to true only if we change the state and send the Abort only in such cases.

If this is something we could do, it might also help with the concern you raised in #57156, since it would provide the "locking" security via the SendState property.

Copy link
Member Author

Choose a reason for hiding this comment

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

Discussed offline that the suggestion is good, but not directly related to the change, so it should be done in a separate PR

return;
}

lock (_state)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,65 @@ async Task ReadUntilAborted()
}
);
}

[Fact]
public async Task AbortAfterDispose_ProperlyOpenedStream_Success()
{
byte[] buffer = new byte[1] { 42 };
var sem = new SemaphoreSlim(0);

await RunClientServer(
clientFunction: async connection =>
{
QuicStream stream = connection.OpenBidirectionalStream();
// Force stream to open on the wire
await stream.WriteAsync(buffer);
await sem.WaitAsync();

stream.Dispose();

// should not throw ODE on aborting
stream.AbortRead(1234);
stream.AbortWrite(5675);
},
serverFunction: async connection =>
{
await using QuicStream stream = await connection.AcceptStreamAsync();
Assert.Equal(1, await stream.ReadAsync(buffer));
sem.Release();

// client will abort both sides, so we will receive the final event
await stream.ShutdownCompleted();
}
);
}

[Fact]
public async Task AbortAfterDispose_StreamCreationFlushedByDispose_Success()
{
await RunClientServer(
clientFunction: connection =>
{
QuicStream stream = connection.OpenBidirectionalStream();

// dispose will flush stream creation on the wire
stream.Dispose();

// should not throw ODE on aborting
stream.AbortRead(1234);
stream.AbortWrite(5675);

return Task.CompletedTask;
},
serverFunction: async connection =>
{
await using QuicStream stream = await connection.AcceptStreamAsync();

// client will abort both sides, so we will receive the final event
await stream.ShutdownCompleted();
}
);
}
}

public sealed class QuicStreamTests_MockProvider : QuicStreamTests<MockProviderFactory>
Expand Down