[Kestrel] Allow HTTP/2 stream drain timeout during trailers (#4070)

- Move all HTTP/2 stream state management into request parsing loop
This commit is contained in:
Stephen Halter 2019-01-03 15:03:00 -08:00 committed by GitHub
parent bfec2c14be
commit b1f778bfb8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 283 additions and 253 deletions

View File

@ -4,9 +4,11 @@
using System;
using System.Buffers;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Pipelines;
using System.Runtime.CompilerServices;
using System.Security.Authentication;
using System.Text;
using System.Threading;
@ -26,26 +28,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
{
public class Http2Connection : IHttp2StreamLifetimeHandler, IHttpHeadersHandler, IRequestProcessor
{
private enum RequestHeaderParsingState
{
Ready,
PseudoHeaderFields,
Headers,
Trailers
}
[Flags]
private enum PseudoHeaderFields
{
None = 0x0,
Authority = 0x1,
Method = 0x2,
Path = 0x4,
Scheme = 0x8,
Status = 0x10,
Unknown = 0x40000000
}
public static byte[] ClientPreface { get; } = Encoding.ASCII.GetBytes("PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n");
private static readonly PseudoHeaderFields _mandatoryRequestPseudoHeaderFields =
@ -78,15 +60,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
private Http2HeadersFrameFlags _headerFlags;
private int _totalParsedHeaderSize;
private bool _isMethodConnect;
private readonly object _stateLock = new object();
private int _highestOpenedStreamId;
private Http2ConnectionState _state = Http2ConnectionState.Open;
private readonly TaskCompletionSource<object> _streamsCompleted = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
private bool _gracefulCloseStarted;
private readonly ConcurrentDictionary<int, Http2Stream> _streams = new ConcurrentDictionary<int, Http2Stream>();
private readonly ConcurrentDictionary<int, Http2Stream> _drainingStreams = new ConcurrentDictionary<int, Http2Stream>();
private readonly Dictionary<int, Http2Stream> _streams = new Dictionary<int, Http2Stream>();
private int _activeStreamCount = 0;
// 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 StreamCloseAwaitable _streamCompletionAwaitable = new StreamCloseAwaitable();
private int _gracefulCloseInitiator;
private int _isClosed;
public Http2Connection(HttpConnectionContext context)
{
var httpLimits = context.ServiceContext.ServerOptions.Limits;
@ -120,6 +105,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
public PipeReader Input => _context.Transport.Input;
public IKestrelTrace Log => _context.ServiceContext.Log;
public IFeatureCollection ConnectionFeatures => _context.ConnectionFeatures;
public ISystemClock SystemClock => _context.ServiceContext.SystemClock;
public ITimeoutControl TimeoutControl => _context.TimeoutControl;
public KestrelServerLimits Limits => _context.ServiceContext.ServerOptions.Limits;
@ -127,33 +113,22 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
public void OnInputOrOutputCompleted()
{
lock (_stateLock)
{
if (_state != Http2ConnectionState.Closed)
{
UpdateState(Http2ConnectionState.Closed);
}
}
TryClose();
_frameWriter.Abort(new ConnectionAbortedException(CoreStrings.ConnectionAbortedByClient));
}
public void Abort(ConnectionAbortedException ex)
{
lock (_stateLock)
if (TryClose())
{
if (_state != Http2ConnectionState.Closed)
{
_frameWriter.WriteGoAwayAsync(_highestOpenedStreamId, Http2ErrorCode.INTERNAL_ERROR);
UpdateState(Http2ConnectionState.Closed);
}
_frameWriter.WriteGoAwayAsync(int.MaxValue, Http2ErrorCode.INTERNAL_ERROR);
}
_frameWriter.Abort(ex);
}
public void StopProcessingNextRequest()
=> StopProcessingNextRequest(true);
=> StopProcessingNextRequest(serverInitiated: true);
public void HandleRequestHeadersTimeout()
{
@ -167,30 +142,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
Abort(new ConnectionAbortedException(CoreStrings.BadRequest_RequestBodyTimeout));
}
public void StopProcessingNextRequest(bool sendGracefulGoAway = false)
public void StopProcessingNextRequest(bool serverInitiated)
{
lock (_stateLock)
var initiator = serverInitiated ? GracefulCloseInitiator.Server : GracefulCloseInitiator.Client;
if (Interlocked.CompareExchange(ref _gracefulCloseInitiator, initiator, GracefulCloseInitiator.None) == GracefulCloseInitiator.None)
{
if (_state == Http2ConnectionState.Open)
{
if (_activeStreamCount == 0)
{
_frameWriter.WriteGoAwayAsync(_highestOpenedStreamId, Http2ErrorCode.NO_ERROR);
UpdateState(Http2ConnectionState.Closed);
// Wake up request processing loop so the connection can complete if there are no pending requests
Input.CancelPendingRead();
}
else
{
if (sendGracefulGoAway)
{
_frameWriter.WriteGoAwayAsync(Int32.MaxValue, Http2ErrorCode.NO_ERROR);
}
UpdateState(Http2ConnectionState.Closing);
}
}
Input.CancelPendingRead();
}
}
@ -211,7 +169,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
return;
}
if (_state != Http2ConnectionState.Closed)
if (_isClosed == 0)
{
await _frameWriter.WriteSettingsAsync(_serverSettings.GetNonProtocolDefaults());
// Inform the client that the connection window is larger than the default. It can't be lowered here,
@ -224,13 +182,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
}
}
while (_state != Http2ConnectionState.Closed)
while (_isClosed == 0)
{
var result = await Input.ReadAsync();
var readableBuffer = result.Buffer;
var consumed = readableBuffer.Start;
var examined = readableBuffer.Start;
// Call UpdateCompletedStreams() prior to frame processing in order to remove any streams that have exceded their drain timeouts.
UpdateCompletedStreams();
try
{
if (!readableBuffer.IsEmpty)
@ -262,6 +223,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
finally
{
Input.AdvanceTo(consumed, examined);
UpdateConnectionState();
}
}
}
@ -305,18 +268,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
try
{
lock (_stateLock)
if (TryClose())
{
if (_state != Http2ConnectionState.Closed)
{
_frameWriter.WriteGoAwayAsync(_highestOpenedStreamId, errorCode);
UpdateState(Http2ConnectionState.Closed);
}
if (_activeStreamCount == 0)
{
_streamsCompleted.TrySetResult(null);
}
await _frameWriter.WriteGoAwayAsync(_highestOpenedStreamId, errorCode);
}
// Ensure aborting each stream doesn't result in unnecessary WINDOW_UPDATE frames being sent.
@ -327,8 +281,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
stream.Abort(new IOException(CoreStrings.Http2StreamAborted, connectionError));
}
await _streamsCompleted.Task;
while (_activeStreamCount > 0)
{
await _streamCompletionAwaitable;
UpdateCompletedStreams();
}
// This cancels keep-alive and request header timeouts, but not the response drain timeout.
TimeoutControl.CancelTimeout();
TimeoutControl.StartDrainTimeout(Limits.MinResponseDataRate, Limits.MaxResponseBufferSize);
_frameWriter.Complete();
@ -364,7 +324,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
private async Task<bool> TryReadPrefaceAsync()
{
while (_state != Http2ConnectionState.Closed)
while (_isClosed == 0)
{
var result = await Input.ReadAsync();
var readableBuffer = result.Buffer;
@ -389,6 +349,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
finally
{
Input.AdvanceTo(consumed, examined);
UpdateConnectionState();
}
}
@ -498,12 +460,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorStreamHalfClosedRemote(_incomingFrame.Type, stream.StreamId), Http2ErrorCode.STREAM_CLOSED);
}
if (_incomingFrame.DataEndStream && stream.IsDraining)
{
// No more frames expected.
RemoveDrainingStream(_incomingFrame.StreamId);
}
return stream.OnDataAsync(_incomingFrame, payload);
}
@ -589,46 +545,42 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
}
else
{
// Cancel keep-alive timeout and start header timeout if necessary. The keep-alive timeout can be
// started on another thread so the lock is necessary.
lock (_stateLock)
// Cancel keep-alive timeout and start header timeout if necessary.
if (TimeoutControl.TimerReason != TimeoutReason.None)
{
if (TimeoutControl.TimerReason != TimeoutReason.None)
{
Debug.Assert(TimeoutControl.TimerReason == TimeoutReason.KeepAlive, "Non keep-alive timeout set at start of stream.");
TimeoutControl.CancelTimeout();
}
if (!_incomingFrame.HeadersEndHeaders)
{
TimeoutControl.SetTimeout(Limits.RequestHeadersTimeout.Ticks, TimeoutReason.RequestHeaders);
}
// Start a new stream
_currentHeadersStream = new Http2Stream(new Http2StreamContext
{
ConnectionId = ConnectionId,
StreamId = _incomingFrame.StreamId,
ServiceContext = _context.ServiceContext,
ConnectionFeatures = _context.ConnectionFeatures,
MemoryPool = _context.MemoryPool,
LocalEndPoint = _context.LocalEndPoint,
RemoteEndPoint = _context.RemoteEndPoint,
StreamLifetimeHandler = this,
ClientPeerSettings = _clientSettings,
ServerPeerSettings = _serverSettings,
FrameWriter = _frameWriter,
ConnectionInputFlowControl = _inputFlowControl,
ConnectionOutputFlowControl = _outputFlowControl,
TimeoutControl = TimeoutControl,
});
_currentHeadersStream.Reset();
_headerFlags = _incomingFrame.HeadersFlags;
var headersPayload = payload.Slice(0, _incomingFrame.HeadersPayloadLength); // Minus padding
return DecodeHeadersAsync(application, _incomingFrame.HeadersEndHeaders, headersPayload);
Debug.Assert(TimeoutControl.TimerReason == TimeoutReason.KeepAlive, "Non keep-alive timeout set at start of stream.");
TimeoutControl.CancelTimeout();
}
if (!_incomingFrame.HeadersEndHeaders)
{
TimeoutControl.SetTimeout(Limits.RequestHeadersTimeout.Ticks, TimeoutReason.RequestHeaders);
}
// Start a new stream
_currentHeadersStream = new Http2Stream(new Http2StreamContext
{
ConnectionId = ConnectionId,
StreamId = _incomingFrame.StreamId,
ServiceContext = _context.ServiceContext,
ConnectionFeatures = _context.ConnectionFeatures,
MemoryPool = _context.MemoryPool,
LocalEndPoint = _context.LocalEndPoint,
RemoteEndPoint = _context.RemoteEndPoint,
StreamLifetimeHandler = this,
ClientPeerSettings = _clientSettings,
ServerPeerSettings = _serverSettings,
FrameWriter = _frameWriter,
ConnectionInputFlowControl = _inputFlowControl,
ConnectionOutputFlowControl = _outputFlowControl,
TimeoutControl = TimeoutControl,
});
_currentHeadersStream.Reset();
_headerFlags = _incomingFrame.HeadersFlags;
var headersPayload = payload.Slice(0, _incomingFrame.HeadersPayloadLength); // Minus padding
return DecodeHeadersAsync(application, _incomingFrame.HeadersEndHeaders, headersPayload);
}
}
@ -685,16 +637,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorStreamAborted(_incomingFrame.Type, stream.StreamId), Http2ErrorCode.STREAM_CLOSED);
}
if (stream.IsDraining)
{
// This stream was aborted by the server earlier and now the client is aborting it as well. No more frames are expected.
RemoveDrainingStream(_incomingFrame.StreamId);
}
else
{
// No additional inbound header or data frames are allowed for this stream after receiving a reset.
stream.AbortRstStreamReceived();
}
// No additional inbound header or data frames are allowed for this stream after receiving a reset.
stream.AbortRstStreamReceived();
}
return Task.CompletedTask;
@ -809,7 +753,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorStreamIdNotZero(_incomingFrame.Type), Http2ErrorCode.PROTOCOL_ERROR);
}
StopProcessingNextRequest(sendGracefulGoAway: false);
StopProcessingNextRequest(serverInitiated: false);
return Task.CompletedTask;
}
@ -896,17 +840,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
}
else
{
lock (_stateLock)
Debug.Assert(TimeoutControl.TimerReason == TimeoutReason.RequestHeaders, "Received continuation frame without request header timeout being set.");
if (_incomingFrame.HeadersEndHeaders)
{
Debug.Assert(TimeoutControl.TimerReason == TimeoutReason.RequestHeaders, "Received continuation frame without request header timeout being set.");
if (_incomingFrame.HeadersEndHeaders)
{
TimeoutControl.CancelTimeout();
}
return DecodeHeadersAsync(application, _incomingFrame.ContinuationEndHeaders, payload);
TimeoutControl.CancelTimeout();
}
return DecodeHeadersAsync(application, _incomingFrame.ContinuationEndHeaders, payload);
}
}
@ -920,7 +861,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
return Task.CompletedTask;
}
// This is always called with the _stateLock acquired.
private Task DecodeHeadersAsync<TContext>(IHttpApplication<TContext> application, bool endHeaders, ReadOnlySequence<byte> payload)
{
try
@ -930,11 +870,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
if (endHeaders)
{
if (_state != Http2ConnectionState.Closed)
{
StartStream(application);
}
StartStream(application);
ResetRequestHeaderParsingState();
}
}
@ -953,16 +889,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
if (endHeaders)
{
if (_currentHeadersStream.IsDraining)
{
// This stream is aborted and abandon, no action required
RemoveDrainingStream(_currentHeadersStream.StreamId);
}
else
{
_currentHeadersStream.OnEndStreamReceived();
}
_currentHeadersStream.OnEndStreamReceived();
ResetRequestHeaderParsingState();
}
@ -1041,64 +968,100 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
}
}
void IHttp2StreamLifetimeHandler.OnStreamCompleted(int streamId)
void IRequestProcessor.Tick(DateTimeOffset now)
{
lock (_stateLock)
Input.CancelPendingRead();
}
void IHttp2StreamLifetimeHandler.OnStreamCompleted(Http2Stream stream)
{
_completedStreams.Enqueue(stream);
_streamCompletionAwaitable.Complete();
}
private void UpdateCompletedStreams()
{
Http2Stream firstRequedStream = null;
var now = SystemClock.UtcNowTicks;
while (_completedStreams.TryDequeue(out var stream))
{
_activeStreamCount--;
// Get, Add, Remove so the steam is always registered in at least one collection at a time.
if (_streams.TryGetValue(streamId, out var stream))
if (stream == firstRequedStream)
{
if (stream.IsDraining)
{
stream.DrainExpirationTicks =
_context.ServiceContext.SystemClock.UtcNowTicks + Constants.RequestBodyDrainTimeout.Ticks;
_drainingStreams.TryAdd(streamId, stream);
}
else
{
_streams.TryRemove(streamId, out _);
}
// We've checked every stream that was in _completedStreams by the time
// _checkCompletedStreams was unset, so exit the loop.
_completedStreams.Enqueue(stream);
break;
}
if (_activeStreamCount == 0)
if (stream.DrainExpirationTicks == default)
{
if (_state == Http2ConnectionState.Closing)
{
_frameWriter.WriteGoAwayAsync(_highestOpenedStreamId, Http2ErrorCode.NO_ERROR);
UpdateState(Http2ConnectionState.Closed);
// This is our first time checking this stream.
_activeStreamCount--;
stream.DrainExpirationTicks = now + Constants.RequestBodyDrainTimeout.Ticks;
}
// Wake up request processing loop so the connection can complete if there are no pending requests
Input.CancelPendingRead();
if (stream.EndStreamReceived || stream.RstStreamReceived || stream.DrainExpirationTicks < now)
{
if (stream == _currentHeadersStream)
{
// The drain expired out while receiving trailers. The most recent incoming frame is either a header or continuation frame for the timed out stream.
throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorStreamClosed(_incomingFrame.Type, _incomingFrame.StreamId), Http2ErrorCode.STREAM_CLOSED);
}
if (_state == Http2ConnectionState.Open)
_streams.Remove(stream.StreamId);
}
else
{
if (firstRequedStream == null)
{
// If we're awaiting headers, either a new stream will be started, or there will be a connection
// error possibly due to a request header timeout, so no need to start a keep-alive timeout.
if (TimeoutControl.TimerReason != TimeoutReason.RequestHeaders)
{
TimeoutControl.SetTimeout(Limits.KeepAliveTimeout.Ticks, TimeoutReason.KeepAlive);
}
}
else
{
// Complete the task waiting on all streams to finish
_streamsCompleted.TrySetResult(null);
firstRequedStream = stream;
}
_completedStreams.Enqueue(stream);
}
}
}
void IRequestProcessor.Tick(DateTimeOffset now)
private void UpdateConnectionState()
{
foreach (var stream in _drainingStreams)
if (_isClosed != 0)
{
if (now.Ticks > stream.Value.DrainExpirationTicks)
return;
}
if (_gracefulCloseInitiator != GracefulCloseInitiator.None && !_gracefulCloseStarted)
{
_gracefulCloseStarted = true;
Log.Http2ConnectionClosing(_context.ConnectionId);
if (_gracefulCloseInitiator == GracefulCloseInitiator.Server && _activeStreamCount > 0)
{
RemoveDrainingStream(stream.Key);
_frameWriter.WriteGoAwayAsync(int.MaxValue, Http2ErrorCode.NO_ERROR);
}
}
if (_activeStreamCount == 0)
{
if (_gracefulCloseStarted)
{
if (TryClose())
{
_frameWriter.WriteGoAwayAsync(_highestOpenedStreamId, Http2ErrorCode.NO_ERROR);
}
}
else
{
if (TimeoutControl.TimerReason == TimeoutReason.None)
{
TimeoutControl.SetTimeout(Limits.KeepAliveTimeout.Ticks, TimeoutReason.KeepAlive);
}
// If we're awaiting headers, either a new stream will be started, or there will be a connection
// error possibly due to a request header timeout, so no need to start a keep-alive timeout.
Debug.Assert(TimeoutControl.TimerReason == TimeoutReason.RequestHeaders ||
TimeoutControl.TimerReason == TimeoutReason.KeepAlive);
}
}
}
@ -1270,29 +1233,80 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
return name.SequenceEqual(_connectionBytes) || (name.SequenceEqual(_teBytes) && !value.SequenceEqual(_trailersBytes));
}
private void UpdateState(Http2ConnectionState state)
private bool TryClose()
{
_state = state;
if (state == Http2ConnectionState.Closing)
if (Interlocked.Exchange(ref _isClosed, 1) == 0)
{
Log.Http2ConnectionClosing(_context.ConnectionId);
}
else if (state == Http2ConnectionState.Closed)
{
// This cancels keep-alive and request header timeouts, but not the response drain timeout.
TimeoutControl.CancelTimeout();
Log.Http2ConnectionClosed(_context.ConnectionId, _highestOpenedStreamId);
return true;
}
return false;
}
private class StreamCloseAwaitable : ICriticalNotifyCompletion
{
private static readonly Action _callbackCompleted = () => { };
// Initialize to completed so UpdateCompletedStreams runs at least once during connection teardown
// if there are still active streams.
private Action _callback = _callbackCompleted;
public StreamCloseAwaitable GetAwaiter() => this;
public bool IsCompleted => ReferenceEquals(_callback, _callbackCompleted);
public void GetResult()
{
Debug.Assert(ReferenceEquals(_callback, _callbackCompleted));
_callback = null;
}
public void OnCompleted(Action continuation)
{
if (ReferenceEquals(_callback, _callbackCompleted) ||
ReferenceEquals(Interlocked.CompareExchange(ref _callback, continuation, null), _callbackCompleted))
{
Task.Run(continuation);
}
}
public void UnsafeOnCompleted(Action continuation)
{
OnCompleted(continuation);
}
public void Complete()
{
Interlocked.Exchange(ref _callback, _callbackCompleted)?.Invoke();
}
}
// Note this may be called concurrently based on incoming frames and Ticks.
private void RemoveDrainingStream(int key)
private enum RequestHeaderParsingState
{
_streams.TryRemove(key, out _);
// It's possible to be marked as draining and have RemoveDrainingStream called
// before being added to the draining collection. In that case the next Tick would
// remove it anyways.
_drainingStreams.TryRemove(key, out _);
Ready,
PseudoHeaderFields,
Headers,
Trailers
}
[Flags]
private enum PseudoHeaderFields
{
None = 0x0,
Authority = 0x1,
Method = 0x2,
Path = 0x4,
Scheme = 0x8,
Status = 0x10,
Unknown = 0x40000000
}
private static class GracefulCloseInitiator
{
public const int None = 0;
public const int Server = 1;
public const int Client = 2;
}
}
}

