Skip to content

Commit 2984b87

Browse files
authoredSep 13, 2023
[QUIC] QuicStream reading/writing work (#90253)
* Added asserts to send buffer helper * Postpone confirming the last RECEIVE event until the data are read * Removed lock * Debug tests * ReadsClosed test fixed, and some better logging * Final task keep alive, abort order, timeout for graceful write-side shutdown in dispose, named constants * Tests * Always wait for SEND_COMPLETE * Exclude BigPayload on platforms where it can OOM * Removed unintended code changes * Reverted postponing reading FIN, if data have chance to get buffered with FIN, we will do that. * Clean ups * Fixed waiting for SEND_COMPLETE * Hold back setting FinalTaskSource and overwrite result if no waiter is there * Cancellation and completion * Comments, fixed FinalTaskSource * Fix assert * Test reseting control stream made more resilient * Attempt to fix still running write while disposing the stream in case of a cancellation * Attempt to fix stress build * Sync Dispose in H3Stream waits for read and write as well
1 parent 7eb6c0c commit 2984b87

18 files changed

+680
-200
lines changed
 

‎src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3Connection.cs

+4-2
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ internal sealed class Http3Connection : HttpConnectionBase
3333

3434
// Our control stream.
3535
private QuicStream? _clientControl;
36+
private Task _sendSettingsTask;
3637

3738
// Server-advertised SETTINGS_MAX_FIELD_SECTION_SIZE
3839
// https://www.rfc-editor.org/rfc/rfc9114.html#section-7.2.4.1-2.2.1
@@ -88,7 +89,7 @@ public Http3Connection(HttpConnectionPool pool, HttpAuthority authority, QuicCon
8889
}
8990

9091
// Errors are observed via Abort().
91-
_ = SendSettingsAsync();
92+
_sendSettingsTask = SendSettingsAsync();
9293

9394
// This process is cleaned up when _connection is disposed, and errors are observed via Abort().
9495
_ = AcceptStreamsAsync();
@@ -150,6 +151,7 @@ private void CheckForShutdown()
150151

151152
if (_clientControl != null)
152153
{
154+
await _sendSettingsTask.ConfigureAwait(false);
153155
await _clientControl.DisposeAsync().ConfigureAwait(false);
154156
_clientControl = null;
155157
}
@@ -486,7 +488,7 @@ private async Task ProcessServerStreamAsync(QuicStream stream)
486488

487489
if (bytesRead == 0)
488490
{
489-
// https://quicwg.org/base-drafts/draft-ietf-quic-http.html#name-unidirectional-streams
491+
// https://www.rfc-editor.org/rfc/rfc9114.html#name-unidirectional-streams
490492
// A sender can close or reset a unidirectional stream unless otherwise specified. A receiver MUST
491493
// tolerate unidirectional streams being closed or reset prior to the reception of the unidirectional
492494
// stream header.

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

+46-13
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ internal sealed class Http3RequestStream : IHttpStreamHeadersHandler, IAsyncDisp
3131
private TaskCompletionSource<bool>? _expect100ContinueCompletionSource; // True indicates we should send content (e.g. received 100 Continue).
3232
private bool _disposed;
3333
private readonly CancellationTokenSource _requestBodyCancellationSource;
34+
private Task? _sendRequestTask; // Set with SendContentAsync, must be awaited before QuicStream.DisposeAsync();
35+
private Task? _readResponseTask; // Set with ReadResponseAsync, must be awaited before QuicStream.DisposeAsync();
3436

3537
// Allocated when we receive a :status header.
3638
private HttpResponseMessage? _response;
@@ -88,9 +90,25 @@ public void Dispose()
8890
{
8991
_disposed = true;
9092
AbortStream();
93+
// We aborted both sides, thus both task should unblock and should be finished before disposing the QuicStream.
94+
WaitUnfinished(_sendRequestTask);
95+
WaitUnfinished(_readResponseTask);
9196
_stream.Dispose();
9297
DisposeSyncHelper();
9398
}
99+
100+
static void WaitUnfinished(Task? task)
101+
{
102+
if (task is not null && !task.IsCompleted)
103+
{
104+
try
105+
{
106+
task.GetAwaiter().GetResult();
107+
}
108+
catch // Exceptions from both tasks are logged via _connection.LogException() in case they're not awaited in SendAsync, so the exception can be ignored here.
109+
{ }
110+
}
111+
}
94112
}
95113

96114
private void RemoveFromConnectionIfDone()
@@ -107,9 +125,25 @@ public async ValueTask DisposeAsync()
107125
{
108126
_disposed = true;
109127
AbortStream();
128+
// We aborted both sides, thus both task should unblock and should be finished before disposing the QuicStream.
129+
await AwaitUnfinished(_sendRequestTask).ConfigureAwait(false);
130+
await AwaitUnfinished(_readResponseTask).ConfigureAwait(false);
110131
await _stream.DisposeAsync().ConfigureAwait(false);
111132
DisposeSyncHelper();
112133
}
134+
135+
static async ValueTask AwaitUnfinished(Task? task)
136+
{
137+
if (task is not null && !task.IsCompleted)
138+
{
139+
try
140+
{
141+
await task.ConfigureAwait(false);
142+
}
143+
catch // Exceptions from both tasks are logged via _connection.LogException() in case they're not awaited in SendAsync, so the exception can be ignored here.
144+
{ }
145+
}
146+
}
113147
}
114148

