Decrement activeStreamCount earlier s.t. client view matches the server. (#12704)

This commit is contained in:
Justin Kotalik 2019-07-31 07:45:36 -07:00 committed by GitHub
parent 7a0a286ce6
commit aab75e8dda
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 157 additions and 37 deletions

View File

@ -614,4 +614,7 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
<data name="Http2StreamResetByApplication" xml:space="preserve"> <data name="Http2StreamResetByApplication" xml:space="preserve">
<value>The HTTP/2 stream was reset by the application with error code {errorCode}.</value> <value>The HTTP/2 stream was reset by the application with error code {errorCode}.</value>
</data> </data>
<data name="Http2TellClientToCalmDown" xml:space="preserve">
<value>A new stream was refused because this connection has too many streams that haven't finished processing. This may happen if many streams are aborted but not yet cleaned up.</value>
</data>
</root> </root>

View File

@ -64,7 +64,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
private bool _gracefulCloseStarted; private bool _gracefulCloseStarted;
private readonly Dictionary<int, Http2Stream> _streams = new Dictionary<int, Http2Stream>(); private readonly Dictionary<int, Http2Stream> _streams = new Dictionary<int, Http2Stream>();
private int _activeStreamCount = 0; private int _clientActiveStreamCount = 0;
private int _serverActiveStreamCount = 0;
// The following are the only fields that can be modified outside of the ProcessRequestsAsync loop. // The following are the only fields that can be modified outside of the ProcessRequestsAsync loop.
private readonly ConcurrentQueue<Http2Stream> _completedStreams = new ConcurrentQueue<Http2Stream>(); private readonly ConcurrentQueue<Http2Stream> _completedStreams = new ConcurrentQueue<Http2Stream>();
@ -219,6 +220,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
Log.Http2StreamError(ConnectionId, ex); Log.Http2StreamError(ConnectionId, ex);
// The client doesn't know this error is coming, allow draining additional frames for now. // The client doesn't know this error is coming, allow draining additional frames for now.
AbortStream(_incomingFrame.StreamId, new IOException(ex.Message, ex)); AbortStream(_incomingFrame.StreamId, new IOException(ex.Message, ex));
await _frameWriter.WriteRstStreamAsync(ex.StreamId, ex.ErrorCode); await _frameWriter.WriteRstStreamAsync(ex.StreamId, ex.ErrorCode);
} }
finally finally
@ -232,7 +234,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
catch (ConnectionResetException ex) catch (ConnectionResetException ex)
{ {
// Don't log ECONNRESET errors when there are no active streams on the connection. Browsers like IE will reset connections regularly. // Don't log ECONNRESET errors when there are no active streams on the connection. Browsers like IE will reset connections regularly.
if (_activeStreamCount > 0) if (_clientActiveStreamCount > 0)
{ {
Log.RequestProcessingError(ConnectionId, ex); Log.RequestProcessingError(ConnectionId, ex);
} }
@ -287,7 +289,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
stream.Abort(new IOException(CoreStrings.Http2StreamAborted, connectionError)); stream.Abort(new IOException(CoreStrings.Http2StreamAborted, connectionError));
} }
while (_activeStreamCount > 0) // Use the server _serverActiveStreamCount to drain all requests on the server side.
// Can't use _clientActiveStreamCount now as we now decrement that count earlier/
// Can't use _streams.Count as we wait for RST/END_STREAM before removing the stream from the dictionary
while (_serverActiveStreamCount > 0)
{ {
await _streamCompletionAwaitable; await _streamCompletionAwaitable;
UpdateCompletedStreams(); UpdateCompletedStreams();
@ -897,11 +902,21 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
throw new Http2StreamErrorException(_currentHeadersStream.StreamId, CoreStrings.Http2ErrorMissingMandatoryPseudoHeaderFields, Http2ErrorCode.PROTOCOL_ERROR); throw new Http2StreamErrorException(_currentHeadersStream.StreamId, CoreStrings.Http2ErrorMissingMandatoryPseudoHeaderFields, Http2ErrorCode.PROTOCOL_ERROR);
} }
if (_activeStreamCount >= _serverSettings.MaxConcurrentStreams) if (_clientActiveStreamCount >= _serverSettings.MaxConcurrentStreams)
{ {
throw new Http2StreamErrorException(_currentHeadersStream.StreamId, CoreStrings.Http2ErrorMaxStreams, Http2ErrorCode.REFUSED_STREAM); throw new Http2StreamErrorException(_currentHeadersStream.StreamId, CoreStrings.Http2ErrorMaxStreams, Http2ErrorCode.REFUSED_STREAM);
} }
// We don't use the _serverActiveRequestCount here as during shutdown, it and the dictionary
// counts get out of sync during shutdown. The streams still exist in the dictionary until the client responds with a RST or END_STREAM.
// Also, we care about the dictionary size for too much memory consumption.
if (_streams.Count >= _serverSettings.MaxConcurrentStreams * 2)
{
// Server is getting hit hard with connection resets.
// Tell client to calm down.
// TODO consider making when to send ENHANCE_YOUR_CALM configurable?
throw new Http2StreamErrorException(_currentHeadersStream.StreamId, CoreStrings.Http2TellClientToCalmDown, Http2ErrorCode.ENHANCE_YOUR_CALM);
}
// This must be initialized before we offload the request or else we may start processing request body frames without it. // This must be initialized before we offload the request or else we may start processing request body frames without it.
_currentHeadersStream.InputRemaining = _currentHeadersStream.RequestHeaders.ContentLength; _currentHeadersStream.InputRemaining = _currentHeadersStream.RequestHeaders.ContentLength;
@ -911,8 +926,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
_currentHeadersStream.OnEndStreamReceived(); _currentHeadersStream.OnEndStreamReceived();
} }
_activeStreamCount++;
_streams[_incomingFrame.StreamId] = _currentHeadersStream; _streams[_incomingFrame.StreamId] = _currentHeadersStream;
IncrementActiveClientStreamCount();
_serverActiveStreamCount++;
// Must not allow app code to block the connection handling loop. // Must not allow app code to block the connection handling loop.
ThreadPool.UnsafeQueueUserWorkItem(_currentHeadersStream, preferLocal: false); ThreadPool.UnsafeQueueUserWorkItem(_currentHeadersStream, preferLocal: false);
} }
@ -950,6 +966,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
{ {
if (_streams.TryGetValue(streamId, out var stream)) if (_streams.TryGetValue(streamId, out var stream))
{ {
stream.DecrementActiveClientStreamCount();
stream.Abort(error); stream.Abort(error);
} }
} }
@ -982,8 +999,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
if (stream.DrainExpirationTicks == default) if (stream.DrainExpirationTicks == default)
{ {
// This is our first time checking this stream. _serverActiveStreamCount--;
_activeStreamCount--;
stream.DrainExpirationTicks = now + Constants.RequestBodyDrainTimeout.Ticks; stream.DrainExpirationTicks = now + Constants.RequestBodyDrainTimeout.Ticks;
} }
@ -1022,13 +1038,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
Log.Http2ConnectionClosing(_context.ConnectionId); Log.Http2ConnectionClosing(_context.ConnectionId);
if (_gracefulCloseInitiator == GracefulCloseInitiator.Server && _activeStreamCount > 0) if (_gracefulCloseInitiator == GracefulCloseInitiator.Server && _clientActiveStreamCount > 0)
{ {
_frameWriter.WriteGoAwayAsync(int.MaxValue, Http2ErrorCode.NO_ERROR); _frameWriter.WriteGoAwayAsync(int.MaxValue, Http2ErrorCode.NO_ERROR);
} }
} }
if (_activeStreamCount == 0) if (_clientActiveStreamCount == 0)
{ {
if (_gracefulCloseStarted) if (_gracefulCloseStarted)
{ {
@ -1235,6 +1251,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
return false; return false;
} }
public void IncrementActiveClientStreamCount()
{
Interlocked.Increment(ref _clientActiveStreamCount);
}
public void DecrementActiveClientStreamCount()
{
Interlocked.Decrement(ref _clientActiveStreamCount);
}
private class StreamCloseAwaitable : ICriticalNotifyCompletion private class StreamCloseAwaitable : ICriticalNotifyCompletion
{ {
private static readonly Action _callbackCompleted = () => { }; private static readonly Action _callbackCompleted = () => { };

View File

@ -164,6 +164,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
if (appCompleted && !_startedWritingDataFrames && (_stream.ResponseTrailers == null || _stream.ResponseTrailers.Count == 0)) if (appCompleted && !_startedWritingDataFrames && (_stream.ResponseTrailers == null || _stream.ResponseTrailers.Count == 0))
{ {
_streamEnded = true; _streamEnded = true;
_stream.DecrementActiveClientStreamCount();
http2HeadersFrame = Http2HeadersFrameFlags.END_STREAM; http2HeadersFrame = Http2HeadersFrameFlags.END_STREAM;
} }
else else
@ -378,6 +379,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
} }
_stream.ResponseTrailers.SetReadOnly(); _stream.ResponseTrailers.SetReadOnly();
_stream.DecrementActiveClientStreamCount();
flushResult = await _frameWriter.WriteResponseTrailers(_streamId, _stream.ResponseTrailers); flushResult = await _frameWriter.WriteResponseTrailers(_streamId, _stream.ResponseTrailers);
} }
else if (readResult.IsCompleted && _streamEnded) else if (readResult.IsCompleted && _streamEnded)
@ -392,7 +394,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
} }
else else
{ {
flushResult = await _frameWriter.WriteDataAsync(_streamId, _flowControl, readResult.Buffer, endStream: readResult.IsCompleted); var endStream = readResult.IsCompleted;
if (endStream)
{
_stream.DecrementActiveClientStreamCount();
}
flushResult = await _frameWriter.WriteDataAsync(_streamId, _flowControl, readResult.Buffer, endStream);
} }
_pipeReader.AdvanceTo(readResult.Buffer.End); _pipeReader.AdvanceTo(readResult.Buffer.End);

