Skip to content

Commit e4b07f4

Browse files
authored
[H/3] Fix disposing of buffers in parallel with running tasks (#117207)
* Fix disposing of buffers in parallel with running tasks * Better wording * Renamed variable and more comments.
1 parent e2ca190 commit e4b07f4

File tree

1 file changed

+60
-26
lines changed

1 file changed

+60
-26
lines changed

src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3RequestStream.cs

Lines changed: 60 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ internal sealed class Http3RequestStream : IHttpStreamHeadersHandler, IAsyncDisp
2626
private Http3Connection _connection;
2727
private long _streamId = -1; // A stream does not have an ID until the first I/O against it. This gets set almost immediately following construction.
2828
private readonly QuicStream _stream;
29+
private volatile bool _finishingBackgroundWrite;
2930
private ArrayBuffer _sendBuffer;
31+
private volatile bool _finishingBackgroundRead;
3032
private ArrayBuffer _recvBuffer;
3133
private TaskCompletionSource<bool>? _expect100ContinueCompletionSource; // True indicates we should send content (e.g. received 100 Continue).
3234
private bool _disposed;
@@ -130,8 +132,18 @@ private void DisposeSyncHelper()
130132
{
131133
_connection.RemoveStream(_stream);
132134

133-
_sendBuffer.Dispose();
134-
_recvBuffer.Dispose();
135+
// If the request sending was offloaded to be done concurrently and not awaited within SendAsync (by calling connection.LogException),
136+
// the _sendBuffer disposal is the responsibility of that offloaded task to prevent returning the buffer to the pool while it still might be in use.
137+
if (!_finishingBackgroundWrite)
138+
{
139+
_sendBuffer.Dispose();
140+
}
141+
// If the response receiving was offloaded to be done concurrently and not awaited within SendAsync (by calling connection.LogException),
142+
// the _recvBuffer disposal is the responsibility of that offloaded task to prevent returning the buffer to the pool while it still might be in use.
143+
if (!_finishingBackgroundRead)
144+
{
145+
_recvBuffer.Dispose();
146+
}
135147
}
136148

137149
public void GoAway()
@@ -196,6 +208,11 @@ await Task.WhenAny(sendRequestTask, readResponseTask).ConfigureAwait(false) == s
196208
}
197209
catch
198210
{
211+
// This is a best effort attempt to transfer the responsibility of disposing _recvBuffer to ReadResponseAsync.
212+
// The task might be past checking the variable or already finished, in which case the buffer won't be returned to the pool.
213+
// Not returning the buffer to the pool is an acceptable trade-off for making sure that the buffer is not used after it's been returned.
214+
_finishingBackgroundRead = true;
215+
199216
// Exceptions will be bubbled up from sendRequestTask here,
200217
// which means the result of readResponseTask won't be observed directly:
201218
// Do a background await to log any exceptions.
@@ -205,6 +222,11 @@ await Task.WhenAny(sendRequestTask, readResponseTask).ConfigureAwait(false) == s
205222
}
206223
else
207224
{
225+
// This is a best effort attempt to transfer the responsibility of disposing _sendBuffer to SendContentAsync.
226+
// The task might be past checking the variable or already finished, in which case the buffer won't be returned to the pool.
227+
// Not returning the buffer to the pool is an acceptable trade-off for making sure that the buffer is not used after it's been returned.
228+
_finishingBackgroundWrite = true;
229+
208230
// Duplex is being used, so we can't wait for content to finish sending.
209231
// Do a background await to log any exceptions.
210232
_connection.LogExceptions(sendRequestTask);
@@ -226,12 +248,6 @@ await Task.WhenAny(sendRequestTask, readResponseTask).ConfigureAwait(false) == s
226248
{
227249
// The server doesn't need the whole request to respond so it's aborting its reading side gracefully, see https://datatracker.ietf.org/doc/html/rfc9114#section-4.1-15.
228250
}
229-
catch (OperationCanceledException)
230-
{
231-
// If the request got cancelled before WritesClosed completed, avoid leaking an unobserved task exception.
232-
_connection.LogExceptions(writesClosed);
233-
throw;
234-
}
235251
}
236252

