Skip to content

Commit b0468b9

Browse files
amcaseyvseanreesermsft
authored andcommitted
Track indicators of excessive stream resets
If the server has to send a lot of ENHANCE_YOUR_CALM messages or the output control flow queue is very large, there are probably a larger than expected number of client-initiated stream resets.
1 parent ace21f3 commit b0468b9

File tree

6 files changed

+134
-8
lines changed

6 files changed

+134
-8
lines changed

src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs

Lines changed: 99 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -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)

src/Servers/Kestrel/Core/src/Internal/Infrastructure/IKestrelTrace.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,12 @@ internal interface IKestrelTrace : ILogger
7878

7979
void Http2FrameSending(string connectionId, Http2Frame frame);
8080

81+
void Http2TooManyEnhanceYourCalms(string connectionId, int count);
82+
8183
void Http2MaxConcurrentStreamsReached(string connectionId);
8284

85+
void Http2FlowControlQueueOperationsExceeded(string connectionId, int count);
86+
8387
void InvalidResponseHeaderRemoved();
8488

8589
void Http3ConnectionError(string connectionId, Http3ConnectionErrorException ex);

src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelTrace.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,22 @@ public void Http3GoAwayStreamId(string connectionId, long goAwayStreamId)
395395
Http3GoAwayStreamId(_http3Logger, connectionId, goAwayStreamId);
396396
}
397397

398+
[LoggerMessage(54, LogLevel.Error, @"Connection id ""{ConnectionId}"" aborted since at least ""{Count}"" ENHANCE_YOUR_CALM responses were required per second.", EventName = "Http2TooManyEnhanceYourCalms")]
399+
private static partial void Http2TooManyEnhanceYourCalms(ILogger logger, string connectionId, int count);
400+
401+
public void Http2TooManyEnhanceYourCalms(string connectionId, int count)
402+
{
403+
Http2TooManyEnhanceYourCalms(_http2Logger, connectionId, count);
404+
}
405+
406+
[LoggerMessage(55, LogLevel.Error, @"Connection id ""{ConnectionId}"" exceeded the output flow control maximum queue size of ""{Count}"".", EventName = "Http2FlowControlQueueOperationsExceeded")]
407+
private static partial void Http2FlowControlQueueOperationsExceeded(ILogger logger, string connectionId, int count);
408+
409+
public void Http2FlowControlQueueOperationsExceeded(string connectionId, int count)
410+
{
411+
Http2FlowControlQueueOperationsExceeded(_http3Logger, connectionId, count);
412+
}
413+
398414
public virtual void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
399415
=> _generalLogger.Log(logLevel, eventId, state, exception, formatter);
400416

src/Servers/Kestrel/perf/Microbenchmarks/Mocks/MockTrace.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,9 @@ public void Http2ConnectionClosing(string connectionId) { }
5959
public void Http2ConnectionClosed(string connectionId, int highestOpenedStreamId) { }
6060
public void Http2FrameReceived(string connectionId, Http2Frame frame) { }
6161
public void Http2FrameSending(string connectionId, Http2Frame frame) { }
62+
public void Http2TooManyEnhanceYourCalms(string connectionId, int count) { }
6263
public void Http2MaxConcurrentStreamsReached(string connectionId) { }
64+
public void Http2FlowControlQueueOperationsExceeded(string connectionId, int count) { }
6365
public void InvalidResponseHeaderRemoved() { }
6466
public void Http3ConnectionError(string connectionId, Http3ConnectionErrorException ex) { }
6567
public void Http3ConnectionClosing(string connectionId) { }

src/Servers/Kestrel/samples/Http2SampleApp/Http2SampleApp.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk.Web">
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
22

33
<PropertyGroup>
44
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>

src/Servers/Kestrel/shared/test/CompositeKestrelTrace.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,12 +234,24 @@ public void Http2FrameSending(string connectionId, Http2Frame frame)
234234
_trace2.Http2FrameSending(connectionId, frame);
235235
}
236236

237+
public void Http2TooManyEnhanceYourCalms(string connectionId, int count)
238+
{
239+
_trace1.Http2TooManyEnhanceYourCalms(connectionId, count);
240+
_trace2.Http2TooManyEnhanceYourCalms(connectionId, count);
241+
}
242+
237243
public void Http2MaxConcurrentStreamsReached(string connectionId)
238244
{
239245
_trace1.Http2MaxConcurrentStreamsReached(connectionId);
240246
_trace2.Http2MaxConcurrentStreamsReached(connectionId);
241247
}
242248

249+
public void Http2FlowControlQueueOperationsExceeded(string connectionId, int count)
250+
{
251+
_trace1.Http2FlowControlQueueOperationsExceeded(connectionId, count);
252+
_trace2.Http2FlowControlQueueOperationsExceeded(connectionId, count);
253+
}
254+
243255
public void InvalidResponseHeaderRemoved()
244256
{
245257
_trace1.InvalidResponseHeaderRemoved();

0 commit comments

Comments
 (0)