View File

@ -24,6 +24,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
private readonly StreamInputFlowControl _inputFlowControl; private readonly StreamInputFlowControl _inputFlowControl;
private readonly StreamOutputFlowControl _outputFlowControl; private readonly StreamOutputFlowControl _outputFlowControl;
private bool _decrementCalled;
public Pipe RequestBodyPipe { get; } public Pipe RequestBodyPipe { get; }
internal long DrainExpirationTicks { get; set; } internal long DrainExpirationTicks { get; set; }
@ -97,6 +98,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
var (oldState, newState) = ApplyCompletionFlag(StreamCompletionFlags.Aborted); var (oldState, newState) = ApplyCompletionFlag(StreamCompletionFlags.Aborted);
if (oldState != newState) if (oldState != newState)
{ {
Debug.Assert(!_decrementCalled);
// Don't block on IO. This never faults. // Don't block on IO. This never faults.
_ = _http2Output.WriteRstStreamAsync(Http2ErrorCode.NO_ERROR); _ = _http2Output.WriteRstStreamAsync(Http2ErrorCode.NO_ERROR);
RequestBodyPipe.Writer.Complete(); RequestBodyPipe.Writer.Complete();
@ -419,6 +421,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
public void AbortRstStreamReceived() public void AbortRstStreamReceived()
{ {
// Client sent a reset stream frame, decrement total count.
DecrementActiveClientStreamCount();
ApplyCompletionFlag(StreamCompletionFlags.RstStreamReceived); ApplyCompletionFlag(StreamCompletionFlags.RstStreamReceived);
Abort(new IOException(CoreStrings.Http2StreamResetByClient)); Abort(new IOException(CoreStrings.Http2StreamResetByClient));
} }
@ -460,6 +465,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
Log.Http2StreamResetAbort(TraceIdentifier, error, abortReason); Log.Http2StreamResetAbort(TraceIdentifier, error, abortReason);
DecrementActiveClientStreamCount();
// Don't block on IO. This never faults. // Don't block on IO. This never faults.
_ = _http2Output.WriteRstStreamAsync(error); _ = _http2Output.WriteRstStreamAsync(error);
@ -481,6 +487,23 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
_inputFlowControl.Abort(); _inputFlowControl.Abort();
} }
public void DecrementActiveClientStreamCount()
{
// Decrement can be called twice, via calling CompleteAsync and then Abort on the HttpContext.
// Only decrement once total.
lock (_completionLock)
{
if (_decrementCalled)
{
return;
}
_decrementCalled = true;
}
_context.StreamLifetimeHandler.DecrementActiveClientStreamCount();
}
private Pipe CreateRequestBodyPipe(uint windowSize) private Pipe CreateRequestBodyPipe(uint windowSize)
=> new Pipe(new PipeOptions => new Pipe(new PipeOptions
( (

View File

@ -6,5 +6,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
internal interface IHttp2StreamLifetimeHandler internal interface IHttp2StreamLifetimeHandler
{ {
void OnStreamCompleted(Http2Stream stream); void OnStreamCompleted(Http2Stream stream);
void DecrementActiveClientStreamCount();
} }
} }

View File

@ -916,6 +916,66 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
}); });
} }
[Fact]
public async Task Frame_MultipleStreams_CanBeCreatedIfClientCountIsLessThanActualMaxStreamCount()
{
_serviceContext.ServerOptions.Limits.Http2.MaxStreamsPerConnection = 1;
var firstRequestBlock = new TaskCompletionSource<object>();
var firstRequestReceived = new TaskCompletionSource<object>();
var makeFirstRequestWait = false;
await InitializeConnectionAsync(async context =>
{
if (!makeFirstRequestWait)
{
makeFirstRequestWait = true;
firstRequestReceived.SetResult(null);
await firstRequestBlock.Task.DefaultTimeout();
}
});
await StartStreamAsync(1, _browserRequestHeaders, endStream: true);
await SendRstStreamAsync(1);
await firstRequestReceived.Task.DefaultTimeout();
await StartStreamAsync(3, _browserRequestHeaders, endStream: true);
await ExpectAsync(Http2FrameType.HEADERS,
withLength: 55,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 3);
firstRequestBlock.SetResult(null);
await StopConnectionAsync(3, ignoreNonGoAwayFrames: false);
}
[Fact]
public async Task Frame_MultipleStreams_RequestsNotFinished_EnhanceYourCalm()
{
_serviceContext.ServerOptions.Limits.Http2.MaxStreamsPerConnection = 1;
var tcs = new TaskCompletionSource<object>();
await InitializeConnectionAsync(async context =>
{
await tcs.Task.DefaultTimeout();
});
await StartStreamAsync(1, _browserRequestHeaders, endStream: false);
await SendRstStreamAsync(1);
await StartStreamAsync(3, _browserRequestHeaders, endStream: true);
await SendRstStreamAsync(3);
await StartStreamAsync(5, _browserRequestHeaders, endStream: true);
await WaitForStreamErrorAsync(
expectedStreamId: 5,
expectedErrorCode: Http2ErrorCode.ENHANCE_YOUR_CALM,
expectedErrorMessage: CoreStrings.Http2TellClientToCalmDown);
tcs.SetResult(null);
await StopConnectionAsync(5, ignoreNonGoAwayFrames: false);
}
[Fact] [Fact]
public async Task DATA_Received_StreamClosedImplicitly_ConnectionError() public async Task DATA_Received_StreamClosedImplicitly_ConnectionError()
{ {