Skip to content

Commit 6c6d304

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 23915d9 commit 6c6d304

File tree

4 files changed

+121
-11
lines changed

4 files changed

+121
-11
lines changed

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

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,18 @@ 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 EnhanceYourCalmMaximumCountProperty = "Microsoft.AspNetCore.Server.Kestrel.Http2.EnhanceYourCalmCount";
50+
51+
private static readonly int _enhanceYourCalmMaximumCount = AppContext.GetData(EnhanceYourCalmMaximumCountProperty) is int eycMaxCount
52+
? eycMaxCount
53+
: 10;
54+
55+
// Accumulate _enhanceYourCalmCount over the course of EnhanceYourCalmTickWindowCount ticks.
56+
// This should make bursts less likely to trigger disconnects.
57+
private const int EnhanceYourCalmTickWindowCount = 5;
58+
59+
private static bool IsEnhanceYourCalmEnabled => _enhanceYourCalmMaximumCount > 0;
60+
4961
private readonly HttpConnectionContext _context;
5062
private readonly Http2FrameWriter _frameWriter;
5163
private readonly Pipe _input;
@@ -74,6 +86,9 @@ internal sealed partial class Http2Connection : IHttp2StreamLifetimeHandler, IHt
7486
private int _clientActiveStreamCount;
7587
private int _serverActiveStreamCount;
7688

89+
private int _enhanceYourCalmCount;
90+
private int _tickCount;
91+
7792
// The following are the only fields that can be modified outside of the ProcessRequestsAsync loop.
7893
private readonly ConcurrentQueue<Http2Stream> _completedStreams = new ConcurrentQueue<Http2Stream>();
7994
private readonly StreamCloseAwaitable _streamCompletionAwaitable = new StreamCloseAwaitable();
@@ -361,13 +376,20 @@ public async Task ProcessRequestsAsync<TContext>(IHttpApplication<TContext> appl
361376
stream.Abort(new IOException(CoreStrings.Http2StreamAborted, connectionError));
362377
}
363378

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)
379+
// For some reason, this loop doesn't terminate when we're trying to abort.
380+
// Since we're making a narrow fix for a patch, we'll bypass it in such scenarios.
381+
// TODO: This is probably a bug - something in here should probably detect aborted
382+
// connections and short-circuit.
383+
if (!IsEnhanceYourCalmEnabled || error is not Http2ConnectionErrorException)
368384
{
369-
await _streamCompletionAwaitable;
370-
UpdateCompletedStreams();
385+
// Use the server _serverActiveStreamCount to drain all requests on the server side.
386+
// Can't use _clientActiveStreamCount now as we now decrement that count earlier/
387+
// Can't use _streams.Count as we wait for RST/END_STREAM before removing the stream from the dictionary
388+
while (_serverActiveStreamCount > 0)
389+
{
390+
await _streamCompletionAwaitable;
391+
UpdateCompletedStreams();
392+
}
371393
}
372394

373395
while (StreamPool.TryPop(out var pooledStream))
@@ -1170,6 +1192,20 @@ private void StartStream()
11701192
// Server is getting hit hard with connection resets.
11711193
// Tell client to calm down.
11721194
// TODO consider making when to send ENHANCE_YOUR_CALM configurable?
1195+
1196+
if (IsEnhanceYourCalmEnabled && Interlocked.Increment(ref _enhanceYourCalmCount) > EnhanceYourCalmTickWindowCount * _enhanceYourCalmMaximumCount)
1197+
{
1198+
Log.Http2TooManyEnhanceYourCalms(_context.ConnectionId, _enhanceYourCalmMaximumCount);
1199+
1200+
// Now that we've logged a useful message, we can put vague text in the exception
1201+
// messages in case they somehow make it back to the client (not expected)
1202+
1203+
// This will close the socket - we want to do that right away
1204+
Abort(new ConnectionAbortedException(CoreStrings.Http2ConnectionFaulted));
1205+
// Throwing an exception as well will help us clean up on our end more quickly by (e.g.) skipping processing of already-buffered input
1206+
throw new Http2ConnectionErrorException(CoreStrings.Http2ConnectionFaulted, Http2ErrorCode.ENHANCE_YOUR_CALM);
1207+
}
1208+
11731209
throw new Http2StreamErrorException(_currentHeadersStream.StreamId, CoreStrings.Http2TellClientToCalmDown, Http2ErrorCode.ENHANCE_YOUR_CALM);
11741210
}
11751211
}
@@ -1241,6 +1277,10 @@ private void AbortStream(int streamId, IOException error)
12411277
void IRequestProcessor.Tick(DateTimeOffset now)
12421278
{
12431279
Input.CancelPendingRead();
1280+
if (IsEnhanceYourCalmEnabled && ++_tickCount % EnhanceYourCalmTickWindowCount == 0)
1281+
{
1282+
Interlocked.Exchange(ref _enhanceYourCalmCount, 0);
1283+
}
12441284
}
12451285

