Add HTTP/2 keep alive pings (#22565)

This commit is contained in:
James Newton-King 2020-06-16 10:13:09 +12:00 committed by GitHub
parent a0827ace5e
commit a861d18d24
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 585 additions and 6 deletions

View File

@ -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 { } }

View File

@ -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>

View File

@ -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;
}
}
}
}

View File

@ -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)
{

View File

@ -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;
}
}
}

View File

@ -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;

View File

@ -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();
}
}
}

View File

@ -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;