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

Swallow ObjectDisposedException when aborting QuicStream from CancellationAction #74634

Conversation

rzikm
Copy link
Member

@rzikm rzikm commented Aug 26, 2022

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.

@ghost
Copy link

ghost commented Aug 26, 2022

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

Issue Details

Fixes #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.

Author: rzikm
Assignees: -
Labels:

area-System.Net.Quic

Milestone: -

@rzikm rzikm requested a review from a team August 26, 2022 08:39
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.

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
Copy link
Member

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.

Copy link
Member

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.

Copy link
Member

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.

Copy link
Member Author

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)

Copy link
Member

@ManickaP ManickaP left a 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!

@karelz karelz added this to the 8.0.0 milestone Aug 27, 2022
@rzikm
Copy link
Member Author

rzikm commented Aug 29, 2022

Should we add the test from #73688 (comment)?

I don't think I can make the test reliable enough to be worth it, the repro in the comment required a Console.ReadLine in QuicStream.Abort in order to work. I think the existing test we were hitting this on (StreamConformanceTests.ReadAsync_CancelPendingValueTask_ThrowsCancellationException) is sufficient (I used it as a model for my repro test). At its core, it uses

        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 using StreamPair streams).

@wfurt
Copy link
Member

wfurt commented Aug 29, 2022

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.

@rzikm
Copy link
Member Author

rzikm commented Sep 6, 2022

Close+Open to trigger CI

@rzikm
Copy link
Member Author

rzikm commented Sep 7, 2022

CI Failure is #74795

@rzikm rzikm merged commit 3709941 into dotnet:main Sep 7, 2022
@rzikm
Copy link
Member Author

rzikm commented Sep 7, 2022

/backport to release/7.0

@github-actions
Copy link
Contributor

github-actions bot commented Sep 7, 2022

Started backporting to release/7.0: https://github.com/dotnet/runtime/actions/runs/3005534353

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

Successfully merging this pull request may close these issues.

[QUIC] Abort on cancellation throws QUIC_STATUS_INVALID_PARAMETER
5 participants