115149
private void DisposeSyncHelper()
@@ -158,52 +192,51 @@ public async Task<HttpResponseMessage> SendAsync(CancellationToken cancellationT
158192
await FlushSendBufferAsync(endStream: _request.Content == null, _requestBodyCancellationSource.Token).ConfigureAwait(false);
159193
}
160194

161-
Task sendContentTask;
162195
if (_request.Content != null)
163196
{
164-
sendContentTask = SendContentAsync(_request.Content!, _requestBodyCancellationSource.Token);
197+
_sendRequestTask = SendContentAsync(_request.Content!, _requestBodyCancellationSource.Token);
165198
}
166199
else
167200
{
168-
sendContentTask = Task.CompletedTask;
201+
_sendRequestTask = Task.CompletedTask;
169202
}
170203

171204
// In parallel, send content and read response.
172205
// Depending on Expect 100 Continue usage, one will depend on the other making progress.
173-
Task readResponseTask = ReadResponseAsync(_requestBodyCancellationSource.Token);
206+
_readResponseTask = ReadResponseAsync(_requestBodyCancellationSource.Token);
174207
bool sendContentObserved = false;
175208

176209
// If we're not doing duplex, wait for content to finish sending here.
177210
// If we are doing duplex and have the unlikely event that it completes here, observe the result.
178211
// See Http2Connection.SendAsync for a full comment on this logic -- it is identical behavior.
179-
if (sendContentTask.IsCompleted ||
212+
if (_sendRequestTask.IsCompleted ||
180213
_request.Content?.AllowDuplex != true ||
181-
await Task.WhenAny(sendContentTask, readResponseTask).ConfigureAwait(false) == sendContentTask ||
182-
sendContentTask.IsCompleted)
214+
await Task.WhenAny(_sendRequestTask, _readResponseTask).ConfigureAwait(false) == _sendRequestTask ||
215+
_sendRequestTask.IsCompleted)
183216
{
184217
try
185218
{
186-
await sendContentTask.ConfigureAwait(false);
219+
await _sendRequestTask.ConfigureAwait(false);
187220
sendContentObserved = true;
188221
}
189222
catch
190223
{
191-
// Exceptions will be bubbled up from sendContentTask here,
192-
// which means the result of readResponseTask won't be observed directly:
224+
// Exceptions will be bubbled up from _sendRequestTask here,
225+
// which means the result of _readResponseTask won't be observed directly:
193226
// Do a background await to log any exceptions.
194-
_connection.LogExceptions(readResponseTask);
227+
_connection.LogExceptions(_readResponseTask);
195228
throw;
196229
}
197230
}
198231
else
199232
{
200233
// Duplex is being used, so we can't wait for content to finish sending.
201234
// Do a background await to log any exceptions.
202-
_connection.LogExceptions(sendContentTask);
235+
_connection.LogExceptions(_sendRequestTask);
203236
}
204237

205238
// Wait for the response headers to be read.
206-
await readResponseTask.ConfigureAwait(false);
239+
await _readResponseTask.ConfigureAwait(false);
207240

208241
Debug.Assert(_response != null && _response.Content != null);
209242
// Set our content stream.

‎src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Http3.cs

