Skip to content

Commit 75ef80d

Browse files
committed
Limit pool size and unit test
1 parent f92fe86 commit 75ef80d

File tree

5 files changed

+110
-13
lines changed

5 files changed

+110
-13
lines changed

src/Servers/Kestrel/Transport.Quic/src/Internal/QuicConnectionContext.cs

+9-3
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ public override ValueTask<ConnectionContext> ConnectAsync(IFeatureCollection? fe
168168
return new ValueTask<ConnectionContext>(context);
169169
}
170170

171-
internal void ReturnStream(QuicStreamContext stream)
171+
internal bool TryReturnStream(QuicStreamContext stream)
172172
{
173173
lock (_poolLock)
174174
{
@@ -191,9 +191,15 @@ internal void ReturnStream(QuicStreamContext stream)
191191
_streamPoolHeartbeatInitialized = true;
192192
}
193193

194-
stream.PoolExpirationTicks = Volatile.Read(ref _heartbeatTicks) + StreamPoolExpiryTicks;
195-
StreamPool.Push(stream);
194+
if (stream.CanReuse && StreamPool.Count < MaxStreamPoolSize)
195+
{
196+
stream.PoolExpirationTicks = Volatile.Read(ref _heartbeatTicks) + StreamPoolExpiryTicks;
197+
StreamPool.Push(stream);
198+
return true;
199+
}
196200
}
201+
202+
return false;
197203
}
198204

199205
private void RemoveExpiredStreams()

src/Servers/Kestrel/Transport.Quic/src/Internal/QuicConnectionListener.cs

+2
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ public QuicConnectionListener(QuicTransportOptions options, IQuicTrace log, EndP
4141
quicListenerOptions.ServerAuthenticationOptions = sslServerAuthenticationOptions;
4242
quicListenerOptions.ListenEndPoint = endpoint as IPEndPoint;
4343
quicListenerOptions.IdleTimeout = options.IdleTimeout;
44+
quicListenerOptions.MaxBidirectionalStreams = options.MaxBidirectionalStreamCount;
45+
quicListenerOptions.MaxUnidirectionalStreams = options.MaxUnidirectionalStreamCount;
4446

4547
_listener = new QuicListener(QuicImplementationProviders.MsQuic, quicListenerOptions);
4648

src/Servers/Kestrel/Transport.Quic/src/Internal/QuicStreamContext.cs

+5-5
Original file line numberDiff line numberDiff line change
@@ -393,12 +393,12 @@ public override async ValueTask DisposeAsync()
393393
_stream.Dispose();
394394
_stream = null!;
395395

396-
if (CanReuse)
397-
{
398-
_connection.ReturnStream(this);
399-
}
400-
else
396+
if (!_connection.TryReturnStream(this))
401397
{
398+
// Dispose when one of:
399+
// - Stream is not bidirection
400+
// - Stream didn't complete gracefully
401+
// - Pool is full
402402
DisposeCore();
403403
}
404404
}

src/Servers/Kestrel/Transport.Quic/test/QuicConnectionContextTests.cs

+89
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Net.Quic;
88
using System.Text;
99
using System.Threading.Tasks;
10+
using Microsoft.AspNetCore.Connections;
1011
using Microsoft.AspNetCore.Connections.Features;
1112
using Microsoft.AspNetCore.Server.Kestrel.FunctionalTests;
1213
using Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Internal;
@@ -223,6 +224,94 @@ public async Task StreamPool_Heartbeat_ExpiredStreamRemoved()
223224
Assert.Equal(0, quicConnectionContext.StreamPool.Count);
224225
}
225226

