-
Notifications
You must be signed in to change notification settings - Fork 4.7k
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
Swallow ObjectDisposedException when aborting QuicStream from CancellationAction #74634
Swallow ObjectDisposedException when aborting QuicStream from CancellationAction #74634
Conversation
Tagging subscribers to this area: @dotnet/ncl Issue DetailsFixes #73688. Depends on #74611 (it changes the type of exception). When QuicStream.(Read|Write)Async is canceled via CancellationTokenSource.CancelAfter, the CancellationAction is invoked from different thread and therefore the Abort call races with Dispose on the original thread. #74611 will make the StreamClose/StreamAbort race safe by making sure we AddRef/Release the related MsQuicSafeHandle, thus converting the race to possible ObjectDisposedException which we can swallow.
|
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.
Should we add the test from #73688 (comment)?
@@ -70,9 +70,18 @@ public sealed partial class QuicStream | |||
{ | |||
CancellationAction = target => | |||
{ | |||
if (target is QuicStream stream) | |||
try |
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.
I'm wondering if we can have private version of stream.Abort
that does not throw and either is silent or returns bool
for tracing if we ant to know if Abort
succeeded or not.
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.
Abort already tries to be no-op in case stream is disposed. But it is not enough. The only way to fix that I can think of, is to do dangerousAddRef in the beginning of Abort, and only after that check _disposed. Or wrap both msquic abort call and dispose of the handle in the lock and check _disposed inside the lock before abort. Otherwise, by the time we access handle on msquic abort call, it might get disposed already, despite the check in the beginning of Abort.
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.
So the ODE would come from the SafeHandle? I'm fine with this change for 7.0. It seems to cause lot of CI troubles. We can figure later if there is cheaper way to do it.
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 ObjectDisposedException
is coming from DangerousAddRef
. At the moment I see no way of avoiding the exception without a lock (we don't have TryAddRef
or similar). I expect the exception to be relatively rare so I think it is fine (at least for now)
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.
LGTM. Thanks for hunting this down!
I don't think I can make the test reliable enough to be worth it, the repro in the comment required a protected async Task ValidateCancelableReadAsyncValueTask_AfterInvocation_ThrowsCancellationException(Stream stream, int cancellationDelay)
{
if (!stream.CanRead || !FullyCancelableOperations)
{
return;
}
var cts = new CancellationTokenSource();
Task<int> t = stream.ReadAsync(new byte[1], cts.Token).AsTask();
cts.CancelAfter(cancellationDelay);
await AssertCanceledAsync(cts.Token, () => t);
} And once this finishes, it immediately starts disposing the streams/connections. (via |
While it would be nice to have dedicated test I think it is ok if we don't - since the issue showed up in existing tests. |
Close+Open to trigger CI |
CI Failure is #74795 |
/backport to release/7.0 |
Started backporting to release/7.0: https://github.com/dotnet/runtime/actions/runs/3005534353 |
Fixes #73688.
Depends on #74669 (it changes the type of exception thrown).
When QuicStream.(Read|Write)Async is canceled via CancellationTokenSource.CancelAfter, the CancellationAction is invoked from different thread and therefore the Abort call races with Dispose on the original thread. #74669 will make the StreamClose/StreamAbort race safe by making sure we AddRef/Release the related MsQuicSafeHandle, thus converting the race to possible ObjectDisposedException which we can swallow.