diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs index c64db15642..14b95a93bd 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs @@ -69,6 +69,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 internal readonly Http2KeepAlive _keepAlive; internal readonly Dictionary _streams = new Dictionary(); internal Http2StreamStack StreamPool; + // Max tracked streams is double max concurrent streams. + // If a small MaxConcurrentStreams value is configured then still track at least to 100 streams + // to support clients that send a burst of streams while the connection is being established. + internal uint MaxTrackedStreams => Math.Max(_serverSettings.MaxConcurrentStreams * 2, 100); internal const int InitialStreamPoolSize = 5; internal const int MaxStreamPoolSize = 100; @@ -1032,7 +1036,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 // We don't use the _serverActiveRequestCount here as during shutdown, it and the dictionary counts get out of sync. // The streams still exist in the dictionary until the client responds with a RST or END_STREAM. // Also, we care about the dictionary size for too much memory consumption. - if (_streams.Count > _serverSettings.MaxConcurrentStreams * 2) + if (_streams.Count > MaxTrackedStreams) { // Server is getting hit hard with connection resets. // Tell client to calm down. @@ -1166,7 +1170,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 // Compare to UpdateCompletedStreams, but only removes streams if over the max stream drain limit. private void MakeSpaceInDrainQueue() { - var maxStreams = _serverSettings.MaxConcurrentStreams * 2; + var maxStreams = MaxTrackedStreams; // If we're tracking too many streams, discard the oldest. while (_streams.Count >= maxStreams && _completedStreams.TryDequeue(out var stream)) { diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs index 19a7809a40..03560f70f2 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs @@ -1460,29 +1460,82 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests } [Fact] - public async Task Frame_MultipleStreams_RequestsNotFinished_EnhanceYourCalm() + public async Task MaxTrackedStreams_SmallMaxConcurrentStreams_LowerLimitOf100Async() { _serviceContext.ServerOptions.Limits.Http2.MaxStreamsPerConnection = 1; + + await InitializeConnectionAsync(_noopApplication); + + Assert.Equal((uint)100, _connection.MaxTrackedStreams); + + await StopConnectionAsync(0, ignoreNonGoAwayFrames: false); + } + + [Fact] + public async Task MaxTrackedStreams_DefaultMaxConcurrentStreams_DoubleLimit() + { + _serviceContext.ServerOptions.Limits.Http2.MaxStreamsPerConnection = 100; + + await InitializeConnectionAsync(_noopApplication); + + Assert.Equal((uint)200, _connection.MaxTrackedStreams); + + await StopConnectionAsync(0, ignoreNonGoAwayFrames: false); + } + + [Fact] + public async Task MaxTrackedStreams_LargeMaxConcurrentStreams_DoubleLimit() + { + _serviceContext.ServerOptions.Limits.Http2.MaxStreamsPerConnection = int.MaxValue; + + await InitializeConnectionAsync(_noopApplication); + + Assert.Equal((uint)int.MaxValue * 2, _connection.MaxTrackedStreams); + + await StopConnectionAsync(0, ignoreNonGoAwayFrames: false); + } + + [Fact] + public Task Frame_MultipleStreams_RequestsNotFinished_LowMaxStreamsPerConnection_EnhanceYourCalmAfter100() + { + // Kestrel always tracks at least 100 streams + return RequestUntilEnhanceYourCalm(maxStreamsPerConnection: 1, sentStreams: 101); + } + + [Fact] + public Task Frame_MultipleStreams_RequestsNotFinished_DefaultMaxStreamsPerConnection_EnhanceYourCalmAfterDoubleMaxStreams() + { + // Kestrel tracks max streams per connection * 2 + return RequestUntilEnhanceYourCalm(maxStreamsPerConnection: 100, sentStreams: 201); + } + + private async Task RequestUntilEnhanceYourCalm(int maxStreamsPerConnection, int sentStreams) + { + _serviceContext.ServerOptions.Limits.Http2.MaxStreamsPerConnection = maxStreamsPerConnection; var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); await InitializeConnectionAsync(async context => { await tcs.Task.DefaultTimeout(); }); - await StartStreamAsync(1, _browserRequestHeaders, endStream: false); - await SendRstStreamAsync(1); - await StartStreamAsync(3, _browserRequestHeaders, endStream: true); - await SendRstStreamAsync(3); - await StartStreamAsync(5, _browserRequestHeaders, endStream: true); + var streamId = 1; + for (var i = 0; i < sentStreams - 1; i++) + { + await StartStreamAsync(streamId, _browserRequestHeaders, endStream: true); + await SendRstStreamAsync(streamId); + streamId += 2; + } + + await StartStreamAsync(streamId, _browserRequestHeaders, endStream: true); await WaitForStreamErrorAsync( - expectedStreamId: 5, + expectedStreamId: streamId, expectedErrorCode: Http2ErrorCode.ENHANCE_YOUR_CALM, expectedErrorMessage: CoreStrings.Http2TellClientToCalmDown); tcs.SetResult(); - await StopConnectionAsync(5, ignoreNonGoAwayFrames: false); + await StopConnectionAsync(streamId, ignoreNonGoAwayFrames: false); } [Fact]