Skip to content

Commit 1500772

Browse files
Merged PR 33980: 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 !33914
2 parents 23915d9 + 90be960 commit 1500772

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 const long StreamPoolExpiryTicks = TimeSpan.TicksPerSecond * 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 Http2FrameWriter _frameWriter;
5176
private readonly Pipe _input;
@@ -74,6 +99,9 @@ internal sealed partial class Http2Connection : IHttp2StreamLifetimeHandler, IHt
7499
private int _clientActiveStreamCount;
75100
private int _serverActiveStreamCount;
76101

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

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

373408
while (StreamPool.TryPop(out var pooledStream))
@@ -1170,6 +1205,20 @@ private void StartStream()
11701205
// Server is getting hit hard with connection resets.
11711206
// Tell client to calm down.
11721207
// TODO consider making when to send ENHANCE_YOUR_CALM configurable?
1208+
1209+
if (IsEnhanceYourCalmEnabled && Interlocked.Increment(ref _enhanceYourCalmCount) > EnhanceYourCalmTickWindowCount * _enhanceYourCalmMaximumCount)
1210+
{
1211+
Log.Http2TooManyEnhanceYourCalms(_context.ConnectionId, _enhanceYourCalmMaximumCount);
1212+
1213+
// Now that we've logged a useful message, we can put vague text in the exception
1214+
// messages in case they somehow make it back to the client (not expected)
1215+
1216+
// This will close the socket - we want to do that right away
1217+
Abort(new ConnectionAbortedException(CoreStrings.Http2ConnectionFaulted));
1218+
// Throwing an exception as well will help us clean up on our end more quickly by (e.g.) skipping processing of already-buffered input
1219+
throw new Http2ConnectionErrorException(CoreStrings.Http2ConnectionFaulted, Http2ErrorCode.ENHANCE_YOUR_CALM);
1220+
}
1221+
11731222
throw new Http2StreamErrorException(_currentHeadersStream.StreamId, CoreStrings.Http2TellClientToCalmDown, Http2ErrorCode.ENHANCE_YOUR_CALM);
11741223
}
11751224
}
@@ -1241,6 +1290,12 @@ private void AbortStream(int streamId, IOException error)
12411290
void IRequestProcessor.Tick(DateTimeOffset now)
12421291
{
12431292
Input.CancelPendingRead();
1293+
// We count EYCs over a window of a given length to avoid flagging short-lived bursts.
1294+
// At the end of each window, reset the count.
1295+
if (IsEnhanceYourCalmEnabled && ++_tickCount % EnhanceYourCalmTickWindowCount == 0)
1296+
{
1297+
Interlocked.Exchange(ref _enhanceYourCalmCount, 0);
1298+
}
12441299
}
12451300

12461301
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
// Highest shared ID is 63. New consecutive IDs start at 64
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)