Skip to content

Commit

Permalink
Add HTTP/2 keep alive pings (#22565)
Browse files Browse the repository at this point in the history
  • Loading branch information
JamesNK authored Jun 15, 2020
1 parent a0827ac commit a861d18
Show file tree
Hide file tree
Showing 8 changed files with 585 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ public Http2Limits() { }
public int HeaderTableSize { get { throw null; } set { } }
public int InitialConnectionWindowSize { get { throw null; } set { } }
public int InitialStreamWindowSize { get { throw null; } set { } }
public System.TimeSpan KeepAlivePingInterval { get { throw null; } set { } }
public System.TimeSpan KeepAlivePingTimeout { get { throw null; } set { } }
public int MaxFrameSize { get { throw null; } set { } }
public int MaxRequestHeaderFieldSize { get { throw null; } set { } }
public int MaxStreamsPerConnection { get { throw null; } set { } }
Expand Down
6 changes: 6 additions & 0 deletions src/Servers/Kestrel/Core/src/CoreStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -605,4 +605,10 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
<data name="HttpsConnectionEstablished" xml:space="preserve">
<value>Connection "{connectionId}" established using the following protocol: {protocol}</value>
</data>
<data name="Http2ErrorKeepAliveTimeout" xml:space="preserve">
<value>Timeout while waiting for incoming HTTP/2 frames after a keep alive ping.</value>
</data>
<data name="ArgumentTimeSpanGreaterOrEqual" xml:space="preserve">
<value>A TimeSpan value greater than or equal to {value} is required.</value>
</data>
</root>
54 changes: 54 additions & 0 deletions src/Servers/Kestrel/Core/src/Http2Limits.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Threading;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;

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

/// <summary>
/// Limits the number of concurrent request streams per HTTP/2 connection. Excess streams will be refused.
Expand Down Expand Up @@ -141,5 +145,55 @@ public int InitialStreamWindowSize
_initialStreamWindowSize = value;
}
}

/// <summary>
/// Gets or sets the keep alive ping interval. The server will send a keep alive ping to the client if it
/// doesn't receive any frames for this period of time. This property is used together with
/// <see cref="KeepAlivePingTimeout"/> to close broken connections.
/// <para>
/// Interval must be greater than or equal to 1 second. Set to <see cref="TimeSpan.MaxValue"/> to
/// disable the keep alive ping interval.
/// Defaults to <see cref="TimeSpan.MaxValue"/>.
/// </para>
/// </summary>
public TimeSpan KeepAlivePingInterval
{
get => _keepAlivePingInterval;
set
{
// Keep alive uses Kestrel's system clock which has a 1 second resolution. Time is greater or equal to clock resolution.
if (value < Heartbeat.Interval && value != Timeout.InfiniteTimeSpan)
{
throw new ArgumentOutOfRangeException(nameof(value), CoreStrings.FormatArgumentTimeSpanGreaterOrEqual(Heartbeat.Interval));
}

_keepAlivePingInterval = value != Timeout.InfiniteTimeSpan ? value : TimeSpan.MaxValue;
}
}

/// <summary>
/// Gets or sets the keep alive ping timeout. Keep alive pings are sent when a period of inactivity exceeds
/// the configured <see cref="KeepAlivePingInterval"/> value. The server will close the connection if it
/// doesn't receive any frames within the timeout.
/// <para>
/// Timeout must be greater than or equal to 1 second. Set to <see cref="TimeSpan.MaxValue"/> to
/// disable the keep alive ping timeout.
/// Defaults to 20 seconds.
/// </para>
/// </summary>
public TimeSpan KeepAlivePingTimeout
{
get => _keepAlivePingTimeout;
set
{
// Keep alive uses Kestrel's system clock which has a 1 second resolution. Time is greater or equal to clock resolution.
if (value < Heartbeat.Interval && value != Timeout.InfiniteTimeSpan)
{
throw new ArgumentOutOfRangeException(nameof(value), CoreStrings.FormatArgumentTimeSpanGreaterOrEqual(Heartbeat.Interval));
}

_keepAlivePingTimeout = value != Timeout.InfiniteTimeSpan ? value : TimeSpan.MaxValue;
}
}
}
}
28 changes: 28 additions & 0 deletions src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ internal partial class Http2Connection : IHttp2StreamLifetimeHandler, IHttpHeade
private int _isClosed;

// Internal for testing
internal readonly Http2KeepAlive _keepAlive;
internal readonly Dictionary<int, Http2Stream> _streams = new Dictionary<int, Http2Stream>();
internal Http2StreamStack StreamPool;

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

