Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

follow up on duplex operation for HTTP/2#38226

Merged
wfurt merged 17 commits intodotnet:masterfrom
wfurt:duplex2
Jun 23, 2019
Merged

follow up on duplex operation for HTTP/2#38226
wfurt merged 17 commits intodotnet:masterfrom
wfurt:duplex2

Conversation

@wfurt
Copy link
Copy Markdown
Member

@wfurt wfurt commented Jun 4, 2019

This is follow-up on #36884 to address concerns about UnobservedTaskExceptions and error propagation.

This change wraps all ignored tasks to make sure it does not end up in failed state without seeing the exception.

I also added test case for case when exception happens while sending RequestBody after Response headers are received. When we hit plain IO exception, reading response will fail as it always did so I did not worry about is new test case.

fixes #36855

Comment thread src/Common/tests/System/Net/Http/Http2LoopbackServer.cs Outdated
Comment thread src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs Outdated
Comment thread src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs Outdated
Comment thread src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs Outdated
@wfurt
Copy link
Copy Markdown
Member Author

wfurt commented Jun 17, 2019

I pushed some changes to address lingering issues raised through this conversation or related issues:

StartWriteAsync can fail as we have seen in #38481 and #35466
There is at least race (as RFC also mentioned) condition like:

  • AddStream() in SendHeadersAsync()
  • We get GOAWAY from server. It has no clue about new stream so we can do cleanup in CheckForShutdown()
  • SendHeadersAsync() or SendRequestBody tries to continue and fails while trying to get credit.

In some of the traces, we probably got GOAWAY and failed right at RequestCreditAsync() SendHeadersAsync()

To address this I moved disposal of CreditManager objects from CheckForShutdown() to actual Dispose, so they follow life cycle of Http2Connection instead of being possibly disposed by GOAWAY frame.

On that note, if somebody Disposes HttpClient -> Http2Connection we are still on same situation.
If we want to avoid UnobservedTaskExceptions in that case I think added try/catch is needed.

With #34638 tracking remaining work for GOAWAY cases I think this fixes #35466

I added missing await to collect error on responseHeadersTask
fixes #38293

As I addresses comments from @geoffkizer I'm not aware of any remaining work for #35652
I also added test case for #38391 HTTP2: SendAsync cancellation token does not send RST_STREAM

In that part I bumped to #9071 where cancellation basically does not work for HttpContent.
I don't know if that is problem for gRPC but our low-level Stream function get CancelationToken.None because we use _request.Content.CopyToAsync() in SendRequestBodyAsync()

https://github.com/dotnet/corefx/blob/master/src/System.Net.Http/src/System/Net/Http/HttpContent.cs#L316-L327

For now, I simply extended test SlowTestStream() to do direct cancelation there so I can verify RST scenario.

fixes #38391
fixes #35652

Cancelation may need some more work but that should be probably separate PR .

It would be great if you can take another look @stephentoub and @geoffkizer

cc: @scalablecory @davidsh @krwq @JamesNK

@wfurt wfurt requested a review from a team June 18, 2019 08:23
@wfurt
Copy link
Copy Markdown
Member Author

wfurt commented Jun 18, 2019

