Skip to content

Commit a861d18

Browse files
authored
Add HTTP/2 keep alive pings (#22565)
1 parent a0827ac commit a861d18

File tree

8 files changed

+585
-6
lines changed

8 files changed

+585
-6
lines changed

src/Servers/Kestrel/Core/ref/Microsoft.AspNetCore.Server.Kestrel.Core.netcoreapp.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ public Http2Limits() { }
7474
public int HeaderTableSize { get { throw null; } set { } }
7575
public int InitialConnectionWindowSize { get { throw null; } set { } }
7676
public int InitialStreamWindowSize { get { throw null; } set { } }
77+
public System.TimeSpan KeepAlivePingInterval { get { throw null; } set { } }
78+
public System.TimeSpan KeepAlivePingTimeout { get { throw null; } set { } }
7779
public int MaxFrameSize { get { throw null; } set { } }
7880
public int MaxRequestHeaderFieldSize { get { throw null; } set { } }
7981
public int MaxStreamsPerConnection { get { throw null; } set { } }

src/Servers/Kestrel/Core/src/CoreStrings.resx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -605,4 +605,10 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
605605
<data name="HttpsConnectionEstablished" xml:space="preserve">
606606
<value>Connection "{connectionId}" established using the following protocol: {protocol}</value>
607607
</data>
608+
<data name="Http2ErrorKeepAliveTimeout" xml:space="preserve">
609+
<value>Timeout while waiting for incoming HTTP/2 frames after a keep alive ping.</value>
610+
</data>
611+
<data name="ArgumentTimeSpanGreaterOrEqual" xml:space="preserve">
612+
<value>A TimeSpan value greater than or equal to {value} is required.</value>
613+
</data>
608614
</root>

src/Servers/Kestrel/Core/src/Http2Limits.cs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
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.Threading;
56
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2;
7+
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
68

79
namespace Microsoft.AspNetCore.Server.Kestrel.Core
810
{
@@ -17,6 +19,8 @@ public class Http2Limits
1719
private int _maxRequestHeaderFieldSize = (int)Http2PeerSettings.DefaultMaxFrameSize;
1820
private int _initialConnectionWindowSize = 1024 * 128; // Larger than the default 64kb, and larger than any one single stream.
1921
private int _initialStreamWindowSize = 1024 * 96; // Larger than the default 64kb
22+
private TimeSpan _keepAlivePingInterval = TimeSpan.MaxValue;
23+
private TimeSpan _keepAlivePingTimeout = TimeSpan.FromSeconds(20);
2024

2125
/// <summary>
2226
/// Limits the number of concurrent request streams per HTTP/2 connection. Excess streams will be refused.
@@ -141,5 +145,55 @@ public int InitialStreamWindowSize
141145
_initialStreamWindowSize = value;
142146
}
143147
}
148+
149+
/// <summary>
150+
/// Gets or sets the keep alive ping interval. The server will send a keep alive ping to the client if it
151+
/// doesn't receive any frames for this period of time. This property is used together with
152+
/// <see cref="KeepAlivePingTimeout"/> to close broken connections.
153+
/// <para>
154+
/// Interval must be greater than or equal to 1 second. Set to <see cref="TimeSpan.MaxValue"/> to
155+
/// disable the keep alive ping interval.
156+
/// Defaults to <see cref="TimeSpan.MaxValue"/>.
157+
/// </para>
158+
/// </summary>
159+
public TimeSpan KeepAlivePingInterval
160+
{
161+
get => _keepAlivePingInterval;
162+
set
163+
{
164+
// Keep alive uses Kestrel's system clock which has a 1 second resolution. Time is greater or equal to clock resolution.
165+
if (value < Heartbeat.Interval && value != Timeout.InfiniteTimeSpan)
166+
{
167+
throw new ArgumentOutOfRangeException(nameof(value), CoreStrings.FormatArgumentTimeSpanGreaterOrEqual(Heartbeat.Interval));
168+
}
169+
170+
_keepAlivePingInterval = value != Timeout.InfiniteTimeSpan ? value : TimeSpan.MaxValue;
171+
}
172+
}
173+
174+
/// <summary>
175+
/// Gets or sets the keep alive ping timeout. Keep alive pings are sent when a period of inactivity exceeds
176+
/// the configured <see cref="KeepAlivePingInterval"/> value. The server will close the connection if it
177+
/// doesn't receive any frames within the timeout.
178+
/// <para>
179+
/// Timeout must be greater than or equal to 1 second. Set to <see cref="TimeSpan.MaxValue"/> to
180+
/// disable the keep alive ping timeout.
181+
/// Defaults to 20 seconds.
182+
/// </para>
183+
/// </summary>
184+
public TimeSpan KeepAlivePingTimeout
185+
{
186+
get => _keepAlivePingTimeout;
187+
set
188+
{
189+
// Keep alive uses Kestrel's system clock which has a 1 second resolution. Time is greater or equal to clock resolution.
190+
if (value < Heartbeat.Interval && value != Timeout.InfiniteTimeSpan)
191+
{
192+
throw new ArgumentOutOfRangeException(nameof(value), CoreStrings.FormatArgumentTimeSpanGreaterOrEqual(Heartbeat.Interval));
193+
}
194+
195+
_keepAlivePingTimeout = value != Timeout.InfiniteTimeSpan ? value : TimeSpan.MaxValue;
196+
}
197+
}
144198
}
145199
}