if (http2Limits.KeepAlivePingInterval != TimeSpan.MaxValue)
{
_keepAlive = new Http2KeepAlive(
http2Limits.KeepAlivePingInterval,
http2Limits.KeepAlivePingTimeout,
context.ServiceContext.SystemClock);
}

_serverSettings.MaxConcurrentStreams = (uint)http2Limits.MaxStreamsPerConnection;
_serverSettings.MaxFrameSize = (uint)http2Limits.MaxFrameSize;
_serverSettings.HeaderTableSize = (uint)http2Limits.HeaderTableSize;
Expand Down Expand Up @@ -211,8 +220,10 @@ public async Task ProcessRequestsAsync<TContext>(IHttpApplication<TContext> appl

try
{
bool frameReceived = false;
while (Http2FrameReader.TryReadFrame(ref buffer, _incomingFrame, _serverSettings.MaxFrameSize, out var framePayload))
{
frameReceived = true;
Log.Http2FrameReceived(ConnectionId, _incomingFrame);
await ProcessFrameAsync(application, framePayload);
}
Expand All @@ -221,6 +232,23 @@ public async Task ProcessRequestsAsync<TContext>(IHttpApplication<TContext> appl
{
return;
}

if (_keepAlive != null)
{
// Note that the keep alive uses a complete frame being received to reset state.
// Some other keep alive implementations use any bytes being received to reset state.
var state = _keepAlive.ProcessKeepAlive(frameReceived);
if (state == KeepAliveState.SendPing)
{
await _frameWriter.WritePingAsync(Http2PingFrameFlags.NONE, Http2KeepAlive.PingPayload);
}
else if (state == KeepAliveState.Timeout)
{
// There isn't a good error code to return with the GOAWAY.
// NO_ERROR isn't a good choice because it indicates the connection is gracefully shutting down.
throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorKeepAliveTimeout, Http2ErrorCode.INTERNAL_ERROR);
}
}
}
catch (Http2StreamErrorException ex)
{
Expand Down
91 changes: 91 additions & 0 deletions src/Servers/Kestrel/Core/src/Internal/Http2/Http2KeepAlive.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Buffers;
using System.Threading;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;

namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
{
internal enum KeepAliveState
{
None,
SendPing,
PingSent,
Timeout
}

internal class Http2KeepAlive
{
// An empty ping payload
internal static readonly ReadOnlySequence<byte> PingPayload = new ReadOnlySequence<byte>(new byte[8]);

private readonly TimeSpan _keepAliveInterval;
private readonly TimeSpan _keepAliveTimeout;
private readonly ISystemClock _systemClock;
private long _lastFrameReceivedTimestamp;
private long _pingSentTimestamp;

// Internal for testing
internal KeepAliveState _state;

public Http2KeepAlive(TimeSpan keepAliveInterval, TimeSpan keepAliveTimeout, ISystemClock systemClock)
{
_keepAliveInterval = keepAliveInterval;
_keepAliveTimeout = keepAliveTimeout;
_systemClock = systemClock;
}

public KeepAliveState ProcessKeepAlive(bool frameReceived)
{
var timestamp = _systemClock.UtcNowTicks;

if (frameReceived)
{
// System clock only has 1 second of precision, so the clock could be up to 1 second in the past.
// To err on the side of caution, add a second to the clock when calculating the ping sent time.
_lastFrameReceivedTimestamp = timestamp + TimeSpan.TicksPerSecond;

// Any frame received after the keep alive interval is exceeded resets the state back to none.
if (_state == KeepAliveState.PingSent)
{
_pingSentTimestamp = 0;
_state = KeepAliveState.None;
}
}
else
{
switch (_state)
{
case KeepAliveState.None:
// Check whether keep alive interval has passed since last frame received
if (timestamp > (_lastFrameReceivedTimestamp + _keepAliveInterval.Ticks))
{
// Ping will be sent immeditely after this method finishes.
// Set the status directly to ping sent and set the timestamp
_state = KeepAliveState.PingSent;
// System clock only has 1 second of precision, so the clock could be up to 1 second in the past.
// To err on the side of caution, add a second to the clock when calculating the ping sent time.
_pingSentTimestamp = _systemClock.UtcNowTicks + TimeSpan.TicksPerSecond;

// Indicate that the ping needs to be sent. This is only returned once
return KeepAliveState.SendPing;
}
break;
case KeepAliveState.PingSent:
if (_keepAliveTimeout != TimeSpan.MaxValue)
{
if (timestamp > (_pingSentTimestamp + _keepAliveTimeout.Ticks))
{
_state = KeepAliveState.Timeout;
}
}
break;
}
}

return _state;
}
}
}
2 changes: 1 addition & 1 deletion src/Servers/Kestrel/shared/test/TestConstants.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
Expand Down
Loading

0 comments on commit a861d18

Please sign in to comment.