Handle Http2Streams that error during stream start (#19386)
This commit is contained in:
parent
bcdee7313a
commit
5b7672c2ad
|
|
@ -65,7 +65,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
private int _gracefulCloseInitiator;
|
private int _gracefulCloseInitiator;
|
||||||
private int _isClosed;
|
private int _isClosed;
|
||||||
|
|
||||||
private Http2StreamStack _streamPool;
|
// Internal for testing
|
||||||
|
internal Http2StreamStack StreamPool;
|
||||||
|
|
||||||
internal const int InitialStreamPoolSize = 5;
|
internal const int InitialStreamPoolSize = 5;
|
||||||
internal const int MaxStreamPoolSize = 40;
|
internal const int MaxStreamPoolSize = 40;
|
||||||
|
|
@ -111,7 +112,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
_serverSettings.InitialWindowSize = (uint)http2Limits.InitialStreamWindowSize;
|
_serverSettings.InitialWindowSize = (uint)http2Limits.InitialStreamWindowSize;
|
||||||
|
|
||||||
// Start pool off at a smaller size if the max number of streams is less than the InitialStreamPoolSize
|
// Start pool off at a smaller size if the max number of streams is less than the InitialStreamPoolSize
|
||||||
_streamPool = new Http2StreamStack(Math.Min(InitialStreamPoolSize, http2Limits.MaxStreamsPerConnection));
|
StreamPool = new Http2StreamStack(Math.Min(InitialStreamPoolSize, http2Limits.MaxStreamsPerConnection));
|
||||||
|
|
||||||
_inputTask = ReadInputAsync();
|
_inputTask = ReadInputAsync();
|
||||||
}
|
}
|
||||||
|
|
@ -572,7 +573,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
|
|
||||||
private Http2Stream GetStream<TContext>(IHttpApplication<TContext> application)
|
private Http2Stream GetStream<TContext>(IHttpApplication<TContext> application)
|
||||||
{
|
{
|
||||||
if (_streamPool.TryPop(out var stream))
|
if (StreamPool.TryPop(out var stream))
|
||||||
{
|
{
|
||||||
stream.InitializeWithExistingContext(_incomingFrame.StreamId);
|
stream.InitializeWithExistingContext(_incomingFrame.StreamId);
|
||||||
return stream;
|
return stream;
|
||||||
|
|
@ -606,9 +607,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
|
|
||||||
private void ReturnStream(Http2Stream stream)
|
private void ReturnStream(Http2Stream stream)
|
||||||
{
|
{
|
||||||
if (_streamPool.Count < MaxStreamPoolSize)
|
if (StreamPool.Count < MaxStreamPoolSize)
|
||||||
{
|
{
|
||||||
_streamPool.Push(stream);
|
StreamPool.Push(stream);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -972,8 +973,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
catch (Http2StreamErrorException)
|
catch (Http2StreamErrorException)
|
||||||
{
|
{
|
||||||
MakeSpaceInDrainQueue();
|
MakeSpaceInDrainQueue();
|
||||||
// Tracked for draining
|
|
||||||
_completedStreams.Enqueue(_currentHeadersStream);
|
// Because this stream isn't being queued, OnRequestProcessingEnded will not be
|
||||||
|
// automatically called and the stream won't be completed.
|
||||||
|
// Manually complete stream to ensure pipes are completed.
|
||||||
|
// Completing the stream will add it to the completed stream queue.
|
||||||
|
_currentHeadersStream.DecrementActiveClientStreamCount();
|
||||||
|
_currentHeadersStream.CompleteStream(errored: true);
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnRequestProcessingEnded()
|
protected override void OnRequestProcessingEnded()
|
||||||
|
{
|
||||||
|
CompleteStream(errored: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CompleteStream(bool errored)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
@ -110,14 +115,23 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
||||||
// If the app finished without reading the request body tell the client not to finish sending it.
|
// If the app finished without reading the request body tell the client not to finish sending it.
|
||||||
if (!EndStreamReceived && !RstStreamReceived)
|
if (!EndStreamReceived && !RstStreamReceived)
|
||||||
{
|
{
|
||||||
Log.RequestBodyNotEntirelyRead(ConnectionIdFeature, TraceIdentifier);
|
if (!errored)
|
||||||
|
{
|
||||||
|
Log.RequestBodyNotEntirelyRead(ConnectionIdFeature, TraceIdentifier);
|
||||||
|
}
|
||||||
|
|
||||||
var (oldState, newState) = ApplyCompletionFlag(StreamCompletionFlags.Aborted);
|
var (oldState, newState) = ApplyCompletionFlag(StreamCompletionFlags.Aborted);
|
||||||
if (oldState != newState)
|
if (oldState != newState)
|
||||||
{
|
{
|
||||||
Debug.Assert(_decrementCalled);
|
Debug.Assert(_decrementCalled);
|
||||||
// Don't block on IO. This never faults.
|
|
||||||
_ = _http2Output.WriteRstStreamAsync(Http2ErrorCode.NO_ERROR);
|
// If there was an error starting the stream then we don't want to write RST_STREAM here.
|
||||||
|
// The connection will handle writing RST_STREAM with the correct error code.
|
||||||
|
if (!errored)
|
||||||
|
{
|
||||||
|
// Don't block on IO. This never faults.
|
||||||
|
_ = _http2Output.WriteRstStreamAsync(Http2ErrorCode.NO_ERROR);
|
||||||
|
}
|
||||||
RequestBodyPipe.Writer.Complete();
|
RequestBodyPipe.Writer.Complete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Connections;
|
using Microsoft.AspNetCore.Connections;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
|
using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2;
|
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2;
|
||||||
|
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
||||||
using Microsoft.AspNetCore.Testing;
|
using Microsoft.AspNetCore.Testing;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Net.Http.Headers;
|
using Microsoft.Net.Http.Headers;
|
||||||
|
|
@ -24,6 +25,202 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
{
|
{
|
||||||
public class Http2ConnectionTests : Http2TestBase
|
public class Http2ConnectionTests : Http2TestBase
|
||||||
{
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task StreamPool_SingleStream_ReturnedToPool()
|
||||||
|
{
|
||||||
|
await InitializeConnectionAsync(_echoApplication);
|
||||||
|
|
||||||
|
Assert.Equal(0, _connection.StreamPool.Count);
|
||||||
|
|
||||||
|
await StartStreamAsync(1, _browserRequestHeaders, endStream: true);
|
||||||
|
|
||||||
|
await ExpectAsync(Http2FrameType.HEADERS,
|
||||||
|
withLength: 55,
|
||||||
|
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
|
||||||
|
withStreamId: 1);
|
||||||
|
|
||||||
|
// Ping will trigger the stream to be returned to the pool so we can assert it
|
||||||
|
await SendPingAsync(Http2PingFrameFlags.NONE);
|
||||||
|
await ExpectAsync(Http2FrameType.PING,
|
||||||
|
withLength: 8,
|
||||||
|
withFlags: (byte)Http2PingFrameFlags.ACK,
|
||||||
|
withStreamId: 0);
|
||||||
|
|
||||||
|
// Stream has been returned to the pool
|
||||||
|
Assert.Equal(1, _connection.StreamPool.Count);
|
||||||
|
|
||||||
|
await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task StreamPool_MultipleStreamsConcurrent_StreamsReturnedToPool()
|
||||||
|
{
|
||||||
|
await InitializeConnectionAsync(_echoApplication);
|
||||||
|
|
||||||
|
Assert.Equal(0, _connection.StreamPool.Count);
|
||||||
|
|
||||||
|
await StartStreamAsync(1, _browserRequestHeaders, endStream: false);
|
||||||
|
await StartStreamAsync(3, _browserRequestHeaders, endStream: false);
|
||||||
|
|
||||||
|
await SendDataAsync(1, _helloBytes, endStream: true);
|
||||||
|
|
||||||
|
await ExpectAsync(Http2FrameType.HEADERS,
|
||||||
|
withLength: 37,
|
||||||
|
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
|
||||||
|
withStreamId: 1);
|
||||||
|
await ExpectAsync(Http2FrameType.DATA,
|
||||||
|
withLength: 5,
|
||||||
|
withFlags: (byte)Http2DataFrameFlags.NONE,
|
||||||
|
withStreamId: 1);
|
||||||
|
await ExpectAsync(Http2FrameType.DATA,
|
||||||
|
withLength: 0,
|
||||||
|
withFlags: (byte)Http2DataFrameFlags.END_STREAM,
|
||||||
|
withStreamId: 1);
|
||||||
|
|
||||||
|
await SendDataAsync(3, _helloBytes, endStream: true);
|
||||||
|
|
||||||
|
await ExpectAsync(Http2FrameType.HEADERS,
|
||||||
|
withLength: 37,
|
||||||
|
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
|
||||||
|
withStreamId: 3);
|
||||||
|
await ExpectAsync(Http2FrameType.DATA,
|
||||||
|
withLength: 5,
|
||||||
|
withFlags: (byte)Http2DataFrameFlags.NONE,
|
||||||
|
withStreamId: 3);
|
||||||
|
await ExpectAsync(Http2FrameType.DATA,
|
||||||
|
withLength: 0,
|
||||||
|
withFlags: (byte)Http2DataFrameFlags.END_STREAM,
|
||||||
|
withStreamId: 3);
|
||||||
|
|
||||||
|
// Ping will trigger the stream to be returned to the pool so we can assert it
|
||||||
|
await SendPingAsync(Http2PingFrameFlags.NONE);
|
||||||
|
await ExpectAsync(Http2FrameType.PING,
|
||||||
|
withLength: 8,
|
||||||
|
withFlags: (byte)Http2PingFrameFlags.ACK,
|
||||||
|
withStreamId: 0);
|
||||||
|
|
||||||
|
// Streams have been returned to the pool
|
||||||
|
Assert.Equal(2, _connection.StreamPool.Count);
|
||||||
|
|
||||||
|
await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task StreamPool_MultipleStreamsInSequence_PooledStreamReused()
|
||||||
|
{
|
||||||
|
await InitializeConnectionAsync(_echoApplication);
|
||||||
|
|
||||||
|
Assert.Equal(0, _connection.StreamPool.Count);
|
||||||
|
|
||||||
|
await StartStreamAsync(1, _browserRequestHeaders, endStream: true);
|
||||||
|
|
||||||
|
await ExpectAsync(Http2FrameType.HEADERS,
|
||||||
|
withLength: 55,
|
||||||
|
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
|
||||||
|
withStreamId: 1);
|
||||||
|
|
||||||
|
// Ping will trigger the stream to be returned to the pool so we can assert it
|
||||||
|
await SendPingAsync(Http2PingFrameFlags.NONE);
|
||||||
|
await ExpectAsync(Http2FrameType.PING,
|
||||||
|
withLength: 8,
|
||||||
|
withFlags: (byte)Http2PingFrameFlags.ACK,
|
||||||
|
withStreamId: 0);
|
||||||
|
|
||||||
|
// Stream has been returned to the pool
|
||||||
|
Assert.Equal(1, _connection.StreamPool.Count);
|
||||||
|
|
||||||
|
await StartStreamAsync(3, _browserRequestHeaders, endStream: true);
|
||||||
|
|
||||||
|
await ExpectAsync(Http2FrameType.HEADERS,
|
||||||
|
withLength: 55,
|
||||||
|
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
|
||||||
|
withStreamId: 3);
|
||||||
|
|
||||||
|
// New stream has been taken from the pool
|
||||||
|
Assert.Equal(0, _connection.StreamPool.Count);
|
||||||
|
|
||||||
|
// Ping will trigger the stream to be returned to the pool so we can assert it
|
||||||
|
await SendPingAsync(Http2PingFrameFlags.NONE);
|
||||||
|
await ExpectAsync(Http2FrameType.PING,
|
||||||
|
withLength: 8,
|
||||||
|
withFlags: (byte)Http2PingFrameFlags.ACK,
|
||||||
|
withStreamId: 0);
|
||||||
|
|
||||||
|
// Stream was reused and returned to the pool
|
||||||
|
Assert.Equal(1, _connection.StreamPool.Count);
|
||||||
|
|
||||||
|
await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task StreamPool_EndedStreamErrorsOnStart_ReturnedToPool()
|
||||||
|
{
|
||||||
|
await InitializeConnectionAsync(_echoApplication);
|
||||||
|
|
||||||
|
_connection.ServerSettings.MaxConcurrentStreams = 1;
|
||||||
|
|
||||||
|
await StartStreamAsync(1, _browserRequestHeaders, endStream: false);
|
||||||
|
|
||||||
|
// This stream will error because it exceeds max concurrent streams
|
||||||
|
await StartStreamAsync(3, _browserRequestHeaders, endStream: true);
|
||||||
|
await WaitForStreamErrorAsync(3, Http2ErrorCode.REFUSED_STREAM, CoreStrings.Http2ErrorMaxStreams);
|
||||||
|
|
||||||
|
// Ping will trigger the stream to be returned to the pool so we can assert it
|
||||||
|
await SendPingAsync(Http2PingFrameFlags.NONE);
|
||||||
|
await ExpectAsync(Http2FrameType.PING,
|
||||||
|
withLength: 8,
|
||||||
|
withFlags: (byte)Http2PingFrameFlags.ACK,
|
||||||
|
withStreamId: 0);
|
||||||
|
|
||||||
|
// Stream returned to the pool
|
||||||
|
Assert.Equal(1, _connection.StreamPool.Count);
|
||||||
|
|
||||||
|
Assert.True(_connection.StreamPool.TryPop(out var stream));
|
||||||
|
|
||||||
|
// Stream has been completed and reset before being returned
|
||||||
|
Assert.Empty(stream.RequestHeaders);
|
||||||
|
|
||||||
|
await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task StreamPool_UnendedStreamErrorsOnStart_ReturnedToPool()
|
||||||
|
{
|
||||||
|
_serviceContext.ServerOptions.Limits.MinRequestBodyDataRate = null;
|
||||||
|
|
||||||
|
await InitializeConnectionAsync(_echoApplication);
|
||||||
|
|
||||||
|
_connection.ServerSettings.MaxConcurrentStreams = 1;
|
||||||
|
|
||||||
|
await StartStreamAsync(1, _browserRequestHeaders, endStream: false);
|
||||||
|
|
||||||
|
// This stream will error because it exceeds max concurrent streams
|
||||||
|
await StartStreamAsync(3, _browserRequestHeaders, endStream: false);
|
||||||
|
await WaitForStreamErrorAsync(3, Http2ErrorCode.REFUSED_STREAM, CoreStrings.Http2ErrorMaxStreams);
|
||||||
|
|
||||||
|
// Ping will trigger the stream to be returned to the pool so we can assert it
|
||||||
|
await SendPingAsync(Http2PingFrameFlags.NONE);
|
||||||
|
await ExpectAsync(Http2FrameType.PING,
|
||||||
|
withLength: 8,
|
||||||
|
withFlags: (byte)Http2PingFrameFlags.ACK,
|
||||||
|
withStreamId: 0);
|
||||||
|
|
||||||
|
AdvanceClock(TimeSpan.FromTicks(Constants.RequestBodyDrainTimeout.Ticks + 1));
|
||||||
|
|
||||||
|
// Ping will trigger the stream to attempt to be returned to the pool
|
||||||
|
await SendPingAsync(Http2PingFrameFlags.NONE);
|
||||||
|
await ExpectAsync(Http2FrameType.PING,
|
||||||
|
withLength: 8,
|
||||||
|
withFlags: (byte)Http2PingFrameFlags.ACK,
|
||||||
|
withStreamId: 0);
|
||||||
|
|
||||||
|
// Stream was returned to the pool because of the drain timeout
|
||||||
|
Assert.Equal(1, _connection.StreamPool.Count);
|
||||||
|
|
||||||
|
await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Frame_Received_OverMaxSize_FrameError()
|
public async Task Frame_Received_OverMaxSize_FrameError()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue