Skip to content

Commit 9e52542

Browse files
Merged PR 34066: Track indicators of excessive stream resets
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. Cherry picked from !33908
2 parents af41286 + 6160e56 commit 9e52542

File tree

4 files changed

+138
-11
lines changed

4 files changed

+138
-11
lines changed

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

Lines changed: 61 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,31 @@ internal sealed partial class Http2Connection : IHttp2StreamLifetimeHandler, IHt
4646
private const int MaxStreamPoolSize = 100;
4747
private readonly TimeSpan StreamPoolExpiry = TimeSpan.FromSeconds(5);
4848

49+
private const string MaximumEnhanceYourCalmCountProperty = "Microsoft.AspNetCore.Server.Kestrel.Http2.MaxEnhanceYourCalmCount";
50+
51+
private static readonly int _enhanceYourCalmMaximumCount = GetMaximumEnhanceYourCalmCount();
52+
53+
private static int GetMaximumEnhanceYourCalmCount()
54+
{
55+
var data = AppContext.GetData(MaximumEnhanceYourCalmCountProperty);
56+
if (data is int count)
57+
{
58+
return count;
59+
}
60+
if (data is string countStr && int.TryParse(countStr, out var parsed))
61+
{
62+
return parsed;
63+
}
64+
65+
return 20; // Empirically derived
66+
}
67+
68+
// Accumulate _enhanceYourCalmCount over the course of EnhanceYourCalmTickWindowCount ticks.
69+
// This should make bursts less likely to trigger disconnects.
70+
private const int EnhanceYourCalmTickWindowCount = 5;
71+
72+
private static bool IsEnhanceYourCalmEnabled => _enhanceYourCalmMaximumCount > 0;
73+
4974
private readonly HttpConnectionContext _context;
5075
private readonly ConnectionMetricsContext _metricsContext;
5176
private readonly Http2FrameWriter _frameWriter;
@@ -75,6 +100,9 @@ internal sealed partial class Http2Connection : IHttp2StreamLifetimeHandler, IHt
75100
private int _clientActiveStreamCount;
76101
private int _serverActiveStreamCount;
77102

103+
private int _enhanceYourCalmCount;
104+
private int _tickCount;
105+
78106
// The following are the only fields that can be modified outside of the ProcessRequestsAsync loop.
79107
private readonly ConcurrentQueue<Http2Stream> _completedStreams = new ConcurrentQueue<Http2Stream>();
80108
private readonly StreamCloseAwaitable _streamCompletionAwaitable = new StreamCloseAwaitable();
@@ -363,13 +391,20 @@ public async Task ProcessRequestsAsync<TContext>(IHttpApplication<TContext> appl
363391
stream.Abort(new IOException(CoreStrings.Http2StreamAborted, connectionError));
364392
}
365393

366-
// Use the server _serverActiveStreamCount to drain all requests on the server side.
367-
// Can't use _clientActiveStreamCount now as we now decrement that count earlier/
368-
// Can't use _streams.Count as we wait for RST/END_STREAM before removing the stream from the dictionary
369-
while (_serverActiveStreamCount > 0)
394+
// For some reason, this loop doesn't terminate when we're trying to abort.
395+
// Since we're making a narrow fix for a patch, we'll bypass it in such scenarios.
396+
// TODO: This is probably a bug - something in here should probably detect aborted
397+
// connections and short-circuit.
398+
if (!IsEnhanceYourCalmEnabled || error is not Http2ConnectionErrorException)
370399
{
371-
await _streamCompletionAwaitable;
372-
UpdateCompletedStreams();
400+
// Use the server _serverActiveStreamCount to drain all requests on the server side.
401+
// Can't use _clientActiveStreamCount now as we now decrement that count earlier/
402+
// Can't use _streams.Count as we wait for RST/END_STREAM before removing the stream from the dictionary
403+
while (_serverActiveStreamCount > 0)
404+
{
405+
await _streamCompletionAwaitable;
406+
UpdateCompletedStreams();
407+
}
373408
}
374409