227+
[ConditionalFact]
228+
[MsQuicSupported]
229+
public async Task StreamPool_ManyConcurrentStreams_StreamPoolFull()
230+
{
231+
// Arrange
232+
await using var connectionListener = await QuicTestHelpers.CreateConnectionListenerFactory(LoggerFactory);
233+
234+
var options = QuicTestHelpers.CreateClientConnectionOptions(connectionListener.EndPoint);
235+
using var clientConnection = new QuicConnection(QuicImplementationProviders.MsQuic, options);
236+
await clientConnection.ConnectAsync().DefaultTimeout();
237+
238+
await using var serverConnection = await connectionListener.AcceptAsync().DefaultTimeout();
239+
240+
var testHeartbeatFeature = new TestHeartbeatFeature();
241+
serverConnection.Features.Set<IConnectionHeartbeatFeature>(testHeartbeatFeature);
242+
243+
// Act
244+
var quicConnectionContext = Assert.IsType<QuicConnectionContext>(serverConnection);
245+
Assert.Equal(0, quicConnectionContext.StreamPool.Count);
246+
247+
var pauseCompleteTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
248+
var allConnectionsOnServerTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
249+
var streamTasks = new List<Task>();
250+
var requestState = new RequestState(clientConnection, serverConnection, allConnectionsOnServerTcs, pauseCompleteTcs.Task);
251+
252+
const int StreamsSent = 101;
253+
for (var i = 0; i < StreamsSent; i++)
254+
{
255+
// TODO: Race condition in QUIC library.
256+
// Delay between sending streams to avoid
257+
// https://github.com/dotnet/runtime/issues/55249
258+
await Task.Delay(50);
259+
streamTasks.Add(SendStream(requestState));
260+
}
261+
262+
await allConnectionsOnServerTcs.Task.DefaultTimeout();
263+
pauseCompleteTcs.SetResult();
264+
265+
await Task.WhenAll(streamTasks).DefaultTimeout();
266+
267+
// Assert
268+
// Up to 100 streams are pooled.
269+
Assert.Equal(100, quicConnectionContext.StreamPool.Count);
270+
271+
static async Task SendStream(RequestState requestState)
272+
{
273+
var clientStream = requestState.QuicConnection.OpenBidirectionalStream();
274+
await clientStream.WriteAsync(TestData, endStream: true).DefaultTimeout();
275+
var serverStream = await requestState.ServerConnection.AcceptAsync().DefaultTimeout();
276+
var readResult = await serverStream.Transport.Input.ReadAtLeastAsync(TestData.Length).DefaultTimeout();
277+
serverStream.Transport.Input.AdvanceTo(readResult.Buffer.End);
278+
279+
// Input should be completed.
280+
readResult = await serverStream.Transport.Input.ReadAsync();
281+
Assert.True(readResult.IsCompleted);
282+
283+
lock (requestState)
284+
{
285+
requestState.ActiveConcurrentConnections++;
286+
if (requestState.ActiveConcurrentConnections == StreamsSent)
287+
{
288+
requestState.AllConnectionsOnServerTcs.SetResult();
289+
}
290+
}
291+
292+
await requestState.PauseCompleteTask;
293+
294+
// Complete reading and writing.
295+
await serverStream.Transport.Input.CompleteAsync();
296+
await serverStream.Transport.Output.CompleteAsync();
297+
298+
var quicStreamContext = Assert.IsType<QuicStreamContext>(serverStream);
299+
300+
// Both send and receive loops have exited.
301+
await quicStreamContext._processingTask.DefaultTimeout();
302+
await quicStreamContext.DisposeAsync();
303+
}
304+
}
305+
306+
private record RequestState(
307+
QuicConnection QuicConnection,
308+
MultiplexedConnectionContext ServerConnection,
309+
TaskCompletionSource AllConnectionsOnServerTcs,
310+
Task PauseCompleteTask)
311+
{
312+
public int ActiveConcurrentConnections { get; set; }
313+
};
314+
226315
private class TestSystemClock : ISystemClock
227316
{
228317
public DateTimeOffset UtcNow { get; set; }

src/Servers/Kestrel/Transport.Quic/test/QuicTestHelpers.cs

+5-5
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ public static QuicTransportFactory CreateTransportFactory(ILoggerFactory loggerF
3232
var quicTransportOptions = new QuicTransportOptions();
3333
quicTransportOptions.Alpn = Alpn;
3434
quicTransportOptions.IdleTimeout = TimeSpan.FromMinutes(1);
35+
quicTransportOptions.MaxBidirectionalStreamCount = 200;
36+
quicTransportOptions.MaxUnidirectionalStreamCount = 200;
3537
if (systemClock != null)
3638
{
3739
quicTransportOptions.SystemClock = systemClock;
@@ -74,8 +76,8 @@ public static QuicClientConnectionOptions CreateClientConnectionOptions(EndPoint
7476
{
7577
return new QuicClientConnectionOptions
7678
{
77-
MaxBidirectionalStreams = 10,
78-
MaxUnidirectionalStreams = 20,
79+
MaxBidirectionalStreams = 200,
80+
MaxUnidirectionalStreams = 200,
7981
RemoteEndPoint = remoteEndPoint,
8082
ClientAuthenticationOptions = new SslClientAuthenticationOptions
8183
{
@@ -98,14 +100,12 @@ public static async Task<QuicStreamContext> CreateAndCompleteBidirectionalStream
98100

99101
// Input should be completed.
100102
readResult = await serverStream.Transport.Input.ReadAsync();
103+
Assert.True(readResult.IsCompleted);
101104

102105
// Complete reading and writing.
103106
await serverStream.Transport.Input.CompleteAsync();
104107
await serverStream.Transport.Output.CompleteAsync();
105108

106-
// Assert
107-
Assert.True(readResult.IsCompleted);
108-
109109
var quicStreamContext = Assert.IsType<QuicStreamContext>(serverStream);
110110

111111
// Both send and receive loops have exited.

0 commit comments

Comments
 (0)