237253
Debug.Assert(_response != null && _response.Content != null);
@@ -387,32 +403,44 @@ await Task.WhenAny(sendRequestTask, readResponseTask).ConfigureAwait(false) == s
387403
/// </summary>
388404
private async Task ReadResponseAsync(CancellationToken cancellationToken)
389405
{
390-
if (HttpTelemetry.Log.IsEnabled()) HttpTelemetry.Log.ResponseHeadersStart();
391-
392-
Debug.Assert(_response == null);
393-
do
406+
try
394407
{
395-
_headerState = HeaderState.StatusHeader;
408+
if (HttpTelemetry.Log.IsEnabled()) HttpTelemetry.Log.ResponseHeadersStart();
396409

397-
(Http3FrameType? frameType, long payloadLength) = await ReadFrameEnvelopeAsync(cancellationToken).ConfigureAwait(false);
398-
399-
if (frameType != Http3FrameType.Headers)
410+
Debug.Assert(_response == null);
411+
do
400412
{
401-
if (NetEventSource.Log.IsEnabled())
413+
_headerState = HeaderState.StatusHeader;
414+
415+
(Http3FrameType? frameType, long payloadLength) = await ReadFrameEnvelopeAsync(cancellationToken).ConfigureAwait(false);
416+
417+
if (frameType != Http3FrameType.Headers)
402418
{
403-
Trace($"Expected HEADERS as first response frame; received {frameType}.");
419+
if (NetEventSource.Log.IsEnabled())
420+
{
421+
Trace($"Expected HEADERS as first response frame; received {frameType}.");
422+
}
423+
throw new HttpIOException(HttpRequestError.InvalidResponse, SR.net_http_invalid_response);
404424
}
405-
throw new HttpIOException(HttpRequestError.InvalidResponse, SR.net_http_invalid_response);
406-
}
407425

408-
await ReadHeadersAsync(payloadLength, cancellationToken).ConfigureAwait(false);
409-
Debug.Assert(_response != null);
410-
}
411-
while ((int)_response.StatusCode < 200);
426+
await ReadHeadersAsync(payloadLength, cancellationToken).ConfigureAwait(false);
427+
Debug.Assert(_response != null);
428+
}
429+
while ((int)_response.StatusCode < 200);
412430

413-
_headerState = HeaderState.TrailingHeaders;
431+
_headerState = HeaderState.TrailingHeaders;
414432

415-
if (HttpTelemetry.Log.IsEnabled()) HttpTelemetry.Log.ResponseHeadersStop((int)_response.StatusCode);
433+
if (HttpTelemetry.Log.IsEnabled()) HttpTelemetry.Log.ResponseHeadersStop((int)_response.StatusCode);
434+
}
435+
finally
436+
{
437+
// Note that we might still observe false here even if we're responsible for the _recvBuffer disposal.
438+
// But in that case, we just don't return the rented buffer to the pool, which is lesser evil than writing to returned one.
439+
if (_finishingBackgroundRead)
440+
{
441+
_recvBuffer.Dispose();
442+
}
443+
}
416444
}
417445

418446
private async Task SendContentAsync(HttpContent content, CancellationToken cancellationToken)
@@ -491,6 +519,12 @@ private async Task SendContentAsync(HttpContent content, CancellationToken cance
491519
}
492520
finally
493521
{
522+
// Note that we might still observe false here even if we're responsible for the _sendBuffer disposal.
523+
// But in that case, we just don't return the rented buffer to the pool, which is lesser evil than writing to returned one.
524+
if (_finishingBackgroundWrite)
525+
{
526+
_sendBuffer.Dispose();
527+
}
494528
_requestSendCompleted = true;
495529
RemoveFromConnectionIfDone();
496530
}

0 commit comments

Comments
 (0)