375410
while (StreamPool.TryPop(out var pooledStream))
@@ -1176,6 +1211,20 @@ private void StartStream()
11761211
// Server is getting hit hard with connection resets.
11771212
// Tell client to calm down.
11781213
// TODO consider making when to send ENHANCE_YOUR_CALM configurable?
1214+
1215+
if (IsEnhanceYourCalmEnabled && Interlocked.Increment(ref _enhanceYourCalmCount) > EnhanceYourCalmTickWindowCount * _enhanceYourCalmMaximumCount)
1216+
{
1217+
Log.Http2TooManyEnhanceYourCalms(_context.ConnectionId, _enhanceYourCalmMaximumCount);
1218+
1219+
// Now that we've logged a useful message, we can put vague text in the exception
1220+
// messages in case they somehow make it back to the client (not expected)
1221+
1222+
// This will close the socket - we want to do that right away
1223+
Abort(new ConnectionAbortedException(CoreStrings.Http2ConnectionFaulted));
1224+
// Throwing an exception as well will help us clean up on our end more quickly by (e.g.) skipping processing of already-buffered input
1225+
throw new Http2ConnectionErrorException(CoreStrings.Http2ConnectionFaulted, Http2ErrorCode.ENHANCE_YOUR_CALM);
1226+
}
1227+
11791228
throw new Http2StreamErrorException(_currentHeadersStream.StreamId, CoreStrings.Http2TellClientToCalmDown, Http2ErrorCode.ENHANCE_YOUR_CALM);
11801229
}
11811230
}
@@ -1248,6 +1297,12 @@ private void AbortStream(int streamId, IOException error)
12481297
void IRequestProcessor.Tick(long timestamp)
12491298
{
12501299
Input.CancelPendingRead();
1300+
// We count EYCs over a window of a given length to avoid flagging short-lived bursts.
1301+
// At the end of each window, reset the count.
1302+
if (IsEnhanceYourCalmEnabled && ++_tickCount % EnhanceYourCalmTickWindowCount == 0)
1303+
{
1304+
Interlocked.Exchange(ref _enhanceYourCalmCount, 0);
1305+
}
12511306
}
12521307

12531308
void IHttp2StreamLifetimeHandler.OnStreamCompleted(Http2Stream stream)

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

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,31 @@ internal sealed class Http2FrameWriter
2020
// This uses C# compiler's ability to refer to static data directly. For more information see https://vcsjones.dev/2019/02/01/csharp-readonly-span-bytes-static
2121
private static ReadOnlySpan<byte> ContinueBytes => new byte[] { 0x08, 0x03, (byte)'1', (byte)'0', (byte)'0' };
2222

23+
private const string MaximumFlowControlQueueSizeProperty = "Microsoft.AspNetCore.Server.Kestrel.Http2.MaxConnectionFlowControlQueueSize";
24+
25+
private static readonly int? ConfiguredMaximumFlowControlQueueSize = GetConfiguredMaximumFlowControlQueueSize();
26+
27+
private static int? GetConfiguredMaximumFlowControlQueueSize()
28+
{
29+
var data = AppContext.GetData(MaximumFlowControlQueueSizeProperty);
30+
31+
if (data is int count)
32+
{
33+
return count;
34+
}
35+
36+
if (data is string countStr && int.TryParse(countStr, out var parsed))
37+
{
38+
return parsed;
39+
}
40+
41+
return null;
42+
}
43+
44+
private readonly int _maximumFlowControlQueueSize;
45+
46+
private bool IsMaximumFlowControlQueueSizeEnabled => _maximumFlowControlQueueSize > 0;
47+
2348
private readonly object _writeLock = new object();
2449
private readonly Http2Frame _outgoingFrame;
2550
private readonly Http2HeadersEnumerator _headersEnumerator = new Http2HeadersEnumerator();
@@ -79,6 +104,16 @@ public Http2FrameWriter(
79104

80105
_hpackEncoder = new DynamicHPackEncoder(serviceContext.ServerOptions.AllowResponseHeaderCompression);
81106

107+
_maximumFlowControlQueueSize = ConfiguredMaximumFlowControlQueueSize is null
108+
? 4 * maxStreamsPerConnection // 4 is a magic number to give us some padding above the expected maximum size
109+
: (int)ConfiguredMaximumFlowControlQueueSize;
110+
111+
if (IsMaximumFlowControlQueueSizeEnabled && _maximumFlowControlQueueSize < maxStreamsPerConnection)
112+
{
113+
_log.Http2FlowControlQueueMaximumTooLow(_connectionContext.ConnectionId, maxStreamsPerConnection, _maximumFlowControlQueueSize);
114+
_maximumFlowControlQueueSize = maxStreamsPerConnection;
115+
}
116+
82117
// This is bounded by the maximum number of concurrent Http2Streams per Http2Connection.
83118
// This isn't the same as SETTINGS_MAX_CONCURRENT_STREAMS, but typically double (with a floor of 100)
84119
// which is the max number of Http2Streams that can end up in the Http2Connection._streams dictionary.
@@ -101,7 +136,8 @@ public void Schedule(Http2OutputProducer producer)
101136
{
102137
if (!_channel.Writer.TryWrite(producer))
103138
{
104-
// It should not be possible to exceed the bound of the channel.
139+
// This can happen if a client resets streams faster than we can clear them out - we end up with a backlog
140+
// exceeding the channel size. Disconnecting seems appropriate in this case.
105141
var ex = new ConnectionAbortedException("HTTP/2 connection exceeded the output operations maximum queue size.");
106142
_log.Http2QueueOperationsExceeded(_connectionId, ex);
107143
_http2Connection.Abort(ex);
@@ -304,7 +340,7 @@ private bool TryQueueProducerForConnectionWindowUpdate(long actual, Http2OutputP
304340
}
305341
else
306342
{
307-
_waitingForMoreConnectionWindow.Enqueue(producer);
343+
EnqueueWaitingForMoreConnectionWindow(producer);
308344
}
309345

310346
return true;
@@ -898,7 +934,7 @@ private void AbortConnectionFlowControl()
898934
_lastWindowConsumer = null;
899935

900936
// Put the consumer of the connection window last
901-
_waitingForMoreConnectionWindow.Enqueue(producer);
937+
EnqueueWaitingForMoreConnectionWindow(producer);
902938
}
903939

904940
while (_waitingForMoreConnectionWindow.TryDequeue(out producer))
@@ -927,7 +963,7 @@ public bool TryUpdateConnectionWindow(int bytes)
927963
_lastWindowConsumer = null;
928964

929965
// Put the consumer of the connection window last
930-
_waitingForMoreConnectionWindow.Enqueue(producer);
966+
EnqueueWaitingForMoreConnectionWindow(producer);
931967
}
932968

933969
while (_waitingForMoreConnectionWindow.TryDequeue(out producer))
@@ -937,4 +973,16 @@ public bool TryUpdateConnectionWindow(int bytes)
937973
}
938974
return true;
939975
}
976+
977+
private void EnqueueWaitingForMoreConnectionWindow(Http2OutputProducer producer)
978+
{
979+
_waitingForMoreConnectionWindow.Enqueue(producer);
980+
// This is re-entrant because abort will cause a final enqueue.
981+
// Easier to check for that condition than to make each enqueuer reason about what to call.
982+
if (!_aborted && IsMaximumFlowControlQueueSizeEnabled && _waitingForMoreConnectionWindow.Count > _maximumFlowControlQueueSize)
983+
{
984+
_log.Http2FlowControlQueueOperationsExceeded(_connectionId, _maximumFlowControlQueueSize);
985+
_http2Connection.Abort(new ConnectionAbortedException("HTTP/2 connection exceeded the outgoing flow control maximum queue size."));
986+
}
987+
}
940988
}

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,21 @@ public void Http2UnexpectedConnectionQueueError(string connectionId, Exception e
8585
Http2Log.Http2UnexpectedConnectionQueueError(_http2Logger, connectionId, ex);
8686
}
8787

