Skip to content

Commit df76007

Browse files
committed
HTTP/3: Pool QuicStreamContext instances
1 parent 5753baf commit df76007

13 files changed

+877
-62
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
6+
namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Internal
7+
{
8+
/// <summary>
9+
/// Abstracts the system clock to facilitate testing.
10+
/// </summary>
11+
internal interface ISystemClock
12+
{
13+
/// <summary>
14+
/// Retrieves the current system time in UTC.
15+
/// </summary>
16+
DateTimeOffset UtcNow { get; }
17+
}
18+
19+
internal class SystemClock : ISystemClock
20+
{
21+
public DateTimeOffset UtcNow => DateTimeOffset.UtcNow;
22+
}
23+
}

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

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5+
using System.Diagnostics;
56
using System.Net.Quic;
67
using System.Threading;
78
using System.Threading.Tasks;
@@ -14,6 +15,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Internal
1415
{
1516
internal class QuicConnectionContext : TransportMultiplexedConnection, IProtocolErrorCodeFeature
1617
{
18+
// Internal for testing.
19+
internal QuicStreamStack StreamPool;
20+
21+
private bool _streamPoolHeartbeatInitialized;
22+
// Ticks updated once per-second in heartbeat event.
23+
private long _heartbeatTicks;
24+
private readonly object _poolLock = new object();
25+
1726
private readonly QuicConnection _connection;
1827
private readonly QuicTransportContext _context;
1928
private readonly IQuicTrace _log;
@@ -23,6 +32,10 @@ internal class QuicConnectionContext : TransportMultiplexedConnection, IProtocol
2332

2433
public long Error { get; set; }
2534

35+
internal const int InitialStreamPoolSize = 5;
36+
internal const int MaxStreamPoolSize = 100;
37+
internal const long StreamPoolExpiryTicks = TimeSpan.TicksPerSecond * 5;
38+
2639
public QuicConnectionContext(QuicConnection connection, QuicTransportContext context)
2740
{
2841
_log = context.Log;
@@ -31,6 +44,8 @@ public QuicConnectionContext(QuicConnection connection, QuicTransportContext con
3144
ConnectionClosed = _connectionClosedTokenSource.Token;
3245
Features.Set<ITlsConnectionFeature>(new FakeTlsConnectionFeature());
3346
Features.Set<IProtocolErrorCodeFeature>(this);
47+
48+
StreamPool = new QuicStreamStack(InitialStreamPoolSize);
3449
}
3550

3651
public override async ValueTask DisposeAsync()
@@ -62,7 +77,25 @@ public override void Abort(ConnectionAbortedException abortReason)
6277
try
6378
{
6479
var stream = await _connection.AcceptStreamAsync(cancellationToken);
65-
var context = new QuicStreamContext(stream, this, _context);
80+
81+
QuicStreamContext? context = null;
82+
83+
// Only use pool for bidirectional streams. Just a handful of unidirecitonal
84+
// streams are created for a connection and they live for the lifetime of the connection.
85+
if (stream.CanRead && stream.CanWrite)
86+
{
87+
lock (_poolLock)
88+
{
89+
StreamPool.TryPop(out context);
90+
}
91+
}
92+
93+
if (context == null)
94+
{
95+
context = new QuicStreamContext(this, _context);
96+
}
97+
98+
context.Initialize(stream);
6699
context.Start();
67100

68101
_log.AcceptedStream(context);
@@ -124,12 +157,61 @@ public override ValueTask<ConnectionContext> ConnectAsync(IFeatureCollection? fe
124157
quicStream = _connection.OpenBidirectionalStream();
125158
}
126159

127-
var context = new QuicStreamContext(quicStream, this, _context);
160+
// Only a handful of control streams are created by the server and they last for the
161+
// lifetime of the connection. No value in pooling them.
162+
QuicStreamContext? context = new QuicStreamContext(this, _context);
163+
context.Initialize(quicStream);
128164
context.Start();
129165

130166
_log.ConnectedStream(context);
131167

132168
return new ValueTask<ConnectionContext>(context);
133169
}
170+
171+
internal bool TryReturnStream(QuicStreamContext stream)
172+
{
173+
lock (_poolLock)
174+
{
175+
if (!_streamPoolHeartbeatInitialized)
176+
{
177+
// Heartbeat feature is added to connection features by Kestrel.
178+
// No event is on the context is raised between feature being added and serving
179+
// connections so initialize heartbeat the first time a stream is added to
180+
// the connection's stream pool.
181+
var heartbeatFeature = Features.Get<IConnectionHeartbeatFeature>();
182+
if (heartbeatFeature != null)
183+
{
184+
heartbeatFeature.OnHeartbeat(static state => ((QuicConnectionContext)state).RemoveExpiredStreams(), this);
185+
}
186+
187+
// Set ticks for the first time. Ticks are then updated in heartbeat.
188+
var now = _context.Options.SystemClock.UtcNow.Ticks;
189+
Volatile.Write(ref _heartbeatTicks, now);
190+
191+
_streamPoolHeartbeatInitialized = true;
192+
}
193+
194+
if (stream.CanReuse && StreamPool.Count < MaxStreamPoolSize)
195+
{
196+
stream.PoolExpirationTicks = Volatile.Read(ref _heartbeatTicks) + StreamPoolExpiryTicks;
197+
StreamPool.Push(stream);
198+
return true;
199+
}
200+
}
201+
202+
return false;
203+
}
204+
205+
private void RemoveExpiredStreams()
206+
{
207+
lock (_poolLock)
208+
{
209+
// Update ticks on heartbeat. A precise value isn't necessary.
210+
var now = _context.Options.SystemClock.UtcNow.Ticks;
211+
Volatile.Write(ref _heartbeatTicks, now);
212+
213+
StreamPool.RemoveExpired(now);
214+
}
215+
}
134216
}
135217
}

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

Lines changed: 2 additions & 0 deletions
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

0 commit comments

Comments
 (0)