-
Notifications
You must be signed in to change notification settings - Fork 5.1k
Drain HTTP/3 response after trailers #116319
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
Changes from 4 commits
d95116d
ce59e6c
2080465
3e22c05
5c47eea
a14c53e
38882ef
bb85d20
6e68380
ece824a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -1,6 +1,7 @@ | ||||||||||||||||||||||||
// Licensed to the .NET Foundation under one or more agreements. | ||||||||||||||||||||||||
// The .NET Foundation licenses this file to you under the MIT license. | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
using System.Buffers; | ||||||||||||||||||||||||
using System.Collections.Generic; | ||||||||||||||||||||||||
using System.Diagnostics; | ||||||||||||||||||||||||
using System.Diagnostics.CodeAnalysis; | ||||||||||||||||||||||||
|
@@ -46,6 +47,9 @@ internal sealed class Http3RequestStream : IHttpStreamHeadersHandler, IAsyncDisp | |||||||||||||||||||||||
/// <summary>Any trailing headers.</summary> | ||||||||||||||||||||||||
private List<(HeaderDescriptor name, string value)>? _trailingHeaders; | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
/// <summary>Response drain task after receiving trailers.</summary> | ||||||||||||||||||||||||
private Task? _responseDrainTask; | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
// When reading response content, keep track of the number of bytes left in the current data frame. | ||||||||||||||||||||||||
private long _responseDataPayloadRemaining; | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
|
@@ -82,56 +86,64 @@ public Http3RequestStream(HttpRequestMessage request, Http3Connection connection | |||||||||||||||||||||||
_responseRecvCompleted = false; | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
public void Dispose() | ||||||||||||||||||||||||
// Synchronous QuicStream.Dispose() is implemented in a sync-over-async manner, | ||||||||||||||||||||||||
// there is no benefit from maintaining a separate synchronous implementation in this type. | ||||||||||||||||||||||||
public void Dispose() => DisposeAsync().AsTask().GetAwaiter().GetResult(); | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
public async ValueTask DisposeAsync() | ||||||||||||||||||||||||
{ | ||||||||||||||||||||||||
if (!_disposed) | ||||||||||||||||||||||||
{ | ||||||||||||||||||||||||
_disposed = true; | ||||||||||||||||||||||||
AbortStream(); | ||||||||||||||||||||||||
if (_stream.WritesClosed.IsCompleted) | ||||||||||||||||||||||||
Task? disposeTask = null; | ||||||||||||||||||||||||
if (_responseDrainTask is not null) | ||||||||||||||||||||||||
{ | ||||||||||||||||||||||||
_connection.LogExceptions(_stream.DisposeAsync().AsTask()); | ||||||||||||||||||||||||
disposeTask = WaitForDrainCompletionAndDisposeAsync(); | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
else | ||||||||||||||||||||||||
{ | ||||||||||||||||||||||||
_stream.Dispose(); | ||||||||||||||||||||||||
AbortStream(); | ||||||||||||||||||||||||
if (_stream.WritesClosed.IsCompleted) | ||||||||||||||||||||||||
{ | ||||||||||||||||||||||||
disposeTask = _stream.DisposeAsync().AsTask(); | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
DisposeSyncHelper(); | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
private void RemoveFromConnectionIfDone() | ||||||||||||||||||||||||
{ | ||||||||||||||||||||||||
if (_responseRecvCompleted && _requestSendCompleted) | ||||||||||||||||||||||||
{ | ||||||||||||||||||||||||
_connection.RemoveStream(_stream); | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
public async ValueTask DisposeAsync() | ||||||||||||||||||||||||
{ | ||||||||||||||||||||||||
if (!_disposed) | ||||||||||||||||||||||||
{ | ||||||||||||||||||||||||
_disposed = true; | ||||||||||||||||||||||||
AbortStream(); | ||||||||||||||||||||||||
if (_stream.WritesClosed.IsCompleted) | ||||||||||||||||||||||||
if (disposeTask is not null) | ||||||||||||||||||||||||
{ | ||||||||||||||||||||||||
_connection.LogExceptions(_stream.DisposeAsync().AsTask()); | ||||||||||||||||||||||||
_connection.LogExceptions(disposeTask); | ||||||||||||||||||||||||
antonfirsov marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||
} | ||||||||||||||||||||||||
else | ||||||||||||||||||||||||
{ | ||||||||||||||||||||||||
await _stream.DisposeAsync().ConfigureAwait(false); | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
DisposeSyncHelper(); | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
_connection.RemoveStream(_stream); | ||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm wondering if we could hit the issue with GC eating the H3 stream while inside There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see us doing the same in Line 65 in ff8c934
I must admit that I'm confused. According to my understanding of the article, the problem is that the state machine is unrooted (thus all its' references to locals and Edit: changed the comment to keep it on point There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @stephentoub can you please help and give some pointers what should the right approach here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is the question? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm introducing a method here that is usually called in a fire-and-forget manner: runtime/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3RequestStream.cs Lines 156 to 163 in bb85d20
The owning There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As noted in the linked blog post, async method state machine objects are kept alive by the thing they're awaiting. The question then isn't whether Http3RequestStream is rooted, but whether _responseDrainTask will eventually complete and is thus itself rooted (since the only way it could be completed is if something rooted was referencing it in order to complete it). |
||||||||||||||||||||||||
_sendBuffer.Dispose(); | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
// If response drain is in progress it might be still using _recvBuffer; let WaitForDrainCompletionAndDisposeAsync() dispose it. | ||||||||||||||||||||||||
if (_responseDrainTask is null) | ||||||||||||||||||||||||
{ | ||||||||||||||||||||||||
_recvBuffer.Dispose(); | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
async Task WaitForDrainCompletionAndDisposeAsync() | ||||||||||||||||||||||||
{ | ||||||||||||||||||||||||
Debug.Assert(_responseDrainTask is not null); | ||||||||||||||||||||||||
await _responseDrainTask.ConfigureAwait(false); | ||||||||||||||||||||||||
AbortStream(); | ||||||||||||||||||||||||
await _stream.DisposeAsync().ConfigureAwait(false); | ||||||||||||||||||||||||
_recvBuffer.Dispose(); | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
private void DisposeSyncHelper() | ||||||||||||||||||||||||
private void RemoveFromConnectionIfDone() | ||||||||||||||||||||||||
{ | ||||||||||||||||||||||||
_connection.RemoveStream(_stream); | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
_sendBuffer.Dispose(); | ||||||||||||||||||||||||
_recvBuffer.Dispose(); | ||||||||||||||||||||||||
if (_responseRecvCompleted && _requestSendCompleted) | ||||||||||||||||||||||||
{ | ||||||||||||||||||||||||
_connection.RemoveStream(_stream); | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
antonfirsov marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||
|
||||||||||||||||||||||||
public void GoAway() | ||||||||||||||||||||||||
|
@@ -564,10 +576,9 @@ private async ValueTask DrainContentLength0Frames(CancellationToken cancellation | |||||||||||||||||||||||
_trailingHeaders = new List<(HeaderDescriptor name, string value)>(); | ||||||||||||||||||||||||
await ReadHeadersAsync(payloadLength, cancellationToken).ConfigureAwait(false); | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
// Stop looping after a trailing header. | ||||||||||||||||||||||||
// There may be extra frames after this one, but they would all be unknown extension | ||||||||||||||||||||||||
// frames that can be safely ignored. Just stop reading here. | ||||||||||||||||||||||||
// Note: this does leave us open to a bad server sending us an out of order DATA frame. | ||||||||||||||||||||||||
// We do not expect more DATA frames after the trailers. | ||||||||||||||||||||||||
// Start draining the response to avoid aborting reads during disposal. | ||||||||||||||||||||||||
_responseDrainTask = DrainResponseAsync(); | ||||||||||||||||||||||||
antonfirsov marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||
goto case null; | ||||||||||||||||||||||||
case null: | ||||||||||||||||||||||||
// Done receiving: copy over trailing headers. | ||||||||||||||||||||||||
|
@@ -1335,6 +1346,54 @@ private void HandleReadResponseContentException(Exception ex, CancellationToken | |||||||||||||||||||||||
throw new HttpIOException(HttpRequestError.Unknown, SR.net_http_client_execution_error, new HttpRequestException(SR.net_http_client_execution_error, ex)); | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
|
||||||||||||||||||||||||
/// <summary> | ||||||||||||||||||||||||
/// Drain the underlying QuicStream without attempting to interpret the data. | ||||||||||||||||||||||||
/// Note: this does leave us open to a bad server sending us out of order frames. | ||||||||||||||||||||||||
/// </summary> | ||||||||||||||||||||||||
private async Task DrainResponseAsync() | ||||||||||||||||||||||||
{ | ||||||||||||||||||||||||
HttpConnectionSettings settings = _connection.Pool.Settings; | ||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You could short-circuit this with checking There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||||||||||||||||||||
TimeSpan drainTime = settings._maxResponseDrainTime; | ||||||||||||||||||||||||
int remaining = settings._maxResponseDrainSize; | ||||||||||||||||||||||||
Debug.Assert(remaining >= 0); | ||||||||||||||||||||||||
if (drainTime == TimeSpan.Zero || remaining == 0) | ||||||||||||||||||||||||
{ | ||||||||||||||||||||||||
return; | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
using CancellationTokenSource cts = new CancellationTokenSource(settings._maxResponseDrainTime); | ||||||||||||||||||||||||
try | ||||||||||||||||||||||||
{ | ||||||||||||||||||||||||
// If there is more data than MaxResponseDrainSize, we will silently stop draining and let Dispose(Async) abort the reads. | ||||||||||||||||||||||||
while (remaining > 0) | ||||||||||||||||||||||||
{ | ||||||||||||||||||||||||
_recvBuffer.EnsureAvailableSpace(1); | ||||||||||||||||||||||||
Memory<byte> buffer = remaining >= _recvBuffer.AvailableMemory.Length ? _recvBuffer.AvailableMemory : _recvBuffer.AvailableMemory.Slice(0, remaining); | ||||||||||||||||||||||||
int bytesRead = await _stream.ReadAsync(buffer, cts.Token).ConfigureAwait(false); | ||||||||||||||||||||||||
if (bytesRead == 0) | ||||||||||||||||||||||||
{ | ||||||||||||||||||||||||
// Reached EOS. | ||||||||||||||||||||||||
return; | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
remaining -= bytesRead; | ||||||||||||||||||||||||
_recvBuffer.Commit(bytesRead); | ||||||||||||||||||||||||
_recvBuffer.Discard(bytesRead); | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
catch (Exception ex) | ||||||||||||||||||||||||
{ | ||||||||||||||||||||||||
// Eat exceptions and stop draining to unblock QuicStream disposal waiting for response drain. | ||||||||||||||||||||||||
if (NetEventSource.Log.IsEnabled()) | ||||||||||||||||||||||||
{ | ||||||||||||||||||||||||
string message = ex is OperationCanceledException oce && oce.CancellationToken == cts.Token ? "Response drain timed out." : $"Response drain failed with exception: {ex}"; | ||||||||||||||||||||||||
Trace(message); | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
return; | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
private async ValueTask<bool> ReadNextDataFrameAsync(HttpResponseMessage response, CancellationToken cancellationToken) | ||||||||||||||||||||||||
{ | ||||||||||||||||||||||||
if (_responseDataPayloadRemaining == -1) | ||||||||||||||||||||||||
|
@@ -1365,11 +1424,10 @@ private async ValueTask<bool> ReadNextDataFrameAsync(HttpResponseMessage respons | |||||||||||||||||||||||
_trailingHeaders = new List<(HeaderDescriptor name, string value)>(); | ||||||||||||||||||||||||
await ReadHeadersAsync(payloadLength, cancellationToken).ConfigureAwait(false); | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
// There may be more frames after this one, but they would all be unknown extension | ||||||||||||||||||||||||
// frames that we are allowed to skip. Just close the stream early. | ||||||||||||||||||||||||
// We do not expect more DATA frames after the trailers. | ||||||||||||||||||||||||
// Start draining the response to avoid aborting reads during disposal. | ||||||||||||||||||||||||
_responseDrainTask = DrainResponseAsync(); | ||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So this change will only drain the stream if there are trailers, right? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yes. Since #60118 talked about only this specific case, and I didn't consider others. Should we do it? If yes should it happen in this PR? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmmm, what I'm thinking is that if it means just to just plug-in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I spent more time examining runtime/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3RequestStream.cs Lines 1374 to 1379 in 0d628da
The primary issue with the trailer handling logic on This will short circuit subsequent calls to runtime/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3RequestStream.cs Lines 1340 to 1344 in 0d628da
|
||||||||||||||||||||||||
|
||||||||||||||||||||||||
// Note: if a server sends additional HEADERS or DATA frames at this point, it | ||||||||||||||||||||||||
// would be a connection error -- not draining the stream means we won't catch this. | ||||||||||||||||||||||||
goto case null; | ||||||||||||||||||||||||
case null: | ||||||||||||||||||||||||
// End of stream. | ||||||||||||||||||||||||
|
Uh oh!
There was an error while loading. Please reload this page.