After chatting with @stephentoub, I pushed new update to:

  • cancel properly streaming RequestBody despite #9071
    (did we missed 3.0 API review train on #9071 ????)
  • add cancelation to ReadResponseHeadersAsync() (pointed out while back by @davidsh) as well as cancelation handling to response stream (and new test for it)

@geoffkizer
Copy link
Copy Markdown

To address this I moved disposal of CreditManager objects from CheckForShutdown() to actual Dispose, so they follow life cycle of Http2Connection instead of being possibly disposed by GOAWAY frame.

How does this help? If I understand correctly, you've made it so that we will actually dispose CreditManager and the stream Dictionary earlier. That seems like it makes things worse, not better.

{
await _request.Content.CopyToAsync(writeStream, null, cancellationToken).ConfigureAwait(false);
// TODO: until #9071 is fixed, cancellation on content.CopyToAsync does not work.
// To work around it, register delegate and set _abortException as needed.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I'm confused here. #9071 is about exposing the cancellationToken publicly. But it looks like there is an internal method that does support cancellation. So why do we need to do this registration thing here?

And what is the registration actually accomplishing? It looks like we don't actually cancel the copy, we just set the exception. So what's the point here?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

CopyToAsync() does take CancelationToken. But than it calls TryGetBuffer() and that fails on steaming content so we fall to

SerializeToStreamAsync(stream, context, cancellationToken);

and that is

internal virtual Task SerializeToStreamAsync(Stream stream, TransportContext context, CancellationToken cancellationToken) => SerializeToStreamAsync(stream, context);

so as far as I can tell we do loose provided cancelation token.

There may be other ways to do this but there is already code check and throw if the _abortException is set. So the registration simply sets it and we will throw it later when processing the stream.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Can't we achieve the same thing by just checking the cancellationToken after the CopyToAsync is complete? I.e., no need to actually register here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

We could but that means we need to finish sending request body, right? For gRPC duplex streaming, that effectively means we cannot cancel request, right?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

How does this particular registration help? When cancellation is requested it'll end up setting _abortException to an OperationCanceledException... but what's paying attention to _abortException? Is something in the CopyToAsync operation monitoring _abortException somehow?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

yes. That was part of your #37223 @stephentoub . We will pick it up with HttpStream in read/write. If there is better way how to propagate cancelationToken itself down, let me know.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We will pick it up with HttpStream in read/write

But only between calls, right? Meaning, if cancellation is requested we'll end up noticing it after a read or after a write, but if we're in the middle of the read or write, we won't actually notice it until that operation completes, correct?

Without a public overload, I don't know of a better way to achieve it, though. The cancellation token passed into CopyToAsync below will help for some of our own content types, but most won't be given it, and thus this token won't be passed into the the constituent read and write operations. We can address that with HTTP/1.1 by tearing down the connection on cancellation, but we can't do that here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes, that is correct. As you mentioned, I did not find better easy way. So I take it as opportunistic improvement.

Having that said, adding overload with cancelation should not be controversial so I'm wondering if there is still chance to push it to 3.0. I guess that would be primary choicer if this was on our radar, right?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

adding overload with cancelation should not be controversial so I'm wondering if there is still chance to push it to 3.0. I guess that would be primary choicer if this was on our radar, right?

@terrajobst, @danmosemsft, could we still get something in here?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Okay, I understand the issue with SerializeToStreamAsync not actually being cancellable. Thanks for clarifying.

But I still don't see why we need to register here. Why not just do cancellationToken.ThrowIfCancellationRequested (or whatever) after CopyToAsync completes?

Registration is only useful if we're actually going to cancel an in-process operation, which is not the case here.

if (wait)
{
await GetWaiterTask().ConfigureAwait(false);
using (cancellationToken.Register(s => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This cancellation logic seems like it should apply everywhere that GetWaiterTask is called, e.g. ReadDataAsync.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

In other words, GetWaiterTask should take a CancellationToken, and throw if the CancellationToken is cancelled before the waiter is signalled.

There's some logic to do this with a regular TaskCompletionSource here:

internal sealed class TaskCompletionSourceWithCancellation<T> : TaskCompletionSource<T>

However, we're not using a regular TaskCompletionSource here. We're using the resettable source. I'm not quite sure the best way to make this work with cancellation.

@stephentoub thoughts?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If we need this logic in other places, I agree with @geoffkizer it'd be good to centralize all of this into GetWaiterTask. The downside to that is it'll make GetWaiterTask more expensive. But maybe that doesn't matter.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@stephentoub Any suggestions re how to make GetWaiterTask cancellable? I'm unclear about the interactions with the resettable task stuff here.

If we need this logic in other places, I agree with @geoffkizer it'd be good to centralize all of this into GetWaiterTask

We do -- see ReadDataAsync for example, we're ignoring the cancellationToken there currently.

The downside to that is it'll make GetWaiterTask more expensive. But maybe that doesn't matter.

Only when a cancellationToken is specified, right? (I realize that's the default behavior with HttpClient, but at least it's possible to avoid this if it really matters)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

When I talk to @stephentoub yesterday, it seems like we always pass cancellationToken token in even if user does not. (To implement timeout and cancelation of all pending requests)

Comment thread src/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Http2.cs Outdated
@wfurt
Copy link
Copy Markdown
Member Author

wfurt commented Jun 18, 2019

To address this I moved disposal of CreditManager objects from CheckForShutdown() to actual Dispose, so they follow life cycle of Http2Connection instead of being possibly disposed by GOAWAY frame.

How does this help? If I understand correctly, you've made it so that we will actually dispose CreditManager and the stream Dictionary earlier. That seems like it makes things worse, not better.

I may need to update the code little bit as I chat with @stephentoub afterwards and we want existing streams working even after Http2Connection is disposed. (which I did not plan for) The main reason to move this @geoffkizer was to not trigger it from processing GOAWAY as we may hit race condition.

I'm wondering if we can do same as you did for _writerLock and simply let it go when last reference is gone instead of disposing them explicitly.

If we want streams to be process even after Http2Connection is disposed, can we alternatively keep CreditManager same way and keep it (partially?) functional after disposed?

I can add some more test around disposing scenarios.

@geoffkizer
Copy link
Copy Markdown

I'm wondering if we can do same as you did for _writerLock and simply let it go when last reference is gone instead of disposing them explicitly.

There's an issue for figuring out how to dispose the _writerLock properly, right?

Regardless, I agree that these should be disposed at the same time the _writerLock is disposed. Since we're not doing this currently, I think it's fine to not dispose these at all for now and fix them up as part of the _writerLock fix. We should make a note in the _writerLock issue to fix these as well, and update the related comment.

@wfurt
Copy link
Copy Markdown
Member Author

wfurt commented Jun 19, 2019

I made some updates and added more tests @geoffkizer .

  • If we dispose HttpHandler->Http2Connection while we have pending Request or if we still have SendRequestBodyAsync() request will fail. Even if we already received response headers, we would try to propagate error to response stream.
  • if are done sending request, we can finish processing response. I added test for that.

I think problematic case was when we pick connection from pool and we receive GOAWAY or call Abort/AbortStreams before we can create new stream. In that case _disposed is set to true and CreditManager will fail.
In past, if connection did not have any active streams, that would also dispose the writeLock.

AddStream() already had logic to deal with race and throw exception with allowRetry: true.
I extended that logic so we either create stream and the check for stream count should work or we will fail and retry unless we hit IO or protocol exception.

Comment thread src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Stream.cs Outdated
Comment thread src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Stream.cs Outdated
Comment thread src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Stream.cs Outdated
Comment thread src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionBase.cs Outdated
Comment thread src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs Outdated
Comment thread src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Stream.cs Outdated
Comment thread src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Stream.cs Outdated
Comment thread src/Common/tests/System/Net/Http/Http2LoopbackConnection.cs Outdated
_connectionWindow.Dispose();
_concurrentStreams.Dispose();

// ISSUE #35466
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Why was this comment removed? We haven't actually fixed the issue here, have we?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I think the flow is understood and remaining work tracked by other existing issues. If you have specific 3.0 task let me know or we can keep it open for future investigation.

@wfurt
Copy link
Copy Markdown
Member Author

wfurt commented Jun 23, 2019

/azp run

@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 4 pipeline(s).

@wfurt
Copy link
Copy Markdown
Member Author

wfurt commented Jun 23, 2019

/azp run

@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 4 pipeline(s).

@wfurt
Copy link
Copy Markdown
Member Author

wfurt commented Jun 23, 2019

/azp run

@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 4 pipeline(s).

@wfurt
Copy link
Copy Markdown
Member Author

wfurt commented Jun 23, 2019

Windows.Debug failed in System.Security.Cryptography.Pkcs

@wfurt wfurt merged commit cad5159 into dotnet:master Jun 23, 2019
steveharter pushed a commit to steveharter/dotnet_corefx that referenced this pull request Jun 25, 2019
* follow up on duplex operation

* updates

* add test for cancelation

* log responseHeadersTask if bodyTask fails first

* improve cancelation

* feedback from review

* update SendAsync_ConcurentSendReceive_Fail test

* feedback from review

* update SendSettingsAckAsync

* more HTTP/2 cancelation tests

* update test to accept any derivative from OperationCanceledException

* feedback from review

* roll-back changes in AcceptConnectionAsync
@wfurt wfurt deleted the duplex2 branch June 15, 2020 18:20
picenka21 pushed a commit to picenka21/runtime that referenced this pull request Feb 18, 2022
* follow up on duplex operation

* updates

* add test for cancelation

* log responseHeadersTask if bodyTask fails first

* improve cancelation

* feedback from review

* update SendAsync_ConcurentSendReceive_Fail test

* feedback from review

* update SendSettingsAckAsync

* more HTTP/2 cancelation tests

* update test to accept any derivative from OperationCanceledException

* feedback from review

* roll-back changes in AcceptConnectionAsync


Commit migrated from dotnet/corefx@cad5159
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

HTTP2: Missing duplex steaming - HttpClient.SendAsync with HttpCompletionOption.ResponseHeadersRead

5 participants