src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ internal partial class Http2Connection : IHttp2StreamLifetimeHandler, IHttpHeade
6666
private int _isClosed;
6767

6868
// Internal for testing
69+
internal readonly Http2KeepAlive _keepAlive;
6970
internal readonly Dictionary<int, Http2Stream> _streams = new Dictionary<int, Http2Stream>();
7071
internal Http2StreamStack StreamPool;
7172

@@ -106,6 +107,14 @@ public Http2Connection(HttpConnectionContext context)
106107
var connectionWindow = (uint)http2Limits.InitialConnectionWindowSize;
107108
_inputFlowControl = new InputFlowControl(connectionWindow, connectionWindow / 2);
108109

110+
if (http2Limits.KeepAlivePingInterval != TimeSpan.MaxValue)
111+
{
112+
_keepAlive = new Http2KeepAlive(
113+
http2Limits.KeepAlivePingInterval,
114+
http2Limits.KeepAlivePingTimeout,
115+
context.ServiceContext.SystemClock);
116+
}
117+
109118
_serverSettings.MaxConcurrentStreams = (uint)http2Limits.MaxStreamsPerConnection;
110119
_serverSettings.MaxFrameSize = (uint)http2Limits.MaxFrameSize;
111120
_serverSettings.HeaderTableSize = (uint)http2Limits.HeaderTableSize;
@@ -211,8 +220,10 @@ public async Task ProcessRequestsAsync<TContext>(IHttpApplication<TContext> appl
211220

212221
try
213222
{
223+
bool frameReceived = false;
214224
while (Http2FrameReader.TryReadFrame(ref buffer, _incomingFrame, _serverSettings.MaxFrameSize, out var framePayload))
215225
{
226+
frameReceived = true;
216227
Log.Http2FrameReceived(ConnectionId, _incomingFrame);
217228
await ProcessFrameAsync(application, framePayload);
218229
}
@@ -221,6 +232,23 @@ public async Task ProcessRequestsAsync<TContext>(IHttpApplication<TContext> appl
221232
{
222233
return;
223234
}
235+
236+
if (_keepAlive != null)
237+
{
238+
// Note that the keep alive uses a complete frame being received to reset state.
239+
// Some other keep alive implementations use any bytes being received to reset state.
240+
var state = _keepAlive.ProcessKeepAlive(frameReceived);
241+
if (state == KeepAliveState.SendPing)
242+
{
243+
await _frameWriter.WritePingAsync(Http2PingFrameFlags.NONE, Http2KeepAlive.PingPayload);
244+
}
245+
else if (state == KeepAliveState.Timeout)
246+
{
247+
// There isn't a good error code to return with the GOAWAY.
248+
// NO_ERROR isn't a good choice because it indicates the connection is gracefully shutting down.
249+
throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorKeepAliveTimeout, Http2ErrorCode.INTERNAL_ERROR);
250+
}
251+
}
224252
}
225253
catch (Http2StreamErrorException ex)
226254
{
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
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+
using System.Buffers;
6+
using System.Threading;
7+
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
8+
9+
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
10+
{
11+
internal enum KeepAliveState
12+
{
13+
None,
14+
SendPing,
15+
PingSent,
16+
Timeout
17+
}
18+
19+
internal class Http2KeepAlive
20+
{
21+
// An empty ping payload
22+
internal static readonly ReadOnlySequence<byte> PingPayload = new ReadOnlySequence<byte>(new byte[8]);
23+
24+
private readonly TimeSpan _keepAliveInterval;
25+
private readonly TimeSpan _keepAliveTimeout;
26+
private readonly ISystemClock _systemClock;
27+
private long _lastFrameReceivedTimestamp;
28+
private long _pingSentTimestamp;
29+
30+
// Internal for testing
31+
internal KeepAliveState _state;
32+
33+
public Http2KeepAlive(TimeSpan keepAliveInterval, TimeSpan keepAliveTimeout, ISystemClock systemClock)
34+
{
35+
_keepAliveInterval = keepAliveInterval;
36+
_keepAliveTimeout = keepAliveTimeout;
37+
_systemClock = systemClock;
38+
}
39+
40+
public KeepAliveState ProcessKeepAlive(bool frameReceived)
41+
{
42+
var timestamp = _systemClock.UtcNowTicks;
43+
44+
if (frameReceived)
45+
{
46+
// System clock only has 1 second of precision, so the clock could be up to 1 second in the past.
47+
// To err on the side of caution, add a second to the clock when calculating the ping sent time.
48+
_lastFrameReceivedTimestamp = timestamp + TimeSpan.TicksPerSecond;
49+
50+
// Any frame received after the keep alive interval is exceeded resets the state back to none.
51+
if (_state == KeepAliveState.PingSent)
52+
{
53+
_pingSentTimestamp = 0;
54+
_state = KeepAliveState.None;
55+
}
56+
}
57+
else
58+
{
59+
switch (_state)
60+
{
61+
case KeepAliveState.None:
62+
// Check whether keep alive interval has passed since last frame received
63+
if (timestamp > (_lastFrameReceivedTimestamp + _keepAliveInterval.Ticks))
64+
{
65+
// Ping will be sent immeditely after this method finishes.
66+
// Set the status directly to ping sent and set the timestamp
67+
_state = KeepAliveState.PingSent;
68+
// System clock only has 1 second of precision, so the clock could be up to 1 second in the past.
69+
// To err on the side of caution, add a second to the clock when calculating the ping sent time.
70+
_pingSentTimestamp = _systemClock.UtcNowTicks + TimeSpan.TicksPerSecond;
71+
72+
// Indicate that the ping needs to be sent. This is only returned once
73+
return KeepAliveState.SendPing;
74+
}
75+
break;
76+
case KeepAliveState.PingSent:
77+
if (_keepAliveTimeout != TimeSpan.MaxValue)
78+
{
79+
if (timestamp > (_pingSentTimestamp + _keepAliveTimeout.Ticks))
80+
{
81+
_state = KeepAliveState.Timeout;
82+
}
83+
}
84+
break;
85+
}
86+
}
87+
88+
return _state;
89+
}
90+
}
91+
}

src/Servers/Kestrel/shared/test/TestConstants.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) .NET Foundation. All rights reserved.
1+
// 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

44
using System;

0 commit comments

Comments
 (0)