From 8de366e1559fd732e6974b8c9bf494bec73949be Mon Sep 17 00:00:00 2001 From: Chris R Date: Fri, 12 Aug 2022 12:41:39 -0700 Subject: [PATCH] Remove QuicTransportOptions.IdleTimeout #34955 --- .../src/Internal/Http3/Http3Connection.cs | 18 ++- .../src/Internal/QuicConnectionListener.cs | 2 +- .../src/PublicAPI.Unshipped.txt | 2 + .../src/QuicTransportOptions.cs | 5 - .../Transport.Quic/test/QuicTestHelpers.cs | 1 - .../Http3/Http3TimeoutTests.cs | 111 ++++++++++++++++-- 6 files changed, 122 insertions(+), 17 deletions(-) diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs index 4993ea7f1264..e89d9fa895e9 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs @@ -97,6 +97,7 @@ private void UpdateHighestOpenedRequestStreamId(long streamId) public Http3ControlStream? EncoderStream { get; set; } public Http3ControlStream? DecoderStream { get; set; } public string ConnectionId => _context.ConnectionId; + public ITimeoutControl TimeoutControl => _context.TimeoutControl; public void StopProcessingNextRequest() => StopProcessingNextRequest(serverInitiated: true); @@ -264,7 +265,7 @@ private void UpdateStreamTimeouts(DateTimeOffset now) if (stream.StreamTimeoutTicks == default) { - stream.StreamTimeoutTicks = _context.TimeoutControl.GetResponseDrainDeadline(ticks, minDataRate); + stream.StreamTimeoutTicks = TimeoutControl.GetResponseDrainDeadline(ticks, minDataRate); } if (stream.StreamTimeoutTicks < ticks) @@ -306,6 +307,9 @@ public async Task ProcessRequestsAsync(IHttpApplication appl // Don't delay on waiting to send outbound control stream settings. outboundControlStreamTask = ProcessOutboundControlStreamAsync(outboundControlStream); + // Close the connection if we don't receive any request streams + TimeoutControl.SetTimeout(Limits.KeepAliveTimeout.Ticks, TimeoutReason.KeepAlive); + while (_stoppedAcceptingStreams == 0) { var streamContext = await _multiplexedContext.AcceptAsync(_acceptStreamsCts.Token); @@ -494,7 +498,7 @@ public async Task ProcessRequestsAsync(IHttpApplication appl await _streamCompletionAwaitable; } - _context.TimeoutControl.CancelTimeout(); + TimeoutControl.CancelTimeout(); } catch { @@ -754,6 +758,11 @@ void IHttp3StreamLifetimeHandler.OnStreamCreated(IHttp3Stream stream) { if (stream.IsRequestStream) { + if (_activeRequestCount == 0 && TimeoutControl.TimerReason == TimeoutReason.KeepAlive) + { + TimeoutControl.CancelTimeout(); + } + _activeRequestCount++; } _streams[stream.StreamId] = stream; @@ -767,6 +776,11 @@ void IHttp3StreamLifetimeHandler.OnStreamCompleted(IHttp3Stream stream) if (stream.IsRequestStream) { _activeRequestCount--; + + if (_activeRequestCount == 0) + { + TimeoutControl.SetTimeout(Limits.KeepAliveTimeout.Ticks, TimeoutReason.KeepAlive); + } } _streams.Remove(stream.StreamId); } diff --git a/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicConnectionListener.cs b/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicConnectionListener.cs index 2414c071adc7..68eb810f13c7 100644 --- a/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicConnectionListener.cs +++ b/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicConnectionListener.cs @@ -88,7 +88,7 @@ public QuicConnectionListener( var connectionOptions = new QuicServerConnectionOptions { ServerAuthenticationOptions = serverAuthenticationOptions, - IdleTimeout = options.IdleTimeout, + IdleTimeout = Timeout.InfiniteTimeSpan, // Kestrel manages connection lifetimes itself so it can send GoAway's. MaxInboundBidirectionalStreams = options.MaxBidirectionalStreamCount, MaxInboundUnidirectionalStreams = options.MaxUnidirectionalStreamCount, DefaultCloseErrorCode = options.DefaultCloseErrorCode, diff --git a/src/Servers/Kestrel/Transport.Quic/src/PublicAPI.Unshipped.txt b/src/Servers/Kestrel/Transport.Quic/src/PublicAPI.Unshipped.txt index 310a87d3161c..4e800a7e0542 100644 --- a/src/Servers/Kestrel/Transport.Quic/src/PublicAPI.Unshipped.txt +++ b/src/Servers/Kestrel/Transport.Quic/src/PublicAPI.Unshipped.txt @@ -5,5 +5,7 @@ Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.QuicTransportOptions.DefaultS Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.QuicTransportOptions.DefaultStreamErrorCode.set -> void Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.QuicTransportOptions.MaxBidirectionalStreamCount.get -> int Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.QuicTransportOptions.MaxUnidirectionalStreamCount.get -> int +*REMOVED*Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.QuicTransportOptions.IdleTimeout.get -> System.TimeSpan +*REMOVED*Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.QuicTransportOptions.IdleTimeout.set -> void *REMOVED*Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.QuicTransportOptions.MaxBidirectionalStreamCount.get -> ushort *REMOVED*Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.QuicTransportOptions.MaxUnidirectionalStreamCount.get -> ushort diff --git a/src/Servers/Kestrel/Transport.Quic/src/QuicTransportOptions.cs b/src/Servers/Kestrel/Transport.Quic/src/QuicTransportOptions.cs index 9bbf0f408691..7896c8c23782 100644 --- a/src/Servers/Kestrel/Transport.Quic/src/QuicTransportOptions.cs +++ b/src/Servers/Kestrel/Transport.Quic/src/QuicTransportOptions.cs @@ -23,11 +23,6 @@ public sealed class QuicTransportOptions /// public int MaxUnidirectionalStreamCount { get; set; } = 10; - /// - /// Sets the idle timeout for connections and streams. - /// - public TimeSpan IdleTimeout { get; set; } = TimeSpan.FromSeconds(130); // Matches KestrelServerLimits.KeepAliveTimeout. - /// /// The maximum read size. /// diff --git a/src/Servers/Kestrel/Transport.Quic/test/QuicTestHelpers.cs b/src/Servers/Kestrel/Transport.Quic/test/QuicTestHelpers.cs index 69e334e3099f..3b26da87cb05 100644 --- a/src/Servers/Kestrel/Transport.Quic/test/QuicTestHelpers.cs +++ b/src/Servers/Kestrel/Transport.Quic/test/QuicTestHelpers.cs @@ -33,7 +33,6 @@ public static QuicTransportFactory CreateTransportFactory( long defaultCloseErrorCode = 0) { var quicTransportOptions = new QuicTransportOptions(); - quicTransportOptions.IdleTimeout = TimeSpan.FromMinutes(1); quicTransportOptions.MaxBidirectionalStreamCount = 200; quicTransportOptions.MaxUnidirectionalStreamCount = 200; quicTransportOptions.DefaultCloseErrorCode = defaultCloseErrorCode; diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TimeoutTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TimeoutTests.cs index 88782e9e57c8..1169dd10bbe8 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TimeoutTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TimeoutTests.cs @@ -1,27 +1,122 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; -using System.Collections.Generic; -using System.Linq; using System.Net.Http; -using System.Reflection.PortableExecutable; -using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Features; -using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Logging; using Moq; -using Xunit; -using Xunit.Sdk; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests; public class Http3TimeoutTests : Http3TestBase { + [Fact] + public async Task KeepAliveTimeout_ControlStreamNotReceived_ConnectionClosed() + { + var limits = _serviceContext.ServerOptions.Limits; + + await Http3Api.InitializeConnectionAsync(_noopApplication).DefaultTimeout(); + + var controlStream = await Http3Api.GetInboundControlStream().DefaultTimeout(); + await controlStream.ExpectSettingsAsync().DefaultTimeout(); + + Http3Api.AdvanceClock(limits.KeepAliveTimeout + TimeSpan.FromTicks(1)); + + await Http3Api.WaitForConnectionStopAsync(0, false, expectedErrorCode: Http3ErrorCode.NoError); + } + + [Fact] + public async Task KeepAliveTimeout_RequestNotReceived_ConnectionClosed() + { + var limits = _serviceContext.ServerOptions.Limits; + + await Http3Api.InitializeConnectionAsync(_noopApplication).DefaultTimeout(); + await Http3Api.CreateControlStream(); + + var controlStream = await Http3Api.GetInboundControlStream().DefaultTimeout(); + await controlStream.ExpectSettingsAsync().DefaultTimeout(); + + Http3Api.AdvanceClock(limits.KeepAliveTimeout + TimeSpan.FromTicks(1)); + + await Http3Api.WaitForConnectionStopAsync(0, false, expectedErrorCode: Http3ErrorCode.NoError); + } + + [Fact] + public async Task KeepAliveTimeout_AfterRequestComplete_ConnectionClosed() + { + var requestHeaders = new[] + { + new KeyValuePair(InternalHeaderNames.Method, "GET"), + new KeyValuePair(InternalHeaderNames.Path, "/"), + new KeyValuePair(InternalHeaderNames.Scheme, "http"), + }; + + var limits = _serviceContext.ServerOptions.Limits; + + await Http3Api.InitializeConnectionAsync(_noopApplication).DefaultTimeout(); + + await Http3Api.CreateControlStream(); + var controlStream = await Http3Api.GetInboundControlStream().DefaultTimeout(); + await controlStream.ExpectSettingsAsync().DefaultTimeout(); + var requestStream = await Http3Api.CreateRequestStream(requestHeaders, endStream: true); + await requestStream.ExpectHeadersAsync(); + + await requestStream.ExpectReceiveEndOfStream(); + await requestStream.OnDisposedTask.DefaultTimeout(); + + Http3Api.AdvanceClock(limits.KeepAliveTimeout + Heartbeat.Interval + TimeSpan.FromTicks(1)); + + await Http3Api.WaitForConnectionStopAsync(4, false, expectedErrorCode: Http3ErrorCode.NoError); + } + + [Fact] + public async Task KeepAliveTimeout_LongRunningRequest_KeepsConnectionAlive() + { + var requestHeaders = new[] + { + new KeyValuePair(InternalHeaderNames.Method, "GET"), + new KeyValuePair(InternalHeaderNames.Path, "/"), + new KeyValuePair(InternalHeaderNames.Scheme, "http"), + }; + + var limits = _serviceContext.ServerOptions.Limits; + var requestReceivedTcs = new TaskCompletionSource(); + var requestFinishedTcs = new TaskCompletionSource(); + + await Http3Api.InitializeConnectionAsync(_ => + { + requestReceivedTcs.SetResult(); + return requestFinishedTcs.Task; + }).DefaultTimeout(); + + await Http3Api.CreateControlStream(); + var controlStream = await Http3Api.GetInboundControlStream().DefaultTimeout(); + await controlStream.ExpectSettingsAsync().DefaultTimeout(); + var requestStream = await Http3Api.CreateRequestStream(requestHeaders, endStream: true); + + await requestReceivedTcs.Task; + + Http3Api.AdvanceClock(limits.KeepAliveTimeout); + Http3Api.AdvanceClock(limits.KeepAliveTimeout); + Http3Api.AdvanceClock(limits.KeepAliveTimeout); + Http3Api.AdvanceClock(limits.KeepAliveTimeout); + Http3Api.AdvanceClock(limits.KeepAliveTimeout); + + requestFinishedTcs.SetResult(); + + await requestStream.ExpectHeadersAsync(); + + await requestStream.ExpectReceiveEndOfStream(); + await requestStream.OnDisposedTask.DefaultTimeout(); + + Http3Api.AdvanceClock(limits.KeepAliveTimeout + Heartbeat.Interval + TimeSpan.FromTicks(1)); + + await Http3Api.WaitForConnectionStopAsync(4, false, expectedErrorCode: Http3ErrorCode.NoError); + } [Fact] public async Task HEADERS_IncompleteFrameReceivedWithinRequestHeadersTimeout_StreamError()