Skip to content

Commit 7a39bf3

Browse files
committed
Support request and response rate timeouts with HTTP/3
1 parent 746aeaf commit 7a39bf3

File tree

9 files changed

+938
-29
lines changed

9 files changed

+938
-29
lines changed

src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ internal class Http3Connection : ITimeoutHandler
2929
private readonly MultiplexedConnectionContext _multiplexedContext;
3030
private readonly Http3ConnectionContext _context;
3131
private readonly ISystemClock _systemClock;
32-
private readonly TimeoutControl _timeoutControl;
3332
private bool _aborted;
3433
private readonly object _protocolSelectionLock = new object();
3534
private int _gracefulCloseInitiator;
@@ -46,8 +45,16 @@ public Http3Connection(Http3ConnectionContext context)
4645
_multiplexedContext = context.ConnectionContext;
4746
_context = context;
4847
_systemClock = context.ServiceContext.SystemClock;
49-
_timeoutControl = new TimeoutControl(this);
50-
_context.TimeoutControl ??= _timeoutControl;
48+
49+
if (_context.TimeoutControl == null)
50+
{
51+
var timeoutControl = new TimeoutControl(this);
52+
53+
// Ensure TimeoutControl._lastTimestamp is initialized before anything that could set timeouts runs.
54+
timeoutControl.Initialize(_systemClock.UtcNowTicks);
55+
56+
_context.TimeoutControl = timeoutControl;
57+
}
5158

5259
_errorCodeFeature = context.ConnectionFeatures.Get<IProtocolErrorCodeFeature>()!;
5360

@@ -72,6 +79,7 @@ internal long HighestStreamId
7279
}
7380
}
7481

82+
public ITimeoutControl TimeoutControl => _context.TimeoutControl!;
7583
private IKestrelTrace Log => _context.ServiceContext.Log;
7684
public KestrelServerLimits Limits => _context.ServiceContext.ServerOptions.Limits;
7785
public Http3ControlStream? OutboundControlStream { get; set; }
@@ -84,9 +92,6 @@ public async Task ProcessStreamsAsync<TContext>(IHttpApplication<TContext> httpA
8492
{
8593
try
8694
{
87-
// Ensure TimeoutControl._lastTimestamp is initialized before anything that could set timeouts runs.
88-
_timeoutControl.Initialize(_systemClock.UtcNowTicks);
89-
9095
var connectionHeartbeatFeature = _context.ConnectionFeatures.Get<IConnectionHeartbeatFeature>();
9196
var connectionLifetimeNotificationFeature = _context.ConnectionFeatures.Get<IConnectionLifetimeNotificationFeature>();
9297

@@ -199,7 +204,7 @@ public void Tick()
199204

200205
// It's safe to use UtcNowUnsynchronized since Tick is called by the Heartbeat.
201206
var now = _systemClock.UtcNowUnsynchronized;
202-
_timeoutControl.Tick(now);
207+
TimeoutControl.Tick(now);
203208

204209
// TODO cancel process stream loop to update logic.
205210
}
@@ -219,6 +224,8 @@ public void OnTimeout(TimeoutReason reason)
219224
SendGoAway(_highestOpenedStreamId);
220225
break;
221226
case TimeoutReason.RequestHeaders:
227+
SendGoAway(_highestOpenedStreamId);
228+
break;
222229
case TimeoutReason.ReadDataRate:
223230
case TimeoutReason.WriteDataRate:
224231
case TimeoutReason.RequestBodyDrain:
@@ -244,7 +251,7 @@ internal async Task InnerProcessStreamsAsync<TContext>(IHttpApplication<TContext
244251
// TODO should we await the control stream task?
245252
var controlTask = CreateControlStream(application);
246253

247-
_timeoutControl.SetTimeout(Limits.KeepAliveTimeout.Ticks, TimeoutReason.KeepAlive);
254+
//TimeoutControl.SetTimeout(Limits.KeepAliveTimeout.Ticks, TimeoutReason.KeepAlive);
248255

249256
try
250257
{
@@ -278,7 +285,7 @@ internal async Task InnerProcessStreamsAsync<TContext>(IHttpApplication<TContext
278285
streamContext.Transport,
279286
streamContext,
280287
_serverSettings);
281-
httpConnectionContext.TimeoutControl = _context.TimeoutControl;
288+
httpConnectionContext.TimeoutControl = _context.TimeoutControl!;
282289

283290
if (!quicStreamFeature.CanWrite)
284291
{
@@ -288,6 +295,7 @@ internal async Task InnerProcessStreamsAsync<TContext>(IHttpApplication<TContext
288295
}
289296
else
290297
{
298+
// Request stream
291299
var streamId = streamIdFeature.StreamId;
292300

293301
HighestStreamId = streamId;
@@ -299,6 +307,7 @@ internal async Task InnerProcessStreamsAsync<TContext>(IHttpApplication<TContext
299307
_activeRequestCount++;
300308
_streams[streamId] = http3Stream;
301309
}
310+
302311
KestrelEventSource.Log.RequestQueuedStart(stream, AspNetCore.Http.HttpProtocol.Http3);
303312
ThreadPool.UnsafeQueueUserWorkItem(stream, preferLocal: false);
304313
}
@@ -363,7 +372,7 @@ internal async Task InnerProcessStreamsAsync<TContext>(IHttpApplication<TContext
363372
await _streamCompletionAwaitable;
364373
}
365374

366-
_timeoutControl.CancelTimeout();
375+
//TimeoutControl.CancelTimeout();
367376
}
368377
catch
369378
{
@@ -411,14 +420,14 @@ private void UpdateConnectionState()
411420
}
412421
else
413422
{
414-
// TODO should keep-alive timeout be a thing for HTTP/3? MsQuic currently tracks this for us?
415-
if (_timeoutControl.TimerReason == TimeoutReason.None)
416-
{
417-
_timeoutControl.SetTimeout(Limits.KeepAliveTimeout.Ticks, TimeoutReason.KeepAlive);
418-
}
419-
420-
// Only reason should be keep-alive.
421-
Debug.Assert(_timeoutControl.TimerReason == TimeoutReason.KeepAlive);
423+
//// TODO should keep-alive timeout be a thing for HTTP/3? MsQuic currently tracks this for us?
424+
//if (TimeoutControl.TimerReason == TimeoutReason.None)
425+
//{
426+
// TimeoutControl.SetTimeout(Limits.KeepAliveTimeout.Ticks, TimeoutReason.KeepAlive);
427+
//}
428+
429+
//// Only reason should be keep-alive.
430+
//Debug.Assert(TimeoutControl.TimerReason == TimeoutReason.KeepAlive);
422431
}
423432
}
424433
}
@@ -451,7 +460,7 @@ private async ValueTask<Http3ControlStream> CreateNewUnidirectionalStreamAsync<T
451460
streamContext.Transport,
452461
streamContext,
453462
_serverSettings);
454-
httpConnectionContext.TimeoutControl = _context.TimeoutControl;
463+
httpConnectionContext.TimeoutControl = _context.TimeoutControl!;
455464

456465
return new Http3ControlStream<TContext>(application, this, httpConnectionContext);
457466
}