+31-6
Original file line numberDiff line numberDiff line change
@@ -1664,10 +1664,17 @@ public async Task ServerSendsTrailingHeaders_Success()
16641664

16651665
}
16661666

1667+
public enum CloseOutboundControlStream
1668+
{
1669+
BogusData,
1670+
Dispose,
1671+
Abort,
1672+
}
16671673
[Theory]
1668-
[InlineData(true)]
1669-
[InlineData(false)]
1670-
public async Task ServerClosesOutboundControlStream_ClientClosesConnection(bool graceful)
1674+
[InlineData(CloseOutboundControlStream.BogusData)]
1675+
[InlineData(CloseOutboundControlStream.Dispose)]
1676+
[InlineData(CloseOutboundControlStream.Abort)]
1677+
public async Task ServerClosesOutboundControlStream_ClientClosesConnection(CloseOutboundControlStream closeType)
16711678
{
16721679
using Http3LoopbackServer server = CreateHttp3LoopbackServer();
16731680

@@ -1680,13 +1687,31 @@ public async Task ServerClosesOutboundControlStream_ClientClosesConnection(bool
16801687
await using Http3LoopbackStream requestStream = await connection.AcceptRequestStreamAsync();
16811688

16821689
// abort the control stream
1683-
if (graceful)
1690+
if (closeType == CloseOutboundControlStream.BogusData)
16841691
{
16851692
await connection.OutboundControlStream.SendResponseBodyAsync(Array.Empty<byte>(), isFinal: true);
16861693
}
1687-
else
1694+
else if (closeType == CloseOutboundControlStream.Dispose)
16881695
{
1689-
connection.OutboundControlStream.Abort(Http3LoopbackConnection.H3_INTERNAL_ERROR);
1696+
await connection.OutboundControlStream.DisposeAsync();
1697+
}
1698+
else if (closeType == CloseOutboundControlStream.Abort)
1699+
{
1700+
int iterations = 5;
1701+
while (iterations-- > 0)
1702+
{
1703+
connection.OutboundControlStream.Abort(Http3LoopbackConnection.H3_INTERNAL_ERROR);
1704+
// This sends RESET_FRAME which might cause complete discard of any data including stream type, leading to client ignoring the stream.
1705+
// Attempt to establish the control stream again then.
1706+
if (await semaphore.WaitAsync(100))
1707+
{
1708+
// Client finished with the expected error.
1709+
return;
1710+
}
1711+
await connection.OutboundControlStream.DisposeAsync();
1712+
await connection.EstablishControlStreamAsync(Array.Empty<SettingsEntry>());
1713+
await Task.Delay(100);
1714+
}
16901715
}
16911716

16921717
// wait for client task before tearing down the requestStream and connection

‎src/libraries/System.Net.Http/tests/StressTests/HttpStress/Directory.Build.targets

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@
66
Define this here because the SDK resets it
77
unconditionally in Microsoft.NETCoreSdk.BundledVersions.props.
88
-->
9-
<NETCoreAppMaximumVersion>8.0</NETCoreAppMaximumVersion>
9+
<NETCoreAppMaximumVersion>9.0</NETCoreAppMaximumVersion>
1010
</PropertyGroup>
1111
</Project>

‎src/libraries/System.Net.Http/tests/StressTests/HttpStress/build-local.ps1

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
## Usage:
44
## ./build-local.ps1 [StressConfiguration] [LibrariesConfiguration]
55

6-
$Version="8.0"
6+
$Version="9.0"
77
$RepoRoot="$(git rev-parse --show-toplevel)"
88
$DailyDotnetRoot= "./.dotnet-daily"
99

‎src/libraries/System.Net.Http/tests/StressTests/HttpStress/build-local.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
## Usage:
66
## ./build-local.sh [StressConfiguration] [LibrariesConfiguration]
77

8-
version=8.0
8+
version=9.0
99
repo_root=$(git rev-parse --show-toplevel)
1010
daily_dotnet_root=./.dotnet-daily
1111

‎src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/MsQuicBuffers.cs

+7-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Collections.Generic;
5+
using System.Diagnostics;
56
using System.Runtime.InteropServices;
67
using Microsoft.Quic;
78

@@ -32,8 +33,8 @@ private void FreeNativeMemory()
3233
{
3334
QUIC_BUFFER* buffers = _buffers;
3435
_buffers = null;
35-
NativeMemory.Free(buffers);
3636
_count = 0;
37+
NativeMemory.Free(buffers);
3738
}
3839

3940
private void Reserve(int count)
@@ -48,6 +49,10 @@ private void Reserve(int count)
4849

4950
private void SetBuffer(int index, ReadOnlyMemory<byte> buffer)
5051
{
52+
Debug.Assert(index < _count);
53+
Debug.Assert(_buffers[index].Buffer is null);
54+
Debug.Assert(_buffers[index].Length == 0);
55+
5156
_buffers[index].Buffer = (byte*)NativeMemory.Alloc((nuint)buffer.Length, (nuint)sizeof(byte));
5257
_buffers[index].Length = (uint)buffer.Length;
5358
buffer.Span.CopyTo(_buffers[index].Span);
@@ -93,8 +98,8 @@ public void Reset()
9398
}
9499
byte* buffer = _buffers[i].Buffer;
95100
_buffers[i].Buffer = null;
96-
NativeMemory.Free(buffer);
97101
_buffers[i].Length = 0;
102+
NativeMemory.Free(buffer);
98103
}
99104
}
100105

