@@ -33,14 +33,51 @@ internal partial class Http2Connection : IHttp2StreamLifetimeHandler, IHttpHeade
33
33
private const PseudoHeaderFields _mandatoryRequestPseudoHeaderFields =
34
34
PseudoHeaderFields . Method | PseudoHeaderFields . Path | PseudoHeaderFields . Scheme ;
35
35
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
+
36
72
private readonly HttpConnectionContext _context ;
37
73
private readonly Http2FrameWriter _frameWriter ;
38
74
private readonly Pipe _input ;
39
75
private readonly Task _inputTask ;
40
76
private readonly int _minAllocBufferSize ;
41
77
private readonly HPackDecoder _hpackDecoder ;
42
78
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
44
81
45
82
private readonly Http2PeerSettings _serverSettings = new Http2PeerSettings ( ) ;
46
83
private readonly Http2PeerSettings _clientSettings = new Http2PeerSettings ( ) ;
@@ -59,6 +96,9 @@ internal partial class Http2Connection : IHttp2StreamLifetimeHandler, IHttpHeade
59
96
private int _clientActiveStreamCount ;
60
97
private int _serverActiveStreamCount ;
61
98
99
+ private int _enhanceYourCalmCount ;
100
+ private int _tickCount ;
101
+
62
102
// The following are the only fields that can be modified outside of the ProcessRequestsAsync loop.
63
103
private readonly ConcurrentQueue < Http2Stream > _completedStreams = new ConcurrentQueue < Http2Stream > ( ) ;
64
104
private readonly StreamCloseAwaitable _streamCompletionAwaitable = new StreamCloseAwaitable ( ) ;
@@ -88,6 +128,9 @@ public Http2Connection(HttpConnectionContext context)
88
128
// Capture the ExecutionContext before dispatching HTTP/2 middleware. Will be restored by streams when processing request
89
129
_context . InitialExecutionContext = ExecutionContext . Capture ( ) ;
90
130
131
+ _outputFlowControlAwaitableProvider = new MultipleAwaitableProvider ( ) ;
132
+ _outputFlowControl = new OutputFlowControl ( _outputFlowControlAwaitableProvider , Http2PeerSettings . DefaultInitialWindowSize ) ;
133
+
91
134
_frameWriter = new Http2FrameWriter (
92
135
context . Transport . Output ,
93
136
context . ConnectionContext ,
@@ -129,6 +172,16 @@ public Http2Connection(HttpConnectionContext context)
129
172
_serverSettings . MaxHeaderListSize = ( uint ) httpLimits . MaxRequestHeadersTotalSize ;
130
173
_serverSettings . InitialWindowSize = ( uint ) http2Limits . InitialStreamWindowSize ;
131
174
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
+
132
185
// Start pool off at a smaller size if the max number of streams is less than the InitialStreamPoolSize
133
186
StreamPool = new PooledStreamStack < Http2Stream > ( Math . Min ( InitialStreamPoolSize , http2Limits . MaxStreamsPerConnection ) ) ;
134
187
@@ -352,13 +405,20 @@ public async Task ProcessRequestsAsync<TContext>(IHttpApplication<TContext> appl
352
405
stream . Abort ( new IOException ( CoreStrings . Http2StreamAborted , connectionError ) ) ;
353
406
}
354
407
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 )
359
413
{
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
+ }
362
422
}
363
423
364
424
while ( StreamPool . TryPop ( out var pooledStream ) )
@@ -1053,6 +1113,20 @@ private void StartStream()
1053
1113
throw new Http2StreamErrorException ( _currentHeadersStream . StreamId , CoreStrings . Http2ErrorMaxStreams , Http2ErrorCode . REFUSED_STREAM ) ;
1054
1114
}
1055
1115
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
+
1056
1130
// We don't use the _serverActiveRequestCount here as during shutdown, it and the dictionary counts get out of sync.
1057
1131
// The streams still exist in the dictionary until the client responds with a RST or END_STREAM.
1058
1132
// Also, we care about the dictionary size for too much memory consumption.
@@ -1061,6 +1135,20 @@ private void StartStream()
1061
1135
// Server is getting hit hard with connection resets.
1062
1136
// Tell client to calm down.
1063
1137
// 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
+
1064
1152
throw new Http2StreamErrorException ( _currentHeadersStream . StreamId , CoreStrings . Http2TellClientToCalmDown , Http2ErrorCode . ENHANCE_YOUR_CALM ) ;
1065
1153
}
1066
1154
}
@@ -1123,6 +1211,10 @@ private void AbortStream(int streamId, IOException error)
1123
1211
void IRequestProcessor . Tick ( DateTimeOffset now )
1124
1212
{
1125
1213
Input . CancelPendingRead ( ) ;
1214
+ if ( IsEnhanceYourCalmEnabled && ++ _tickCount % EnhanceYourCalmTickWindowCount == 0 )
1215
+ {
1216
+ Interlocked . Exchange ( ref _enhanceYourCalmCount , 0 ) ;
1217
+ }
1126
1218
}
1127
1219
1128
1220
void IHttp2StreamLifetimeHandler . OnStreamCompleted ( Http2Stream stream )
0 commit comments