@@ -33,14 +33,51 @@ internal partial class Http2Connection : IHttp2StreamLifetimeHandler, IHttpHeade
3333 private const PseudoHeaderFields _mandatoryRequestPseudoHeaderFields =
3434 PseudoHeaderFields . Method | PseudoHeaderFields . Path | PseudoHeaderFields . Scheme ;
3535
36+ private const string EnhanceYourCalmMaximumCountProperty = "Microsoft.AspNetCore.Server.Kestrel.Http2.EnhanceYourCalmCount" ;
37+ private const string MaximumFlowControlQueueSizeProperty = "Microsoft.AspNetCore.Server.Kestrel.Http2.MaxConnectionFlowControlQueueSize" ;
38+
39+ private static readonly int _enhanceYourCalmMaximumCount = AppContext . GetData ( EnhanceYourCalmMaximumCountProperty ) is int eycMaxCount
40+ ? eycMaxCount
41+ : 10 ;
42+
43+ // Accumulate _enhanceYourCalmCount over the course of EnhanceYourCalmTickWindowCount ticks.
44+ // This should make bursts less likely to trigger disconnects.
45+ private const int EnhanceYourCalmTickWindowCount = 5 ;
46+
47+ private static bool IsEnhanceYourCalmEnabled => _enhanceYourCalmMaximumCount > 0 ;
48+
49+ private static readonly int ? ConfiguredMaximumFlowControlQueueSize = GetConfiguredMaximumFlowControlQueueSize ( ) ;
50+
51+ private static int ? GetConfiguredMaximumFlowControlQueueSize ( )
52+ {
53+ var data = AppContext . GetData ( MaximumFlowControlQueueSizeProperty ) ;
54+
55+ if ( data is int count )
56+ {
57+ return count ;
58+ }
59+
60+ if ( data is string countStr && int . TryParse ( countStr , out var parsed ) )
61+ {
62+ return parsed ;
63+ }
64+
65+ return null ;
66+ }
67+
68+ private readonly int _maximumFlowControlQueueSize ;
69+
70+ private bool IsMaximumFlowControlQueueSizeEnabled => _maximumFlowControlQueueSize > 0 ;
71+
3672 private readonly HttpConnectionContext _context ;
3773 private readonly Http2FrameWriter _frameWriter ;
3874 private readonly Pipe _input ;
3975 private readonly Task _inputTask ;
4076 private readonly int _minAllocBufferSize ;
4177 private readonly HPackDecoder _hpackDecoder ;
4278 private readonly InputFlowControl _inputFlowControl ;
43- private readonly OutputFlowControl _outputFlowControl = new OutputFlowControl ( new MultipleAwaitableProvider ( ) , Http2PeerSettings . DefaultInitialWindowSize ) ;
79+ private readonly OutputFlowControl _outputFlowControl ;
80+ private readonly AwaitableProvider _outputFlowControlAwaitableProvider ; // Keep our own reference so we can track queue size
4481
4582 private readonly Http2PeerSettings _serverSettings = new Http2PeerSettings ( ) ;
4683 private readonly Http2PeerSettings _clientSettings = new Http2PeerSettings ( ) ;
@@ -59,6 +96,9 @@ internal partial class Http2Connection : IHttp2StreamLifetimeHandler, IHttpHeade
5996 private int _clientActiveStreamCount ;
6097 private int _serverActiveStreamCount ;
6198
99+ private int _enhanceYourCalmCount ;
100+ private int _tickCount ;
101+
62102 // The following are the only fields that can be modified outside of the ProcessRequestsAsync loop.
63103 private readonly ConcurrentQueue < Http2Stream > _completedStreams = new ConcurrentQueue < Http2Stream > ( ) ;
64104 private readonly StreamCloseAwaitable _streamCompletionAwaitable = new StreamCloseAwaitable ( ) ;
@@ -88,6 +128,9 @@ public Http2Connection(HttpConnectionContext context)
88128 // Capture the ExecutionContext before dispatching HTTP/2 middleware. Will be restored by streams when processing request
89129 _context . InitialExecutionContext = ExecutionContext . Capture ( ) ;
90130
131+ _outputFlowControlAwaitableProvider = new MultipleAwaitableProvider ( ) ;
132+ _outputFlowControl = new OutputFlowControl ( _outputFlowControlAwaitableProvider , Http2PeerSettings . DefaultInitialWindowSize ) ;
133+
91134 _frameWriter = new Http2FrameWriter (
92135 context . Transport . Output ,
93136 context . ConnectionContext ,
@@ -129,6 +172,16 @@ public Http2Connection(HttpConnectionContext context)
129172 _serverSettings . MaxHeaderListSize = ( uint ) httpLimits . MaxRequestHeadersTotalSize ;
130173 _serverSettings . InitialWindowSize = ( uint ) http2Limits . InitialStreamWindowSize ;
131174
175+ _maximumFlowControlQueueSize = ConfiguredMaximumFlowControlQueueSize is null
176+ ? 4 * http2Limits . MaxStreamsPerConnection
177+ : ( int ) ConfiguredMaximumFlowControlQueueSize ;
178+
179+ if ( _maximumFlowControlQueueSize < http2Limits . MaxStreamsPerConnection )
180+ {
181+ _maximumFlowControlQueueSize = http2Limits . MaxStreamsPerConnection ;
182+ Log . LogTrace ( $ "The configured maximum flow control queue size { ConfiguredMaximumFlowControlQueueSize } is less than the maximum streams per connection { http2Limits . MaxStreamsPerConnection } - increasing to match.") ;
183+ }
184+
132185 // Start pool off at a smaller size if the max number of streams is less than the InitialStreamPoolSize
133186 StreamPool = new PooledStreamStack < Http2Stream > ( Math . Min ( InitialStreamPoolSize , http2Limits . MaxStreamsPerConnection ) ) ;
134187
@@ -352,13 +405,20 @@ public async Task ProcessRequestsAsync<TContext>(IHttpApplication<TContext> appl
352405 stream . Abort ( new IOException ( CoreStrings . Http2StreamAborted , connectionError ) ) ;
353406 }
354407
355- // Use the server _serverActiveStreamCount to drain all requests on the server side.
356- // Can't use _clientActiveStreamCount now as we now decrement that count earlier/
357- // Can't use _streams.Count as we wait for RST/END_STREAM before removing the stream from the dictionary
358- while ( _serverActiveStreamCount > 0 )
408+ // For some reason, this loop doesn't terminate when we're trying to abort.
409+ // Since we're making a narrow fix for a patch, we'll bypass it in such scenarios.
410+ // TODO: This is probably a bug - something in here should probably detect aborted
411+ // connections and short-circuit.
412+ if ( ! ( IsEnhanceYourCalmEnabled || IsMaximumFlowControlQueueSizeEnabled ) || error is not Http2ConnectionErrorException )
359413 {
360- await _streamCompletionAwaitable ;
361- UpdateCompletedStreams ( ) ;
414+ // Use the server _serverActiveStreamCount to drain all requests on the server side.
415+ // Can't use _clientActiveStreamCount now as we now decrement that count earlier/
416+ // Can't use _streams.Count as we wait for RST/END_STREAM before removing the stream from the dictionary
417+ while ( _serverActiveStreamCount > 0 )
418+ {
419+ await _streamCompletionAwaitable ;
420+ UpdateCompletedStreams ( ) ;
421+ }
362422 }
363423
364424 while ( StreamPool . TryPop ( out var pooledStream ) )
@@ -1053,6 +1113,20 @@ private void StartStream()
10531113 throw new Http2StreamErrorException ( _currentHeadersStream . StreamId , CoreStrings . Http2ErrorMaxStreams , Http2ErrorCode . REFUSED_STREAM ) ;
10541114 }
10551115
1116+ if ( IsMaximumFlowControlQueueSizeEnabled && _outputFlowControlAwaitableProvider . ActiveCount > _maximumFlowControlQueueSize )
1117+ {
1118+ Log . Http2FlowControlQueueOperationsExceeded ( _context . ConnectionId , _maximumFlowControlQueueSize ) ;
1119+
1120+ // Now that we've logged a useful message, we can put vague text in the exception
1121+ // messages in case they somehow make it back to the client (not expected)
1122+
1123+ // This will close the socket - we want to do that right away
1124+ Abort ( new ConnectionAbortedException ( "HTTP/2 connection exceeded the outgoing flow control maximum queue size." ) ) ;
1125+
1126+ // Throwing an exception as well will help us clean up on our end more quickly by (e.g.) skipping processing of already-buffered input
1127+ throw new Http2ConnectionErrorException ( CoreStrings . Http2ConnectionFaulted , Http2ErrorCode . INTERNAL_ERROR ) ;
1128+ }
1129+
10561130 // We don't use the _serverActiveRequestCount here as during shutdown, it and the dictionary counts get out of sync.
10571131 // The streams still exist in the dictionary until the client responds with a RST or END_STREAM.
10581132 // Also, we care about the dictionary size for too much memory consumption.
@@ -1061,6 +1135,20 @@ private void StartStream()
10611135 // Server is getting hit hard with connection resets.
10621136 // Tell client to calm down.
10631137 // TODO consider making when to send ENHANCE_YOUR_CALM configurable?
1138+
1139+ if ( IsEnhanceYourCalmEnabled && Interlocked . Increment ( ref _enhanceYourCalmCount ) > EnhanceYourCalmTickWindowCount * _enhanceYourCalmMaximumCount )
1140+ {
1141+ Log . Http2TooManyEnhanceYourCalms ( _context . ConnectionId , _enhanceYourCalmMaximumCount ) ;
1142+
1143+ // Now that we've logged a useful message, we can put vague text in the exception
1144+ // messages in case they somehow make it back to the client (not expected)
1145+
1146+ // This will close the socket - we want to do that right away
1147+ Abort ( new ConnectionAbortedException ( CoreStrings . Http2ConnectionFaulted ) ) ;
1148+ // Throwing an exception as well will help us clean up on our end more quickly by (e.g.) skipping processing of already-buffered input
1149+ throw new Http2ConnectionErrorException ( CoreStrings . Http2ConnectionFaulted , Http2ErrorCode . ENHANCE_YOUR_CALM ) ;
1150+ }
1151+
10641152 throw new Http2StreamErrorException ( _currentHeadersStream . StreamId , CoreStrings . Http2TellClientToCalmDown , Http2ErrorCode . ENHANCE_YOUR_CALM ) ;
10651153 }
10661154 }
@@ -1123,6 +1211,10 @@ private void AbortStream(int streamId, IOException error)
11231211 void IRequestProcessor . Tick ( DateTimeOffset now )
11241212 {
11251213 Input . CancelPendingRead ( ) ;
1214+ if ( IsEnhanceYourCalmEnabled && ++ _tickCount % EnhanceYourCalmTickWindowCount == 0 )
1215+ {
1216+ Interlocked . Exchange ( ref _enhanceYourCalmCount , 0 ) ;
1217+ }
11261218 }
11271219
11281220 void IHttp2StreamLifetimeHandler . OnStreamCompleted ( Http2Stream stream )
0 commit comments