12461286
void IHttp2StreamLifetimeHandler.OnStreamCompleted(Http2Stream stream)

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

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,29 @@ 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+
2346
private readonly object _writeLock = new object();
2447
private readonly Http2Frame _outgoingFrame;
2548
private readonly Http2HeadersEnumerator _headersEnumerator = new Http2HeadersEnumerator();
@@ -79,6 +102,16 @@ public Http2FrameWriter(
79102

80103
_hpackEncoder = new DynamicHPackEncoder(serviceContext.ServerOptions.AllowResponseHeaderCompression);
81104

105+
_maximumFlowControlQueueSize = ConfiguredMaximumFlowControlQueueSize is null
106+
? 4 * maxStreamsPerConnection
107+
: (int)ConfiguredMaximumFlowControlQueueSize;
108+
109+
if (_maximumFlowControlQueueSize < maxStreamsPerConnection)
110+
{
111+
_log.Http2FlowControlQueueMaximumTooLow(_connectionContext.ConnectionId, maxStreamsPerConnection, _maximumFlowControlQueueSize);
112+
_maximumFlowControlQueueSize = maxStreamsPerConnection;
113+
}
114+
82115
// This is bounded by the maximum number of concurrent Http2Streams per Http2Connection.
83116
// This isn't the same as SETTINGS_MAX_CONCURRENT_STREAMS, but typically double (with a floor of 100)
84117
// which is the max number of Http2Streams that can end up in the Http2Connection._streams dictionary.
@@ -101,7 +134,8 @@ public void Schedule(Http2OutputProducer producer)
101134
{
102135
if (!_channel.Writer.TryWrite(producer))
103136
{
104-
// It should not be possible to exceed the bound of the channel.
137+
// This can happen if a client resets streams faster than we can clear them out - we end up with a backlog
138+
// exceeding the channel size. Disconnecting seems appropriate in this case.
105139
var ex = new ConnectionAbortedException("HTTP/2 connection exceeded the output operations maximum queue size.");
106140
_log.Http2QueueOperationsExceeded(_connectionId, ex);
107141
_http2Connection.Abort(ex);
@@ -304,7 +338,7 @@ private bool TryQueueProducerForConnectionWindowUpdate(long actual, Http2OutputP
304338
}
305339
else
306340
{
307-
_waitingForMoreConnectionWindow.Enqueue(producer);
341+
EnqueueWaitingForMoreConnectionWindow(producer);
308342
}
309343

310344
return true;
@@ -898,7 +932,7 @@ private void AbortConnectionFlowControl()
898932
_lastWindowConsumer = null;
899933

900934
// Put the consumer of the connection window last
901-
_waitingForMoreConnectionWindow.Enqueue(producer);
935+
EnqueueWaitingForMoreConnectionWindow(producer);
902936
}
903937

904938
while (_waitingForMoreConnectionWindow.TryDequeue(out producer))
@@ -927,7 +961,7 @@ public bool TryUpdateConnectionWindow(int bytes)
927961
_lastWindowConsumer = null;
928962

929963
// Put the consumer of the connection window last
930-
_waitingForMoreConnectionWindow.Enqueue(producer);
964+
EnqueueWaitingForMoreConnectionWindow(producer);
931965
}
932966

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

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.Error, @"Connection id ""{ConnectionId}"" aborted since at least ""{Count}"" ENHANCE_YOUR_CALM responses were required per second.", EventName = "Http2TooManyEnhanceYourCalms")]
150+
public static partial void Http2TooManyEnhanceYourCalms(ILogger logger, string connectionId, int count);
151+
152+
[LoggerMessage(65, LogLevel.Error, @"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.Trace, @"Connection id ""{ConnectionId}"" configured maximum flow control queue size ""{Actual}"" is less than the maximum streams per connection ""{Expected}"" - increasing to match.", 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)