Skip to content

Commit a822d39

Browse files
authored
QUIC stream limits (#52704)
Implements the 3rd option Allowing the caller to perform their own wait from #32079 (comment) Adds WaitForAvailable(Bidi|Uni)rectionalStreamsAsync: - triggered by peer announcement about new streams (QUIC_CONNECTION_EVENT_TYPE.STREAMS_AVAILABLE) - if the connection is closed/disposed, the method throws QuicConnectionAbortedException which fitted our H3 better than boolean (can be changed) Changes stream limit type to int
1 parent a9d2f03 commit a822d39

File tree

17 files changed

+611
-163
lines changed

17 files changed

+611
-163
lines changed

src/libraries/Common/tests/System/Net/Http/Http3LoopbackServer.cs

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,26 +20,32 @@ public sealed class Http3LoopbackServer : GenericLoopbackServer
2020

2121
public override Uri Address => new Uri($"https://{_listener.ListenEndPoint}/");
2222

23-
public Http3LoopbackServer(QuicImplementationProvider quicImplementationProvider = null, GenericLoopbackOptions options = null)
23+
public Http3LoopbackServer(QuicImplementationProvider quicImplementationProvider = null, Http3Options options = null)
2424
{
25-
options ??= new GenericLoopbackOptions();
25+
options ??= new Http3Options();
2626

2727
_cert = Configuration.Certificates.GetServerCertificate();
2828

29-
var sslOpts = new SslServerAuthenticationOptions
29+
var listenerOptions = new QuicListenerOptions()
3030
{
31-
EnabledSslProtocols = options.SslProtocols,
32-
ApplicationProtocols = new List<SslApplicationProtocol>
31+
ListenEndPoint = new IPEndPoint(options.Address, 0),
32+
ServerAuthenticationOptions = new SslServerAuthenticationOptions
3333
{
34-
new SslApplicationProtocol("h3-31"),
35-
new SslApplicationProtocol("h3-30"),
36-
new SslApplicationProtocol("h3-29")
34+
EnabledSslProtocols = options.SslProtocols,
35+
ApplicationProtocols = new List<SslApplicationProtocol>
36+
{
37+
new SslApplicationProtocol("h3-31"),
38+
new SslApplicationProtocol("h3-30"),
39+
new SslApplicationProtocol("h3-29")
40+
},
41+
ServerCertificate = _cert,
42+
ClientCertificateRequired = false
3743
},
38-
ServerCertificate = _cert,
39-
ClientCertificateRequired = false
44+
MaxUnidirectionalStreams = options.MaxUnidirectionalStreams,
45+
MaxBidirectionalStreams = options.MaxBidirectionalStreams,
4046
};
4147

42-
_listener = new QuicListener(quicImplementationProvider ?? QuicImplementationProviders.Default, new IPEndPoint(options.Address, 0), sslOpts);
48+
_listener = new QuicListener(quicImplementationProvider ?? QuicImplementationProviders.Default, listenerOptions);
4349
}
4450

4551
public override void Dispose()
@@ -82,7 +88,7 @@ public Http3LoopbackServerFactory(QuicImplementationProvider quicImplementationP
8288

8389
public override GenericLoopbackServer CreateServer(GenericLoopbackOptions options = null)
8490
{
85-
return new Http3LoopbackServer(_quicImplementationProvider, options);
91+
return new Http3LoopbackServer(_quicImplementationProvider, CreateOptions(options));
8692
}
8793

8894
public override async Task CreateServerAsync(Func<GenericLoopbackServer, Uri, Task> funcAsync, int millisecondsTimeout = 60000, GenericLoopbackOptions options = null)
@@ -97,5 +103,29 @@ public override Task<GenericLoopbackConnection> CreateConnectionAsync(Socket soc
97103
// This method is always unacceptable to call for HTTP/3.
98104
throw new NotImplementedException("HTTP/3 does not operate over a Socket.");
99105
}
106+
107+
private static Http3Options CreateOptions(GenericLoopbackOptions options)
108+
{
109+
Http3Options http3Options = new Http3Options();
110+
if (options != null)
111+
{
112+
http3Options.Address = options.Address;
113+
http3Options.UseSsl = options.UseSsl;
114+
http3Options.SslProtocols = options.SslProtocols;
115+
http3Options.ListenBacklog = options.ListenBacklog;
116+
}
117+
return http3Options;
118+
}
119+
}
120+
public class Http3Options : GenericLoopbackOptions
121+
{
122+
public int MaxUnidirectionalStreams {get; set; }
123+
124+
public int MaxBidirectionalStreams {get; set; }
125+
public Http3Options()
126+
{
127+
MaxUnidirectionalStreams = 100;
128+
MaxBidirectionalStreams = 100;
129+
}
100130
}
101131
}

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