‎src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/MsQuicConfiguration.cs

-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
using System.Security.Cryptography.X509Certificates;
88
using System.Threading;
99
using Microsoft.Quic;
10-
using static Microsoft.Quic.MsQuic;
1110

1211
namespace System.Net.Quic;
1312

‎src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/MsQuicExtensions.cs

+2
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ public override string ToString()
6666
=> $"{{ {nameof(SEND_SHUTDOWN_COMPLETE.Graceful)} = {SEND_SHUTDOWN_COMPLETE.Graceful} }}",
6767
QUIC_STREAM_EVENT_TYPE.SHUTDOWN_COMPLETE
6868
=> $"{{ {nameof(SHUTDOWN_COMPLETE.ConnectionShutdown)} = {SHUTDOWN_COMPLETE.ConnectionShutdown}, {nameof(SHUTDOWN_COMPLETE.ConnectionShutdownByApp)} = {SHUTDOWN_COMPLETE.ConnectionShutdownByApp}, {nameof(SHUTDOWN_COMPLETE.ConnectionClosedRemotely)} = {SHUTDOWN_COMPLETE.ConnectionClosedRemotely}, {nameof(SHUTDOWN_COMPLETE.ConnectionErrorCode)} = {SHUTDOWN_COMPLETE.ConnectionErrorCode}, {nameof(SHUTDOWN_COMPLETE.ConnectionCloseStatus)} = {SHUTDOWN_COMPLETE.ConnectionCloseStatus} }}",
69+
QUIC_STREAM_EVENT_TYPE.IDEAL_SEND_BUFFER_SIZE
70+
=> $"{{ {nameof(IDEAL_SEND_BUFFER_SIZE.ByteCount)} = {IDEAL_SEND_BUFFER_SIZE.ByteCount} }}",
6971
_ => string.Empty
7072
};
7173
}

‎src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/MsQuicTlsSecret.cs

-2
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,9 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
#if DEBUG
5-
using System.Collections.Generic;
65
using System.IO;
76
using System.Runtime.InteropServices;
87
using System.Text;
9-
using System.Threading;
108
using Microsoft.Quic;
119
using static Microsoft.Quic.MsQuic;
1210

‎src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/ReceiveBuffers.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ public int CopyFrom(ReadOnlySpan<QUIC_BUFFER> quicBuffers, int totalLength, bool
6666
}
6767
}
6868

69-
public int CopyTo(Memory<byte> buffer, out bool isCompleted, out bool isEmpty)
69+
public int CopyTo(Memory<byte> buffer, out bool completed, out bool empty)
7070
{
7171
lock (_syncRoot)
7272
{
@@ -79,8 +79,8 @@ public int CopyTo(Memory<byte> buffer, out bool isCompleted, out bool isEmpty)
7979
_buffer.Discard(copied);
8080
}
8181

82-
isCompleted = _buffer.IsEmpty && _final;
83-
isEmpty = _buffer.IsEmpty;
82+
completed = _buffer.IsEmpty && _final;
83+
empty = _buffer.IsEmpty;
8484

8585
return copied;
8686
}

0 commit comments

Comments
 (0)
Please sign in to comment.