View File

@ -1,12 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
{
public enum Http2ConnectionState
{
Open = 0,
Closing,
Closed
}
}

View File

@ -529,6 +529,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
{
lock (_writeLock)
{
if (_completed)
{
return Task.CompletedTask;
}
_outgoingFrame.PrepareGoAway(lastStreamId, errorCode);
WriteHeaderUnsynchronized();

View File

@ -66,7 +66,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
public bool EndStreamReceived => (_completionState & StreamCompletionFlags.EndStreamReceived) == StreamCompletionFlags.EndStreamReceived;
private bool IsAborted => (_completionState & StreamCompletionFlags.Aborted) == StreamCompletionFlags.Aborted;
internal bool RstStreamReceived => (_completionState & StreamCompletionFlags.RstStreamReceived) == StreamCompletionFlags.RstStreamReceived;
internal bool IsDraining => (_completionState & StreamCompletionFlags.Draining) == StreamCompletionFlags.Draining;
public bool ReceivedEmptyRequestBody
{
@ -94,8 +93,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
{
Log.RequestBodyNotEntirelyRead(ConnectionIdFeature, TraceIdentifier);
ApplyCompletionFlag(StreamCompletionFlags.Draining);
var states = ApplyCompletionFlag(StreamCompletionFlags.Aborted);
if (states.OldState != states.NewState)
{
@ -117,7 +114,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
}
finally
{
_context.StreamLifetimeHandler.OnStreamCompleted(StreamId);
_context.StreamLifetimeHandler.OnStreamCompleted(this);
}
}
@ -509,7 +506,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
RstStreamReceived = 1,
EndStreamReceived = 2,
Aborted = 4,
Draining = 8,
}
}
}