Lines changed: 19 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,6 @@ internal sealed class Http3Connection : HttpConnectionBase, IDisposable
4949
private int _haveServerQpackDecodeStream;
5050
private int _haveServerQpackEncodeStream;
5151

52-
// Manages MAX_STREAM count from server.
53-
private long _maximumRequestStreams;
54-
private long _requestStreamsRemaining;
55-
private readonly Queue<TaskCompletionSourceWithCancellation<bool>> _waitingRequests = new Queue<TaskCompletionSourceWithCancellation<bool>>();
56-
5752
// A connection-level error will abort any future operations.
5853
private Exception? _abortException;
5954

@@ -87,8 +82,6 @@ public Http3Connection(HttpConnectionPool pool, HttpAuthority? origin, HttpAutho
8782
string altUsedValue = altUsedDefaultPort ? authority.IdnHost : authority.IdnHost + ":" + authority.Port.ToString(Globalization.CultureInfo.InvariantCulture);
8883
_altUsedEncodedHeader = QPack.QPackEncoder.EncodeLiteralHeaderFieldWithoutNameReferenceToArray(KnownHeaders.AltUsed.Name, altUsedValue);
8984

90-
_maximumRequestStreams = _requestStreamsRemaining = connection.GetRemoteAvailableBidirectionalStreamCount();
91-
9285
// Errors are observed via Abort().
9386
_ = SendSettingsAsync();
9487

@@ -166,54 +159,41 @@ public override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage req
166159
{
167160
Debug.Assert(async);
168161

169-
// Wait for an available stream (based on QUIC MAX_STREAMS) if there isn't one available yet.
170-
171-
TaskCompletionSourceWithCancellation<bool>? waitForAvailableStreamTcs = null;
172-
173-
lock (SyncObj)
174-
{
175-
long remaining = _requestStreamsRemaining;
176-
177-
if (remaining > 0)
178-
{
179-
_requestStreamsRemaining = remaining - 1;
180-
}
181-
else
182-
{
183-
waitForAvailableStreamTcs = new TaskCompletionSourceWithCancellation<bool>();
184-
_waitingRequests.Enqueue(waitForAvailableStreamTcs);
185-
}
186-
}
187-
188-
if (waitForAvailableStreamTcs != null)
189-
{
190-
await waitForAvailableStreamTcs.WaitWithCancellationAsync(cancellationToken).ConfigureAwait(false);
191-
}
192-
193162
// Allocate an active request
194-
195163
QuicStream? quicStream = null;
196164
Http3RequestStream? requestStream = null;
165+
ValueTask waitTask = default;
197166

198167
try
199168
{
200-
lock (SyncObj)
169+
while (true)
201170
{
202-
if (_connection != null)
171+
lock (SyncObj)
203172
{
204-
quicStream = _connection.OpenBidirectionalStream();
205-
requestStream = new Http3RequestStream(request, this, quicStream);
206-
_activeRequests.Add(quicStream, requestStream);
173+
if (_connection == null)
174+
{
175+
break;
176+
}
177+
178+
if (_connection.GetRemoteAvailableBidirectionalStreamCount() > 0)
179+
{
180+
quicStream = _connection.OpenBidirectionalStream();
181+
requestStream = new Http3RequestStream(request, this, quicStream);
182+
_activeRequests.Add(quicStream, requestStream);
183+
break;
184+
}
185+
waitTask = _connection.WaitForAvailableBidirectionalStreamsAsync(cancellationToken);
207186
}
187+
188+
// Wait for an available stream (based on QUIC MAX_STREAMS) if there isn't one available yet.
189+
await waitTask.ConfigureAwait(false);
208190
}
209191

210192
if (quicStream == null)
211193
{
212194
throw new HttpRequestException(SR.net_http_request_aborted, null, RequestRetryType.RetryOnConnectionFailure);
213195
}
214196

215-
// 0-byte write to force QUIC to allocate a stream ID.
216-
await quicStream.WriteAsync(Array.Empty<byte>(), cancellationToken).ConfigureAwait(false);
217197
requestStream!.StreamId = quicStream.StreamId;
218198

219199
bool goAway;
@@ -246,76 +226,6 @@ public override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage req
246226
}
247227
}
248228

249-
/// <summary>
250-
/// Waits for MAX_STREAMS to be raised by the server.
251-
/// </summary>
252-
private Task WaitForAvailableRequestStreamAsync(CancellationToken cancellationToken)
253-
{
254-
TaskCompletionSourceWithCancellation<bool> tcs;
255-
256-
lock (SyncObj)
257-
{
258-
long remaining = _requestStreamsRemaining;
259-
260-
if (remaining > 0)
261-
{
262-
_requestStreamsRemaining = remaining - 1;
263-
return Task.CompletedTask;
264-
}
265-
266-
tcs = new TaskCompletionSourceWithCancellation<bool>();
267-
_waitingRequests.Enqueue(tcs);
268-
}
269-
270-
// Note: cancellation on connection shutdown is handled in CancelWaiters.
271-
return tcs.WaitWithCancellationAsync(cancellationToken).AsTask();
272-
}
273-
274-
/// <summary>
275-
/// Cancels any waiting SendAsync calls.
276-
/// </summary>
277-
/// <remarks>Requires <see cref="SyncObj"/> to be held.</remarks>
278-
private void CancelWaiters()
279-
{
280-
Debug.Assert(Monitor.IsEntered(SyncObj));
281-
282-
while (_waitingRequests.TryDequeue(out TaskCompletionSourceWithCancellation<bool>? tcs))
283-
{
284-
tcs.TrySetException(new HttpRequestException(SR.net_http_request_aborted, null, RequestRetryType.RetryOnConnectionFailure));
285-
}
286-
}
287-
288-
// TODO: how do we get this event? -> HandleEventStreamsAvailable reports currently available Uni/Bi streams
289-
private void OnMaximumStreamCountIncrease(long newMaximumStreamCount)
290-
{
291-
lock (SyncObj)
292-
{
293-
if (newMaximumStreamCount <= _maximumRequestStreams)
294-
{
295-
return;
296-
}
297-
298-
IncreaseRemainingStreamCount(newMaximumStreamCount - _maximumRequestStreams);
299-
_maximumRequestStreams = newMaximumStreamCount;
300-
}
301-
}
302-
303-
private void IncreaseRemainingStreamCount(long delta)
304-
{
305-
Debug.Assert(Monitor.IsEntered(SyncObj));
306-
Debug.Assert(delta > 0);
307-
308-
_requestStreamsRemaining += delta;
309-
310-
while (_requestStreamsRemaining != 0 && _waitingRequests.TryDequeue(out TaskCompletionSourceWithCancellation<bool>? tcs))
311-
{
312-
if (tcs.TrySetResult(true))
313-
{
314-
--_requestStreamsRemaining;
315-
}
316-
}
317-
}
318-
319229
/// <summary>
320230
/// Aborts the connection with an error.
321231
/// </summary>
@@ -358,7 +268,6 @@ internal Exception Abort(Exception abortException)
358268
_connectionClosedTask = _connection.CloseAsync((long)connectionResetErrorCode).AsTask();
359269
}
360270

361-
CancelWaiters();
362271
CheckForShutdown();
363272
}
364273

@@ -396,7 +305,6 @@ private void OnServerGoAway(long lastProcessedStreamId)
396305
}
397306
}
398307

399-
CancelWaiters();
400308
CheckForShutdown();
401309
}
402310

@@ -414,8 +322,6 @@ public void RemoveStream(QuicStream stream)
414322
bool removed = _activeRequests.Remove(stream);
415323
Debug.Assert(removed == true);
416324

417-
IncreaseRemainingStreamCount(1);
418-
419325
if (ShuttingDown)
420326
{
421327
CheckForShutdown();

0 commit comments

Comments
 (0)