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

[HTTP/3] Abort response stream on dispose if content not finished #57156

Merged
merged 3 commits into from
Aug 19, 2021

Conversation

ManickaP
Copy link
Member

Sends abort read/write if H/3 stream is disposed before respective contents are finished.

Fixes #56129

cc: @CarnaViire

@ghost
Copy link

ghost commented Aug 10, 2021

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

Issue Details

Sends abort read/write if H/3 stream is disposed before respective contents are finished.

Fixes #56129

cc: @CarnaViire

Author: ManickaP
Assignees: -
Labels:

area-System.Net.Http

Milestone: -

@@ -1187,6 +1192,25 @@ private async ValueTask<bool> ReadNextDataFrameAsync(HttpResponseMessage respons
public void Trace(string message, [CallerMemberName] string? memberName = null) =>
_connection.Trace(StreamId, message, memberName);

private void AbortStream()
{
// ToDo : locking??? we don't have any in H/3 stream, we don't have a sync root or anything.
Copy link
Member Author

@ManickaP ManickaP Aug 10, 2021

Choose a reason for hiding this comment

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

I was looking at H2Stream corresponding method:


And the main difference is that we lack any locking mechanism in H3Stream, which kind of worries me.

Copy link
Member

Choose a reason for hiding this comment

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

TL;DR: if we check the flag and it's not EOF and on the next line when we call Abort the flag is changed already - it is not much better than just calling Abort without any checks, is it?

We actually had a bit of a discussion on using flags to symbolize EOFs in my first attempt on cancellation PR #54334 (comment)

I guess we might want to put some proper locking here. as HTTP/2 has.. or just always call abort ant let quic stream/msquic handle the race? If we imagine Dispose being called synchronously after finishing reading the response -- I imagine graceful shutdown should be complete inside QuicStream by that time. So Abort will be just a no-op. But I may be wrong. Always calling Abort may lead to unexpected behavior in streaming scenarios 🤔 but I feel like this check without any locks will not be much better that always aborting.

@geoffkizer what do you think?

Copy link
Member Author

Choose a reason for hiding this comment

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

I'd say that checking is still better then not checking at all. There's a potential for race, but in a happy path (dispose the response after you've finished the reading), this should work and not send any aborts.

Anyway, I was surprised that there's no locking mechanism at all. So my question is, do we use any other technique I'm missing? Or is this an oversight that just works because we're testing the right thing? Or are we just not thread-safe on the level of a response message thus we don't care here?

@ManickaP
Copy link
Member Author

ManickaP commented Aug 10, 2021

@CarnaViire add #56683: could you point me at the test which might hit that problem? I'll try to repro it locally and then see if I can fix it as well.

@CarnaViire
Copy link
Member

add #56683: could you point me at the test which might hit that problem? I'll try to repro it locally and then see if I can fix it as well.

The test is

public async Task ResponseCancellation_BothCancellationTokenAndDispose_Success()

Namely here you can remove ODE from expected exceptions
if (ex is not OperationCanceledException and not ObjectDisposedException)

The ThrowIfDisposed checks I proposed to remove are

internal override void AbortRead(long errorCode)
{
ThrowIfDisposed();

internal override void AbortWrite(long errorCode)
{
ThrowIfDisposed();

But I thought you touched MsQuicStream, but you did not. So I can do the change myself in a separate PR tomorrow.

@ManickaP
Copy link
Member Author

But I thought you touched MsQuicStream, but you did not. So I can do the change myself in a separate PR tomorrow.

Ok then, do it. I won't try to cram it in this PR.

@ManickaP
Copy link
Member Author

Newly added test is failing in CI, need to investigate, hold off reviews for now please.

@ManickaP ManickaP force-pushed the mapichov/56129_send_abort_dispose branch 2 times, most recently from 03537c9 to af6a189 Compare August 17, 2021 17:07
@ManickaP ManickaP force-pushed the mapichov/56129_send_abort_dispose branch from af6a189 to 60949a3 Compare August 18, 2021 12:51
@ManickaP ManickaP force-pushed the mapichov/56129_send_abort_dispose branch from 60949a3 to 3ad021f Compare August 18, 2021 12:54
@ManickaP
Copy link
Member Author

This is ready for review @CarnaViire @geoffkizer

{
throw GetConnectionAbortedException(_state);
}

// Change the state in the same lock where we check for final states to prevent coming back from Aborted/ConnectionClosed.
Debug.Assert(_state.SendState != SendState.Pending);
Copy link
Member

Choose a reason for hiding this comment

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

Nit: should we do an actual throw here? for Pending and Finished states. Both of them would mean someone tries to do a send with an another concurrent send in flight -- that we don't support, and Send state machine, fragile as it is, may blow up because of it. Concurrent reads do throw now. But we can choose not to do it now, your choice.

I also feel bad that we don't have any final state after we send FIN flag (like ReadsCompleted) so that means we also can't prevent user calling Send after passing the FIN flag which I would guess will result in msquic exception (INVALID_STATE most possibly). But that's out of scope of this PR, just me ranting. I guess that's what #55817 is for in the end.

Copy link
Member Author

Choose a reason for hiding this comment

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

I share the same grievances and worries as you. I chose to just shuffle the code around to fix the problem on hand instead of doing the ultimate right thing, which I think we should leave to #55817. We've lived with just the assert until now, this change shouldn't make any worse (hopefully 😅 )

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.

LGTM. I commented my observations but we may not act on it now.

@ManickaP ManickaP merged commit b75e55b into dotnet:main Aug 19, 2021
@ManickaP ManickaP deleted the mapichov/56129_send_abort_dispose branch August 19, 2021 10:44
@ManickaP
Copy link
Member Author

/backport to release/6.0

@github-actions
Copy link
Contributor

Started backporting to release/6.0: https://github.com/dotnet/runtime/actions/runs/1147767776

@ViktorHofer
Copy link
Member

port to release/6.0

@ManickaP do you want this to go into RC1 or RC2? The branch that you picked is RC2.

@ManickaP
Copy link
Member Author

@ViktorHofer RC2

@karelz karelz added this to the 7.0.0 milestone Aug 20, 2021
@karelz
Copy link
Member

karelz commented Aug 24, 2021

/backport to release/6.0-rc1

@github-actions
Copy link
Contributor

Started backporting to release/6.0-rc1: https://github.com/dotnet/runtime/actions/runs/1161485673

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.

[HTTP/3] Cancellation of response download not reported correctly on server
4 participants