88+
public void Http2TooManyEnhanceYourCalms(string connectionId, int count)
89+
{
90+
Http2Log.Http2TooManyEnhanceYourCalms(_http2Logger, connectionId, count);
91+
}
92+
93+
public void Http2FlowControlQueueOperationsExceeded(string connectionId, int count)
94+
{
95+
Http2Log.Http2FlowControlQueueOperationsExceeded(_http2Logger, connectionId, count);
96+
}
97+
98+
public void Http2FlowControlQueueMaximumTooLow(string connectionId, int expected, int actual)
99+
{
100+
Http2Log.Http2FlowControlQueueMaximumTooLow(_http2Logger, connectionId, expected, actual);
101+
}
102+
88103
private static partial class Http2Log
89104
{
90105
[LoggerMessage(29, LogLevel.Debug, @"Connection id ""{ConnectionId}"": HTTP/2 connection error.", EventName = "Http2ConnectionError")]
@@ -130,5 +145,14 @@ private static partial class Http2Log
130145
public static partial void Http2UnexpectedConnectionQueueError(ILogger logger, string connectionId, Exception ex);
131146

132147
// IDs prior to 64 are reserved for back compat (the various KestrelTrace loggers used to share a single sequence)
148+
149+
[LoggerMessage(64, LogLevel.Debug, @"Connection id ""{ConnectionId}"" aborted since at least {Count} ENHANCE_YOUR_CALM responses were recorded per second.", EventName = "Http2TooManyEnhanceYourCalms")]
150+
public static partial void Http2TooManyEnhanceYourCalms(ILogger logger, string connectionId, int count);
151+
152+
[LoggerMessage(65, LogLevel.Debug, @"Connection id ""{ConnectionId}"" exceeded the output flow control maximum queue size of {Count}.", EventName = "Http2FlowControlQueueOperationsExceeded")]
153+
public static partial void Http2FlowControlQueueOperationsExceeded(ILogger logger, string connectionId, int count);
154+
155+
[LoggerMessage(66, LogLevel.Debug, @"Connection id ""{ConnectionId}"" configured maximum flow control queue size {Actual} is less than the maximum streams per connection {Expected}. Increasing configured value to {Expected}.", EventName = "Http2FlowControlQueueMaximumTooLow")]
156+
public static partial void Http2FlowControlQueueMaximumTooLow(ILogger logger, string connectionId, int expected, int actual);
133157
}
134158
}

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>

0 commit comments

Comments
 (0)