From 229e4969662193afe17ad418b175e0a798041a78 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Fri, 14 Aug 2020 10:06:02 +1200 Subject: [PATCH] 3.1: Reset KeepAliveTimeout on HTTP/2 ping (#24858) * Reset KeepAliveTimeout on HTTP/2 ping (#24644) * Fix flaky keepalive ping test (#24804) * Fix flaky keepalive ping test * Clean up stream * Remove quarantine attribute * Fix test --- .../src/Internal/Http2/Http2Connection.cs | 6 ++ .../Http2/Http2TimeoutTests.cs | 70 +++++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs index 3ccdccef69..cbd16b7a3f 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs @@ -747,6 +747,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorUnexpectedFrameLength(_incomingFrame.Type, 8), Http2ErrorCode.FRAME_SIZE_ERROR); } + // Incoming ping resets connection keep alive timeout + if (TimeoutControl.TimerReason == TimeoutReason.KeepAlive) + { + TimeoutControl.ResetTimeout(Limits.KeepAliveTimeout.Ticks, TimeoutReason.KeepAlive); + } + if (_incomingFrame.PingAck) { // TODO: verify that payload is equal to the outgoing PING frame diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TimeoutTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TimeoutTests.cs index 23e8374e6f..2a2ba1edc8 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TimeoutTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TimeoutTests.cs @@ -120,6 +120,76 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests _mockTimeoutHandler.VerifyNoOtherCalls(); } + [Fact] + public async Task PING_WithinKeepAliveTimeout_ResetKeepAliveTimeout() + { + var mockSystemClock = _serviceContext.MockSystemClock; + var limits = _serviceContext.ServerOptions.Limits; + + _timeoutControl.Initialize(mockSystemClock.UtcNow.Ticks); + + CreateConnection(); + + await InitializeConnectionAsync(_noopApplication); + + // Connection starts and sets keep alive timeout + _mockTimeoutControl.Verify(c => c.SetTimeout(It.IsAny(), TimeoutReason.KeepAlive), Times.Once); + _mockTimeoutControl.Verify(c => c.ResetTimeout(It.IsAny(), TimeoutReason.KeepAlive), Times.Never); + + await SendPingAsync(Http2PingFrameFlags.NONE); + await ExpectAsync(Http2FrameType.PING, + withLength: 8, + withFlags: (byte)Http2PingFrameFlags.ACK, + withStreamId: 0); + + // Server resets keep alive timeout + _mockTimeoutControl.Verify(c => c.ResetTimeout(It.IsAny(), TimeoutReason.KeepAlive), Times.Once); + } + + [Fact] + public async Task PING_NoKeepAliveTimeout_DoesNotResetKeepAliveTimeout() + { + var mockSystemClock = _serviceContext.MockSystemClock; + var limits = _serviceContext.ServerOptions.Limits; + + _timeoutControl.Initialize(mockSystemClock.UtcNow.Ticks); + + CreateConnection(); + + await InitializeConnectionAsync(_echoApplication); + + // Connection starts and sets keep alive timeout + _mockTimeoutControl.Verify(c => c.SetTimeout(It.IsAny(), TimeoutReason.KeepAlive), Times.Once); + _mockTimeoutControl.Verify(c => c.ResetTimeout(It.IsAny(), TimeoutReason.KeepAlive), Times.Never); + _mockTimeoutControl.Verify(c => c.CancelTimeout(), Times.Never); + + // Stream will stay open because it is waiting for request body to end + await StartStreamAsync(1, _browserRequestHeaders, endStream: false); + + // Starting a stream cancels the keep alive timeout + _mockTimeoutControl.Verify(c => c.CancelTimeout(), Times.Once); + + await SendPingAsync(Http2PingFrameFlags.NONE); + await ExpectAsync(Http2FrameType.PING, + withLength: 8, + withFlags: (byte)Http2PingFrameFlags.ACK, + withStreamId: 0); + + // Server doesn't reset keep alive timeout because it isn't running + _mockTimeoutControl.Verify(c => c.ResetTimeout(It.IsAny(), TimeoutReason.KeepAlive), Times.Never); + + // End stream + await SendDataAsync(1, _helloWorldBytes, endStream: true); + await ExpectAsync(Http2FrameType.HEADERS, + withLength: 37, + withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, + withStreamId: 1); + await ExpectAsync(Http2FrameType.DATA, + withLength: _helloWorldBytes.Length, + withFlags: (byte)Http2DataFrameFlags.NONE, + withStreamId: 1); + } + [Fact] public async Task HEADERS_ReceivedWithoutAllCONTINUATIONs_WithinRequestHeadersTimeout_AbortsConnection() {