src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,12 @@ private bool TryClose()
344344

345345
public async Task ProcessRequestAsync<TContext>(IHttpApplication<TContext> application) where TContext : notnull
346346
{
347+
// Start request header timeout. Reset when headers are received by a request stream.
348+
if (TimeoutControl.TimerReason == TimeoutReason.None)
349+
{
350+
TimeoutControl.SetTimeout(Limits.RequestHeadersTimeout.Ticks, TimeoutReason.RequestHeaders);
351+
}
352+
347353
Exception? error = null;
348354

349355
try
@@ -512,6 +518,11 @@ private Task ProcessHeadersFrameAsync<TContext>(IHttpApplication<TContext> appli
512518
break;
513519
}
514520

521+
if (TimeoutControl.TimerReason == TimeoutReason.RequestHeaders)
522+
{
523+
TimeoutControl.CancelTimeout();
524+
}
525+
515526
InputRemaining = HttpRequestHeaders.ContentLength;
516527

517528
_appCompleted = new TaskCompletionSource();

src/Servers/Kestrel/Core/src/Internal/Http3ConnectionContext.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
using System.Buffers;
55
using System.Net;
6-
using Microsoft.AspNetCore.Connections;
76
using Microsoft.AspNetCore.Connections.Experimental;
87
using Microsoft.AspNetCore.Http.Features;
98
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
@@ -38,6 +37,9 @@ public Http3ConnectionContext(
3837
public IPEndPoint? LocalEndPoint { get; }
3938
public IPEndPoint? RemoteEndPoint { get; }
4039

41-
public ITimeoutControl TimeoutControl { get; set; } = default!; // Always set by HttpConnection
40+
/// <summary>
41+
/// Will be be set by unit tests or created by Http3Connection ctor.
42+
/// </summary>
43+
public ITimeoutControl? TimeoutControl { get; set; }
4244
}
4345
}

src/Servers/Kestrel/Core/src/Internal/HttpConnection.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ public async Task ProcessRequestsAsync<TContext>(IHttpApplication<TContext> http
7474

7575
default:
7676
// SelectProtocol() only returns Http1, Http2 or None.
77-
throw new NotSupportedException($"{nameof(SelectProtocol)} returned something other than Http1, Http2, Http3 or None.");
77+
throw new NotSupportedException($"{nameof(SelectProtocol)} returned something other than Http1, Http2 or None.");
7878
}
7979

8080
_requestProcessor = requestProcessor;

src/Servers/Kestrel/Core/src/Internal/Infrastructure/ITimeoutControl.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4+
using System;
45
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.FlowControl;
56

67
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
@@ -14,6 +15,8 @@ internal interface ITimeoutControl
1415
void CancelTimeout();
1516