View File

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

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;

View File

@ -56,13 +56,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests.Http2
.Setup(m => m.Http2ConnectionClosing(It.IsAny<string>()))
.Callback(() => requestStopping.SetResult(null));
var testContext = new TestServiceContext(LoggerFactory, mockKestrelTrace.Object);
testContext.InitializeHeartbeat();
using (var server = new TestServer(async context =>
{
requestStarted.SetResult(null);
await requestUnblocked.Task.DefaultTimeout();
await context.Response.WriteAsync("hello world " + context.Request.Protocol);
},
new TestServiceContext(LoggerFactory, mockKestrelTrace.Object),
testContext,
kestrelOptions =>
{
kestrelOptions.Listen(IPAddress.Loopback, 0, listenOptions =>

View File

@ -2678,6 +2678,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
await InitializeConnectionAsync(_echoApplication);
StartHeartbeat();
// Start some streams
await StartStreamAsync(1, _browserRequestHeaders, endStream: false);
await StartStreamAsync(3, _browserRequestHeaders, endStream: false);
@ -3559,7 +3561,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
_connection.Abort(new ConnectionAbortedException());
await _closedStateReached.Task.DefaultTimeout();
VerifyGoAway(await ReceiveFrameAsync(), 0, Http2ErrorCode.INTERNAL_ERROR);
VerifyGoAway(await ReceiveFrameAsync(), int.MaxValue, Http2ErrorCode.INTERNAL_ERROR);
}
[Fact]
@ -3627,6 +3629,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
await InitializeConnectionAsync(_echoApplication);
StartHeartbeat();
await StartStreamAsync(1, _browserRequestHeaders, endStream: false);
_connection.StopProcessingNextRequest();
@ -3657,6 +3661,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
await InitializeConnectionAsync(_echoApplication);
StartHeartbeat();
await StartStreamAsync(1, _browserRequestHeaders, endStream: false);
_connection.StopProcessingNextRequest();

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
@ -1567,7 +1567,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
withFlags: (byte)Http2DataFrameFlags.NONE,
withStreamId: 0);
VerifyGoAway(goAway, 1, Http2ErrorCode.INTERNAL_ERROR);
VerifyGoAway(goAway, int.MaxValue, Http2ErrorCode.INTERNAL_ERROR);
_pair.Application.Output.Complete();
await _connectionTask;
@ -1995,7 +1995,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
_pair.Application.Output.Complete();
await WaitForConnectionErrorAsync<HPackEncodingException>(ignoreNonGoAwayFrames: false, expectedLastStreamId: 1, Http2ErrorCode.INTERNAL_ERROR,
await WaitForConnectionErrorAsync<HPackEncodingException>(ignoreNonGoAwayFrames: false, expectedLastStreamId: int.MaxValue, Http2ErrorCode.INTERNAL_ERROR,
CoreStrings.HPackErrorNotEnoughBuffer);
}

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
@ -152,6 +152,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
protected readonly RequestDelegate _appAbort;
protected TestServiceContext _serviceContext;
private Timer _timer;
internal DuplexPipe.DuplexPipePair _pair;
protected Http2Connection _connection;
@ -385,6 +386,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
public override void Dispose()
{
_timer?.Dispose();
_pair.Application?.Input.Complete();
_pair.Application?.Output.Complete();
_pair.Transport?.Input.Complete();
@ -457,6 +459,19 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
withStreamId: 0);
}
protected void StartHeartbeat()
{
if (_timer == null)
{
_timer = new Timer(OnHeartbeat, state: this, dueTime: Heartbeat.Interval, period: Heartbeat.Interval);
}
}
private static void OnHeartbeat(object state)
{
((IRequestProcessor)((Http2TestBase)state)._connection)?.Tick(default);
}
protected Task StartStreamAsync(int streamId, IEnumerable<KeyValuePair<string, string>> headers, bool endStream)
{
var writableBuffer = _pair.Application.Output;

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
@ -77,6 +77,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await InitializeConnectionAsync(_noopApplication);
StartHeartbeat();
AdvanceClock(limits.KeepAliveTimeout + Heartbeat.Interval);
// keep-alive timeout set but not fired.
@ -148,7 +150,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await WaitForConnectionErrorAsync<BadHttpRequestException>(
ignoreNonGoAwayFrames: false,
expectedLastStreamId: 1,
expectedLastStreamId: int.MaxValue,
Http2ErrorCode.INTERNAL_ERROR,
CoreStrings.BadRequest_RequestHeadersTimeout);
@ -192,7 +194,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
[Theory]
[InlineData(Http2FrameType.DATA)]
[InlineData(Http2FrameType.CONTINUATION, Skip = "https://github.com/aspnet/KestrelHttpServer/issues/3077")]
[InlineData(Http2FrameType.CONTINUATION)]
public async Task AbortedStream_ResetsAndDrainsRequest_RefusesFramesAfterCooldownExpires(Http2FrameType finalFrameType)
{
var mockSystemClock = _serviceContext.MockSystemClock;
@ -216,6 +218,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
if (finalFrameType == Http2FrameType.CONTINUATION)
{
await SendHeadersAsync(1, Http2HeadersFrameFlags.END_STREAM, new byte[0]);
await SendContinuationAsync(1, Http2ContinuationFrameFlags.NONE, new byte[0]);
}
// There's a race when the appfunc is exiting about how soon it unregisters the stream, so retry until success.
@ -223,7 +226,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
// Just past the timeout
mockSystemClock.UtcNow += Constants.RequestBodyDrainTimeout + TimeSpan.FromTicks(1);
(_connection as IRequestProcessor).Tick(mockSystemClock.UtcNow);
// Send an extra frame to make it fail
switch (finalFrameType)
@ -305,7 +307,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await WaitForConnectionErrorAsync<ConnectionAbortedException>(
ignoreNonGoAwayFrames: false,
expectedLastStreamId: 1,
expectedLastStreamId: int.MaxValue,
Http2ErrorCode.INTERNAL_ERROR,
null);
@ -363,7 +365,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await WaitForConnectionErrorAsync<ConnectionAbortedException>(
ignoreNonGoAwayFrames: false,
expectedLastStreamId: 1,
expectedLastStreamId: int.MaxValue,
Http2ErrorCode.INTERNAL_ERROR,
null);
@ -417,7 +419,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await WaitForConnectionErrorAsync<ConnectionAbortedException>(
ignoreNonGoAwayFrames: false,
expectedLastStreamId: 1,
expectedLastStreamId: int.MaxValue,
Http2ErrorCode.INTERNAL_ERROR,
null);
@ -473,7 +475,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await WaitForConnectionErrorAsync<ConnectionAbortedException>(
ignoreNonGoAwayFrames: false,
expectedLastStreamId: 1,
expectedLastStreamId: int.MaxValue,
Http2ErrorCode.INTERNAL_ERROR,
null);
@ -541,7 +543,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await WaitForConnectionErrorAsync<ConnectionAbortedException>(
ignoreNonGoAwayFrames: false,
expectedLastStreamId: 3,
expectedLastStreamId: int.MaxValue,
Http2ErrorCode.INTERNAL_ERROR,
null);
@ -590,7 +592,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await WaitForConnectionErrorAsync<ConnectionAbortedException>(
ignoreNonGoAwayFrames: false,
expectedLastStreamId: 1,
expectedLastStreamId: int.MaxValue,
Http2ErrorCode.INTERNAL_ERROR,
null);
@ -643,7 +645,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await WaitForConnectionErrorAsync<ConnectionAbortedException>(
ignoreNonGoAwayFrames: false,
expectedLastStreamId: 1,
expectedLastStreamId: int.MaxValue,
Http2ErrorCode.INTERNAL_ERROR,
null);
@ -712,7 +714,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await WaitForConnectionErrorAsync<ConnectionAbortedException>(
ignoreNonGoAwayFrames: false,
expectedLastStreamId: 3,
expectedLastStreamId: int.MaxValue,
Http2ErrorCode.INTERNAL_ERROR,
null);
@ -782,7 +784,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await WaitForConnectionErrorAsync<ConnectionAbortedException>(
ignoreNonGoAwayFrames: false,
expectedLastStreamId: 3,
expectedLastStreamId: int.MaxValue,
Http2ErrorCode.INTERNAL_ERROR,
null);
@ -877,7 +879,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await WaitForConnectionErrorAsync<ConnectionAbortedException>(
ignoreNonGoAwayFrames: false,
expectedLastStreamId: 3,
expectedLastStreamId: int.MaxValue,
Http2ErrorCode.INTERNAL_ERROR,
null);