Add HTTP/2 keep alive pings (#22565)
This commit is contained in:
parent
a0827ace5e
commit
a861d18d24
|
|
@ -74,6 +74,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
|
|||
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 { } }
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
{
|
||||
|
|
@ -17,6 +19,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
|
|||
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.
|
||||
|
|
@ -141,5 +145,55 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
|
|||
_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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
|||
private int _isClosed;
|
||||
|
||||
// Internal for testing
|
||||
internal readonly Http2KeepAlive _keepAlive;
|
||||
internal readonly Dictionary<int, Http2Stream> _streams = new Dictionary<int, Http2Stream>();
|
||||
internal Http2StreamStack StreamPool;
|
||||
|
||||
|
|
@ -106,6 +107,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
|||
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;
|
||||
|
|
@ -211,8 +220,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
|||
|
||||
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);
|
||||
}
|
||||
|
|
@ -221,6 +232,23 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
|||
{
|
||||
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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,389 @@
|
|||
// 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.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||
{
|
||||
public class Http2KeepAliveTests : Http2TestBase
|
||||
{
|
||||
[Fact]
|
||||
public async Task KeepAlivePingInterval_InfiniteTimeSpan_KeepAliveNotEnabled()
|
||||
{
|
||||
_serviceContext.ServerOptions.Limits.Http2.KeepAlivePingInterval = Timeout.InfiniteTimeSpan;
|
||||
|
||||
await InitializeConnectionAsync(_noopApplication).DefaultTimeout();
|
||||
|
||||
Assert.Null(_connection._keepAlive);
|
||||
|
||||
await StopConnectionAsync(expectedLastStreamId: 0, ignoreNonGoAwayFrames: false).DefaultTimeout();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task KeepAlivePingTimeout_InfiniteTimeSpan_NoGoAway()
|
||||
{
|
||||
_serviceContext.ServerOptions.Limits.Http2.KeepAlivePingInterval = TimeSpan.FromSeconds(1);
|
||||
_serviceContext.ServerOptions.Limits.Http2.KeepAlivePingTimeout = Timeout.InfiniteTimeSpan;
|
||||
|
||||
await InitializeConnectionAsync(_noopApplication).DefaultTimeout();
|
||||
|
||||
DateTimeOffset now = _serviceContext.MockSystemClock.UtcNow;
|
||||
|
||||
// Heartbeat
|
||||
TriggerTick(now);
|
||||
|
||||
// Heartbeat that exceeds interval
|
||||
TriggerTick(now + TimeSpan.FromSeconds(1.1 * 2));
|
||||
|
||||
await ExpectAsync(Http2FrameType.PING,
|
||||
withLength: 8,
|
||||
withFlags: (byte)Http2PingFrameFlags.NONE,
|
||||
withStreamId: 0).DefaultTimeout();
|
||||
|
||||
// Heartbeat that exceeds timeout
|
||||
TriggerTick(now + TimeSpan.FromSeconds(1.1 * 3));
|
||||
TriggerTick(now + TimeSpan.FromSeconds(1.1 * 4));
|
||||
TriggerTick(now + TimeSpan.FromSeconds(1.1 * 20));
|
||||
|
||||
Assert.Equal(KeepAliveState.PingSent, _connection._keepAlive._state);
|
||||
|
||||
await StopConnectionAsync(expectedLastStreamId: 0, ignoreNonGoAwayFrames: false).DefaultTimeout();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task IntervalExceeded_WithoutActivity_PingSent()
|
||||
{
|
||||
_serviceContext.ServerOptions.Limits.Http2.KeepAlivePingInterval = TimeSpan.FromSeconds(1);
|
||||
|
||||
await InitializeConnectionAsync(_noopApplication).DefaultTimeout();
|
||||
|
||||
DateTimeOffset now = _serviceContext.MockSystemClock.UtcNow;
|
||||
|
||||
// Heartbeat
|
||||
TriggerTick(now);
|
||||
|
||||
// Heartbeat that exceeds interval
|
||||
TriggerTick(now + TimeSpan.FromSeconds(1.1 * 2));
|
||||
|
||||
await ExpectAsync(Http2FrameType.PING,
|
||||
withLength: 8,
|
||||
withFlags: (byte)Http2PingFrameFlags.NONE,
|
||||
withStreamId: 0).DefaultTimeout();
|
||||
|
||||
await StopConnectionAsync(expectedLastStreamId: 0, ignoreNonGoAwayFrames: false).DefaultTimeout();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task IntervalExceeded_WithActivity_NoPingSent()
|
||||
{
|
||||
_serviceContext.ServerOptions.Limits.Http2.KeepAlivePingInterval = TimeSpan.FromSeconds(1);
|
||||
|
||||
await InitializeConnectionAsync(_noopApplication).DefaultTimeout();
|
||||
|
||||
DateTimeOffset now = _serviceContext.MockSystemClock.UtcNow;
|
||||
|
||||
// Heartbeat
|
||||
TriggerTick(now);
|
||||
|
||||
await SendPingAsync(Http2PingFrameFlags.NONE).DefaultTimeout();
|
||||
await ExpectAsync(Http2FrameType.PING,
|
||||
withLength: 8,
|
||||
withFlags: (byte)Http2PingFrameFlags.ACK,
|
||||
withStreamId: 0).DefaultTimeout();
|
||||
|
||||
// Heartbeat that exceeds interval
|
||||
TriggerTick(now + TimeSpan.FromSeconds(1.1));
|
||||
|
||||
await StopConnectionAsync(expectedLastStreamId: 0, ignoreNonGoAwayFrames: false).DefaultTimeout();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task IntervalNotExceeded_NoPingSent()
|
||||
{
|
||||
_serviceContext.ServerOptions.Limits.Http2.KeepAlivePingInterval = TimeSpan.FromSeconds(5);
|
||||
|
||||
await InitializeConnectionAsync(_noopApplication).DefaultTimeout();
|
||||
|
||||
DateTimeOffset now = new DateTimeOffset(1, TimeSpan.Zero);
|
||||
|
||||
// Heartbeats
|
||||
TriggerTick(now);
|
||||
TriggerTick(now + TimeSpan.FromSeconds(1.1));
|
||||
TriggerTick(now + TimeSpan.FromSeconds(1.1 * 2));
|
||||
TriggerTick(now + TimeSpan.FromSeconds(1.1 * 3));
|
||||
|
||||
await StopConnectionAsync(expectedLastStreamId: 0, ignoreNonGoAwayFrames: false).DefaultTimeout();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task IntervalExceeded_MultipleTimes_PingsNotSentWhileAwaitingOnAck()
|
||||
{
|
||||
_serviceContext.ServerOptions.Limits.Http2.KeepAlivePingInterval = TimeSpan.FromSeconds(1);
|
||||
|
||||
await InitializeConnectionAsync(_noopApplication).DefaultTimeout();
|
||||
|
||||
DateTimeOffset now = _serviceContext.MockSystemClock.UtcNow;
|
||||
|
||||
TriggerTick(now);
|
||||
TriggerTick(now + TimeSpan.FromSeconds(1.1 * 2));
|
||||
|
||||
await ExpectAsync(Http2FrameType.PING,
|
||||
withLength: 8,
|
||||
withFlags: (byte)Http2PingFrameFlags.NONE,
|
||||
withStreamId: 0).DefaultTimeout();
|
||||
|
||||
TriggerTick(now + TimeSpan.FromSeconds(1.1 * 3));
|
||||
TriggerTick(now + TimeSpan.FromSeconds(1.1 * 4));
|
||||
TriggerTick(now + TimeSpan.FromSeconds(1.1 * 5));
|
||||
|
||||
await StopConnectionAsync(expectedLastStreamId: 0, ignoreNonGoAwayFrames: false).DefaultTimeout();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task IntervalExceeded_MultipleTimes_PingSentAfterAck()
|
||||
{
|
||||
_serviceContext.ServerOptions.Limits.Http2.KeepAlivePingInterval = TimeSpan.FromSeconds(1);
|
||||
|
||||
await InitializeConnectionAsync(_noopApplication).DefaultTimeout();
|
||||
|
||||
DateTimeOffset now = _serviceContext.MockSystemClock.UtcNow;
|
||||
|
||||
// Heartbeats
|
||||
TriggerTick(now);
|
||||
TriggerTick(now + TimeSpan.FromSeconds(1.1 * 2));
|
||||
|
||||
await ExpectAsync(Http2FrameType.PING,
|
||||
withLength: 8,
|
||||
withFlags: (byte)Http2PingFrameFlags.NONE,
|
||||
withStreamId: 0).DefaultTimeout();
|
||||
await SendPingAsync(Http2PingFrameFlags.ACK).DefaultTimeout();
|
||||
|
||||
TriggerTick(now + TimeSpan.FromSeconds(1.1 * 3));
|
||||
TriggerTick(now + TimeSpan.FromSeconds(1.1 * 4));
|
||||
|
||||
await ExpectAsync(Http2FrameType.PING,
|
||||
withLength: 8,
|
||||
withFlags: (byte)Http2PingFrameFlags.NONE,
|
||||
withStreamId: 0).DefaultTimeout();
|
||||
await SendPingAsync(Http2PingFrameFlags.ACK).DefaultTimeout();
|
||||
|
||||
TriggerTick(now + TimeSpan.FromSeconds(1.1 * 5));
|
||||
TriggerTick(now + TimeSpan.FromSeconds(1.1 * 6));
|
||||
|
||||
await ExpectAsync(Http2FrameType.PING,
|
||||
withLength: 8,
|
||||
withFlags: (byte)Http2PingFrameFlags.NONE,
|
||||
withStreamId: 0).DefaultTimeout();
|
||||
|
||||
await StopConnectionAsync(expectedLastStreamId: 0, ignoreNonGoAwayFrames: false).DefaultTimeout();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TimeoutExceeded_NoAck_GoAway()
|
||||
{
|
||||
_serviceContext.ServerOptions.Limits.Http2.KeepAlivePingInterval = TimeSpan.FromSeconds(1);
|
||||
_serviceContext.ServerOptions.Limits.Http2.KeepAlivePingTimeout = TimeSpan.FromSeconds(3);
|
||||
|
||||
await InitializeConnectionAsync(_noopApplication).DefaultTimeout();
|
||||
|
||||
DateTimeOffset now = _serviceContext.MockSystemClock.UtcNow;
|
||||
|
||||
// Heartbeat
|
||||
TriggerTick(now);
|
||||
|
||||
// Heartbeat that exceeds interval
|
||||
TriggerTick(now + TimeSpan.FromSeconds(1.1 * 2));
|
||||
|
||||
await ExpectAsync(Http2FrameType.PING,
|
||||
withLength: 8,
|
||||
withFlags: (byte)Http2PingFrameFlags.NONE,
|
||||
withStreamId: 0).DefaultTimeout();
|
||||
|
||||
// Heartbeat that exceeds timeout
|
||||
TriggerTick(now + TimeSpan.FromSeconds(1.1 * 3));
|
||||
TriggerTick(now + TimeSpan.FromSeconds(1.1 * 4));
|
||||
TriggerTick(now + TimeSpan.FromSeconds(1.1 * 5));
|
||||
TriggerTick(now + TimeSpan.FromSeconds(1.1 * 6));
|
||||
|
||||
Assert.Equal(KeepAliveState.Timeout, _connection._keepAlive._state);
|
||||
|
||||
VerifyGoAway(await ReceiveFrameAsync().DefaultTimeout(), 0, Http2ErrorCode.INTERNAL_ERROR);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TimeoutExceeded_NonPingActivity_NoGoAway()
|
||||
{
|
||||
_serviceContext.ServerOptions.Limits.Http2.KeepAlivePingInterval = TimeSpan.FromSeconds(1);
|
||||
_serviceContext.ServerOptions.Limits.Http2.KeepAlivePingTimeout = TimeSpan.FromSeconds(3);
|
||||
|
||||
await InitializeConnectionAsync(_noopApplication).DefaultTimeout();
|
||||
|
||||
DateTimeOffset now = _serviceContext.MockSystemClock.UtcNow;
|
||||
|
||||
// Heartbeat
|
||||
TriggerTick(now);
|
||||
|
||||
// Heartbeat that exceeds interval
|
||||
TriggerTick(now + TimeSpan.FromSeconds(1.1 * 2));
|
||||
|
||||
Assert.Equal(KeepAliveState.PingSent, _connection._keepAlive._state);
|
||||
await ExpectAsync(Http2FrameType.PING,
|
||||
withLength: 8,
|
||||
withFlags: (byte)Http2PingFrameFlags.NONE,
|
||||
withStreamId: 0).DefaultTimeout();
|
||||
|
||||
await StartStreamAsync(1, _browserRequestHeaders, endStream: true).DefaultTimeout();
|
||||
Assert.Equal(KeepAliveState.None, _connection._keepAlive._state);
|
||||
|
||||
await ExpectAsync(Http2FrameType.HEADERS,
|
||||
withLength: 36,
|
||||
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
|
||||
withStreamId: 1).DefaultTimeout();
|
||||
|
||||
await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false).DefaultTimeout();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task IntervalExceeded_StreamStarted_NoPingSent()
|
||||
{
|
||||
_serviceContext.ServerOptions.Limits.Http2.KeepAlivePingInterval = TimeSpan.FromSeconds(1);
|
||||
|
||||
await InitializeConnectionAsync(_noopApplication).DefaultTimeout();
|
||||
|
||||
DateTimeOffset now = _serviceContext.MockSystemClock.UtcNow;
|
||||
|
||||
// Heartbeat
|
||||
TriggerTick(now);
|
||||
|
||||
await StartStreamAsync(1, _browserRequestHeaders, endStream: true).DefaultTimeout();
|
||||
|
||||
await ExpectAsync(Http2FrameType.HEADERS,
|
||||
withLength: 36,
|
||||
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
|
||||
withStreamId: 1).DefaultTimeout();
|
||||
|
||||
// Heartbeat that exceeds interval
|
||||
TriggerTick(now + TimeSpan.FromSeconds(1.1));
|
||||
|
||||
await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false).DefaultTimeout();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task IntervalExceeded_ConnectionFlowControlUsedUpThenPings_NoPingSent()
|
||||
{
|
||||
_serviceContext.ServerOptions.Limits.Http2.KeepAlivePingInterval = TimeSpan.FromSeconds(1);
|
||||
|
||||
// Reduce connection window size so that one stream can fill it
|
||||
_serviceContext.ServerOptions.Limits.Http2.InitialConnectionWindowSize = 65535;
|
||||
|
||||
var tcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
await InitializeConnectionAsync(async c =>
|
||||
{
|
||||
// Don't consume any request data
|
||||
await tcs.Task;
|
||||
// Send headers
|
||||
await c.Request.Body.FlushAsync();
|
||||
}, expectedWindowUpdate: false).DefaultTimeout();
|
||||
|
||||
DateTimeOffset now = _serviceContext.MockSystemClock.UtcNow;
|
||||
|
||||
// Heartbeat
|
||||
TriggerTick(now);
|
||||
|
||||
await StartStreamAsync(1, _browserRequestHeaders, endStream: false).DefaultTimeout();
|
||||
|
||||
// Use up connection flow control
|
||||
await SendDataAsync(1, new byte[16384], false).DefaultTimeout();
|
||||
await SendDataAsync(1, new byte[16384], false).DefaultTimeout();
|
||||
await SendDataAsync(1, new byte[16384], false).DefaultTimeout();
|
||||
await SendDataAsync(1, new byte[16383], false).DefaultTimeout();
|
||||
|
||||
// Heartbeat that exceeds interval
|
||||
TriggerTick(now + TimeSpan.FromSeconds(1.1));
|
||||
|
||||
// Send ping that will update the keep alive on the server
|
||||
await SendPingAsync(Http2PingFrameFlags.NONE).DefaultTimeout();
|
||||
await ExpectAsync(Http2FrameType.PING,
|
||||
withLength: 8,
|
||||
withFlags: (byte)Http2PingFrameFlags.ACK,
|
||||
withStreamId: 0).DefaultTimeout();
|
||||
|
||||
// Heartbeat that exceeds interval
|
||||
TriggerTick(now + TimeSpan.FromSeconds(1.1 * 2));
|
||||
|
||||
// Continue request delegate on server
|
||||
tcs.SetResult(null);
|
||||
|
||||
await ExpectAsync(Http2FrameType.HEADERS,
|
||||
withLength: 36,
|
||||
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
|
||||
withStreamId: 1).DefaultTimeout();
|
||||
|
||||
// Server could send RST_STREAM
|
||||
await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: true).DefaultTimeout();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TimeoutExceeded_ConnectionFlowControlUsedUpThenPings_NoGoAway()
|
||||
{
|
||||
_serviceContext.ServerOptions.Limits.Http2.KeepAlivePingInterval = TimeSpan.FromSeconds(1);
|
||||
|
||||
// Reduce connection window size so that one stream can fill it
|
||||
_serviceContext.ServerOptions.Limits.Http2.InitialConnectionWindowSize = 65535;
|
||||
|
||||
var tcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
await InitializeConnectionAsync(async c =>
|
||||
{
|
||||
// Don't consume any request data
|
||||
await tcs.Task;
|
||||
// Send headers
|
||||
await c.Request.Body.FlushAsync();
|
||||
}, expectedWindowUpdate: false).DefaultTimeout();
|
||||
|
||||
DateTimeOffset now = _serviceContext.MockSystemClock.UtcNow;
|
||||
|
||||
// Heartbeat
|
||||
TriggerTick(now);
|
||||
|
||||
await StartStreamAsync(1, _browserRequestHeaders, endStream: false).DefaultTimeout();
|
||||
|
||||
// Use up connection flow control
|
||||
await SendDataAsync(1, new byte[16384], false).DefaultTimeout();
|
||||
await SendDataAsync(1, new byte[16384], false).DefaultTimeout();
|
||||
await SendDataAsync(1, new byte[16384], false).DefaultTimeout();
|
||||
await SendDataAsync(1, new byte[16383], false).DefaultTimeout();
|
||||
|
||||
// Heartbeat that exceeds interval
|
||||
TriggerTick(now + TimeSpan.FromSeconds(1.1));
|
||||
|
||||
// Heartbeat that triggers keep alive ping
|
||||
TriggerTick(now + TimeSpan.FromSeconds(1.1 * 2));
|
||||
|
||||
await ExpectAsync(Http2FrameType.PING,
|
||||
withLength: 8,
|
||||
withFlags: (byte)Http2PingFrameFlags.NONE,
|
||||
withStreamId: 0).DefaultTimeout();
|
||||
|
||||
// Send ping ack that will reset the keep alive on the server
|
||||
await SendPingAsync(Http2PingFrameFlags.ACK).DefaultTimeout();
|
||||
|
||||
// Heartbeat that exceeds interval
|
||||
TriggerTick(now + TimeSpan.FromSeconds(1.1 * 3));
|
||||
|
||||
// Continue request delegate on server
|
||||
tcs.SetResult(null);
|
||||
|
||||
await ExpectAsync(Http2FrameType.HEADERS,
|
||||
withLength: 36,
|
||||
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
|
||||
withStreamId: 1).DefaultTimeout();
|
||||
|
||||
// Server could send RST_STREAM
|
||||
await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: true).DefaultTimeout();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -441,7 +441,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
.Callback<TimeoutReason>(r => httpConnection.OnTimeout(r));
|
||||
}
|
||||
|
||||
protected async Task InitializeConnectionAsync(RequestDelegate application, int expectedSettingsCount = 3)
|
||||
protected async Task InitializeConnectionAsync(RequestDelegate application, int expectedSettingsCount = 3, bool expectedWindowUpdate = true)
|
||||
{
|
||||
if (_connection == null)
|
||||
{
|
||||
|
|
@ -473,10 +473,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
withFlags: 0,
|
||||
withStreamId: 0);
|
||||
|
||||
await ExpectAsync(Http2FrameType.WINDOW_UPDATE,
|
||||
withLength: 4,
|
||||
withFlags: 0,
|
||||
withStreamId: 0);
|
||||
if (expectedWindowUpdate)
|
||||
{
|
||||
await ExpectAsync(Http2FrameType.WINDOW_UPDATE,
|
||||
withLength: 4,
|
||||
withFlags: 0,
|
||||
withStreamId: 0);
|
||||
}
|
||||
|
||||
await ExpectAsync(Http2FrameType.SETTINGS,
|
||||
withLength: 0,
|
||||
|
|
@ -497,6 +500,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
((IRequestProcessor)((Http2TestBase)state)._connection)?.Tick(default);
|
||||
}
|
||||
|
||||
protected void TriggerTick(DateTimeOffset now)
|
||||
{
|
||||
_serviceContext.MockSystemClock.UtcNow = now;
|
||||
((IRequestProcessor)_connection)?.Tick(now);
|
||||
}
|
||||
|
||||
protected Task StartStreamAsync(int streamId, IEnumerable<KeyValuePair<string, string>> headers, bool endStream)
|
||||
{
|
||||
var writableBuffer = _pair.Application.Output;
|
||||
|
|
|
|||
Loading…
Reference in New Issue