1617
void InitializeHttp2(InputFlowControl connectionInputFlowControl);
18+
void Tick(DateTimeOffset now);
19+
1720
void StartRequestBody(MinDataRate minRate);
1821
void StopRequestBody();
1922
void StartTimingRead();

src/Servers/Kestrel/perf/Microbenchmarks/Mocks/MockTimeoutControl.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4+
using System;
45
using Microsoft.AspNetCore.Server.Kestrel.Core;
56
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.FlowControl;
67
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
@@ -58,5 +59,9 @@ public void StopTimingRead()
5859
public void StopTimingWrite()
5960
{
6061
}
62+
63+
public void Tick(DateTimeOffset now)
64+
{
65+
}
6166
}
6267
}

src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -432,8 +432,6 @@ void IHttpHeadersHandler.OnHeadersComplete(bool endStream) { }
432432

433433
protected void CreateConnection()
434434
{
435-
var limits = _serviceContext.ServerOptions.Limits;
436-
437435
// Always dispatch test code back to the ThreadPool. This prevents deadlocks caused by continuing
438436
// Http2Connection.ProcessRequestsAsync() loop with writer locks acquired. Run product code inline to make
439437
// it easier to verify request frames are processed correctly immediately after sending the them.
@@ -1381,6 +1379,11 @@ public virtual void BytesWrittenToBuffer(MinDataRate minRate, long size)
13811379
{
13821380
_realTimeoutControl.BytesWrittenToBuffer(minRate, size);
13831381
}
1382+
1383+
public virtual void Tick(DateTimeOffset now)
1384+
{
1385+
_realTimeoutControl.Tick(now);
1386+
}
13841387
}
13851388
}
13861389
}

src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TestBase.cs

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434

3535
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
3636
{
37-
public class Http3TestBase : TestApplicationErrorLoggerLoggedTest, IDisposable
37+
public abstract class Http3TestBase : TestApplicationErrorLoggerLoggedTest, IDisposable
3838
{
3939
internal TestServiceContext _serviceContext;
4040
internal readonly TimeoutControl _timeoutControl;
@@ -134,7 +134,7 @@ internal async ValueTask<Http3ControlStream> GetInboundControlStream()
134134
}
135135
}
136136

137-
return null;
137+
return _inboundControlStream;
138138
}
139139

140140
internal async Task WaitForConnectionErrorAsync<TException>(bool ignoreNonGoAwayFrames, long expectedLastStreamId, Http3ErrorCode expectedErrorCode, params string[] expectedErrorMessage)
@@ -170,6 +170,21 @@ internal void VerifyGoAway(Http3FrameWithPayload frame, long expectedLastStreamI
170170
Assert.Equal(expectedLastStreamId, streamId);
171171
}
172172

173+
protected void AdvanceClock(TimeSpan timeSpan)
174+
{
175+
var clock = _serviceContext.MockSystemClock;
176+
var endTime = clock.UtcNow + timeSpan;
177+
178+
while (clock.UtcNow + Heartbeat.Interval < endTime)
179+
{
180+
clock.UtcNow += Heartbeat.Interval;
181+
_timeoutControl.Tick(clock.UtcNow);
182+
}
183+
184+
clock.UtcNow = endTime;
185+
_timeoutControl.Tick(clock.UtcNow);
186+
}
187+
173188
protected async Task InitializeConnectionAsync(RequestDelegate application)
174189
{
175190
if (Connection == null)
@@ -196,9 +211,6 @@ internal async ValueTask<Http3RequestStream> InitializeConnectionAndStreamsAsync
196211

197212
protected void CreateConnection()
198213
{
199-
var limits = _serviceContext.ServerOptions.Limits;
200-
201-
202214
MultiplexedConnectionContext = new TestMultiplexedConnectionContext(this);
203215

204216
var httpConnectionContext = new Http3ConnectionContext(
@@ -399,6 +411,17 @@ public async Task<bool> SendHeadersAsync(IEnumerable<KeyValuePair<string, string
399411
return done;
400412
}
401413

414+
internal async Task SendHeadersPartialAsync()
415+
{
416+
// Send HEADERS frame header without content.
417+
var outputWriter = _pair.Application.Output;
418+
var frame = new Http3RawFrame();
419+
frame.PrepareData();
420+
frame.Length = 10;
421+
Http3FrameWriter.WriteHeader(frame, outputWriter);
422+
await SendAsync(Span<byte>.Empty);
423+
}
424+
402425
internal async Task SendDataAsync(Memory<byte> data, bool endStream = false)
403426
{
404427
var frame = new Http3RawFrame();
@@ -501,6 +524,12 @@ public Http3ControlStream(ConnectionContext streamContext)
501524
StreamContext = streamContext;
502525
}
503526

527+
internal async Task ExpectSettingsAsync()
528+
{
529+
var http3WithPayload = await ReceiveFrameAsync();
530+
Assert.Equal(Http3FrameType.Settings, http3WithPayload.Type);
531+
}
532+
504533
public async Task WriteStreamIdAsync(int id)
505534
{
506535
var writableBuffer = _pair.Application.Output;

0 commit comments

Comments
 (0)