Add HTTP/2 request body data rate limit (#3051)

This commit is contained in:
Stephen Halter 2018-10-26 10:46:46 -07:00 committed by GitHub
parent d9aba751ae
commit 395b681348
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 750 additions and 254 deletions

View File

@ -5,6 +5,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Features
{
/// <summary>
/// Feature to set the minimum data rate at which the the request body must be sent by the client.
/// This feature is not available for HTTP/2 requests. Instead, use <see cref="KestrelServerLimits.MinRequestBodyDataRate"/>
/// for server-wide configuration which applies to both HTTP/2 and HTTP/1.x.
/// </summary>
public interface IHttpMinRequestBodyDataRateFeature
{
@ -12,6 +14,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Features
/// The minimum data rate in bytes/second at which the request body must be sent by the client.
/// Setting this property to null indicates no minimum data rate should be enforced.
/// This limit has no effect on upgraded connections which are always unlimited.
/// This feature is not available for HTTP/2 requests. Instead, use <see cref="KestrelServerLimits.MinRequestBodyDataRate"/>
/// for server-wide configuration which applies to both HTTP/2 and HTTP/1.x.
/// </summary>
MinDataRate MinDataRate { get; set; }
}

View File

@ -5,6 +5,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Features
{
/// <summary>
/// Feature to set the minimum data rate at which the response must be received by the client.
/// This feature is not available for HTTP/2 requests. Instead, use <see cref="KestrelServerLimits.MinResponseDataRate"/>
/// for server-wide configuration which applies to both HTTP/2 and HTTP/1.x.
/// </summary>
public interface IHttpMinResponseDataRateFeature
{
@ -12,6 +14,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Features
/// The minimum data rate in bytes/second at which the response must be received by the client.
/// Setting this property to null indicates no minimum data rate should be enforced.
/// This limit has no effect on upgraded connections which are always unlimited.
/// This feature is not available for HTTP/2 requests. Instead, use <see cref="KestrelServerLimits.MinResponseDataRate"/>
/// for server-wide configuration which applies to both HTTP/2 and HTTP/1.x.
/// </summary>
MinDataRate MinDataRate { get; set; }
}

View File

@ -11,7 +11,9 @@ using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
public partial class Http1Connection : IHttpUpgradeFeature
public partial class Http1Connection : IHttpUpgradeFeature,
IHttpMinRequestBodyDataRateFeature,
IHttpMinResponseDataRateFeature
{
bool IHttpUpgradeFeature.IsUpgradableRequest => IsUpgradableRequest;
@ -44,5 +46,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
return _streams.Upgrade();
}
MinDataRate IHttpMinRequestBodyDataRateFeature.MinDataRate
{
get => MinRequestBodyDataRate;
set => MinRequestBodyDataRate = value;
}
MinDataRate IHttpMinResponseDataRateFeature.MinDataRate
{
get => MinResponseDataRate;
set => MinResponseDataRate = value;
}
}
}

View File

@ -59,11 +59,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
public PipeReader Input => _context.Transport.Input;
public ITimeoutControl TimeoutControl => _context.TimeoutControl;
public bool RequestTimedOut => _requestTimedOut;
public override bool IsUpgradableRequest => _upgradeAvailable;
public MinDataRate MinRequestBodyDataRate { get; set; }
public MinDataRate MinResponseDataRate { get; set; }
protected override void OnRequestProcessingEnded()
{
Input.Complete();
@ -125,6 +128,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
public void HandleRequestHeadersTimeout()
=> SendTimeoutResponse();
public void HandleReadDataRateTimeout()
{
Log.RequestBodyMinimumDataRateNotSatisfied(ConnectionId, TraceIdentifier, MinRequestBodyDataRate.BytesPerSecond);
SendTimeoutResponse();
}
public void ParseRequest(ReadOnlySequence<byte> buffer, out SequencePosition consumed, out SequencePosition examined)
{
consumed = buffer.Start;
@ -423,13 +432,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
protected override void OnReset()
{
ResetIHttpUpgradeFeature();
ResetHttp1Features();
_requestTimedOut = false;
_requestTargetForm = HttpRequestTarget.Unknown;
_absoluteRequestTarget = null;
_remainingRequestHeadersBytesAllowed = ServerOptions.Limits.MaxRequestHeadersTotalSize + 2;
_requestCount++;
MinRequestBodyDataRate = ServerOptions.Limits.MinRequestBodyDataRate;
MinResponseDataRate = ServerOptions.Limits.MinResponseDataRate;
}
protected override void OnRequestProcessingEnding()

View File

@ -18,10 +18,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
private volatile bool _canceled;
private Task _pumpTask;
private bool _timingReads;
protected Http1MessageBody(Http1Connection context)
: base(context)
: base(context, context.MinRequestBodyDataRate)
{
_context = context;
}
@ -39,8 +38,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
TryProduceContinue();
}
TryStartTimingReads();
while (true)
{
var result = await awaitable;
@ -66,22 +63,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
bool done;
done = Read(readableBuffer, _context.RequestBodyPipe.Writer, out consumed, out examined);
var writeAwaitable = _context.RequestBodyPipe.Writer.FlushAsync();
var backpressure = false;
if (!writeAwaitable.IsCompleted)
{
// Backpressure, stop controlling incoming data rate until data is read.
backpressure = true;
TryPauseTimingReads();
}
await writeAwaitable;
if (backpressure)
{
TryResumeTimingReads();
}
await _context.RequestBodyPipe.Writer.FlushAsync();
if (done)
{
@ -109,7 +91,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
BadHttpRequestException.Throw(RequestRejectionReason.UnexpectedEndOfRequestContent);
}
}
finally
{
@ -126,11 +107,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
finally
{
_context.RequestBodyPipe.Writer.Complete(error);
TryStopTimingReads();
}
}
public override Task StopAsync()
protected override Task OnStopAsync()
{
if (!_context.HasStartedConsumingRequestBody)
{
@ -219,8 +199,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
protected void Copy(ReadOnlySequence<byte> readableBuffer, PipeWriter writableBuffer)
{
_context.TimeoutControl.BytesRead(readableBuffer.Length);
if (readableBuffer.IsSingleSegment)
{
writableBuffer.Write(readableBuffer.First.Span);
@ -244,53 +222,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
throw new NotImplementedException();
}
private void TryStartTimingReads()
{
if (!RequestUpgrade)
{
Log.RequestBodyStart(_context.ConnectionIdFeature, _context.TraceIdentifier);
// REVIEW: This makes it no longer effective to change the min rate after the app starts reading.
// Is this OK? Should we throw from the MinRequestBodyDataRate setter in this case?
var minRate = _context.MinRequestBodyDataRate;
if (minRate != null)
{
_timingReads = true;
_context.TimeoutControl.StartTimingReads(minRate);
}
}
}
private void TryPauseTimingReads()
{
if (_timingReads)
{
_context.TimeoutControl.PauseTimingReads();
}
}
private void TryResumeTimingReads()
{
if (_timingReads)
{
_context.TimeoutControl.ResumeTimingReads();
}
}
private void TryStopTimingReads()
{
if (!RequestUpgrade)
{
Log.RequestBodyDone(_context.ConnectionIdFeature, _context.TraceIdentifier);
if (_timingReads)
{
_context.TimeoutControl.StopTimingReads();
}
}
}
public static MessageBody For(
HttpVersion httpVersion,
HttpRequestHeaders headers,

View File

@ -19,9 +19,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
IHttpRequestLifetimeFeature,
IHttpRequestIdentifierFeature,
IHttpBodyControlFeature,
IHttpMaxRequestBodySizeFeature,
IHttpMinRequestBodyDataRateFeature,
IHttpMinResponseDataRateFeature
IHttpMaxRequestBodySizeFeature
{
// NOTE: When feature interfaces are added to or removed from this HttpProtocol class implementation,
// then the list of `implementedFeatures` in the generated code project MUST also be updated.
@ -192,21 +190,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
}
}
MinDataRate IHttpMinRequestBodyDataRateFeature.MinDataRate
{
get => MinRequestBodyDataRate;
set => MinRequestBodyDataRate = value;
}
MinDataRate IHttpMinResponseDataRateFeature.MinDataRate
{
get => MinResponseDataRate;
set => MinResponseDataRate = value;
}
protected void ResetIHttpUpgradeFeature()
protected void ResetHttp1Features()
{
_currentIHttpUpgradeFeature = this;
_currentIHttpMinRequestBodyDataRateFeature = this;
_currentIHttpMinResponseDataRateFeature = this;
}
protected void ResetHttp2Features()

View File

@ -71,8 +71,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
_currentIHttpRequestLifetimeFeature = this;
_currentIHttpConnectionFeature = this;
_currentIHttpMaxRequestBodySizeFeature = this;
_currentIHttpMinRequestBodyDataRateFeature = this;
_currentIHttpMinResponseDataRateFeature = this;
_currentIHttpBodyControlFeature = this;
_currentIServiceProvidersFeature = null;
@ -87,6 +85,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
_currentITlsConnectionFeature = null;
_currentIHttpWebSocketFeature = null;
_currentISessionFeature = null;
_currentIHttpMinRequestBodyDataRateFeature = null;
_currentIHttpMinResponseDataRateFeature = null;
_currentIHttpSendFileFeature = null;
}

View File

@ -83,6 +83,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
public ServiceContext ServiceContext => _context.ServiceContext;
private IPEndPoint LocalEndPoint => _context.LocalEndPoint;
private IPEndPoint RemoteEndPoint => _context.RemoteEndPoint;
public ITimeoutControl TimeoutControl => _context.TimeoutControl;
public IFeatureCollection ConnectionFeatures => _context.ConnectionFeatures;
public IHttpOutputProducer Output { get; protected set; }
@ -275,10 +276,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
protected HttpResponseHeaders HttpResponseHeaders { get; } = new HttpResponseHeaders();
public MinDataRate MinRequestBodyDataRate { get; set; }
public MinDataRate MinResponseDataRate { get; set; }
public void InitializeStreams(MessageBody messageBody)
{
if (_streams == null)
@ -363,9 +360,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
_responseBytesWritten = 0;
MinRequestBodyDataRate = ServerOptions.Limits.MinRequestBodyDataRate;
MinResponseDataRate = ServerOptions.Limits.MinResponseDataRate;
OnReset();
}
@ -628,7 +622,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
RequestBodyPipe.Reader.Complete();
// Wait for MessageBody.PumpAsync() to call RequestBodyPipe.Writer.Complete().
// Wait for Http1MessageBody.PumpAsync() to call RequestBodyPipe.Writer.Complete().
await messageBody.StopAsync();
}
}

View File

@ -4,6 +4,7 @@
using System;
using System.Buffers;
using System.IO;
using System.IO.Pipelines;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
@ -16,13 +17,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
private static readonly MessageBody _zeroContentLengthKeepAlive = new ForZeroContentLength(keepAlive: true);
private readonly HttpProtocol _context;
private readonly MinDataRate _minRequestBodyDataRate;
private bool _send100Continue = true;
private long _consumedBytes;
private bool _stopped;
protected MessageBody(HttpProtocol context)
private bool _timingEnabled;
private bool _backpressure;
private long _alreadyTimedBytes;
protected MessageBody(HttpProtocol context, MinDataRate minRequestBodyDataRate)
{
_context = context;
_minRequestBodyDataRate = minRequestBodyDataRate;
}
public static MessageBody ZeroContentLengthClose => _zeroContentLengthClose;
@ -39,12 +47,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
public virtual async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default(CancellationToken))
{
TryInit();
TryStart();
while (true)
{
var result = await _context.RequestBodyPipe.Reader.ReadAsync(cancellationToken);
var result = await StartTimingReadAsync(cancellationToken);
var readableBuffer = result.Buffer;
var readableBufferLength = readableBuffer.Length;
StopTimingRead(readableBufferLength);
var consumed = readableBuffer.End;
var actual = 0;
@ -52,8 +63,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
if (!readableBuffer.IsEmpty)
{
// buffer.Count is int
actual = (int)Math.Min(readableBuffer.Length, buffer.Length);
// buffer.Length is int
actual = (int)Math.Min(readableBufferLength, buffer.Length);
// Make sure we don't double-count bytes on the next read.
_alreadyTimedBytes = readableBufferLength - actual;
var slice = readableBuffer.Slice(0, actual);
consumed = readableBuffer.GetPosition(actual);
slice.CopyTo(buffer.Span);
@ -63,6 +78,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
if (result.IsCompleted)
{
TryStop();
return 0;
}
}
@ -79,14 +95,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
public virtual async Task CopyToAsync(Stream destination, CancellationToken cancellationToken = default(CancellationToken))
{
TryInit();
TryStart();
while (true)
{
var result = await _context.RequestBodyPipe.Reader.ReadAsync(cancellationToken);
var result = await StartTimingReadAsync(cancellationToken);
var readableBuffer = result.Buffer;
var consumed = readableBuffer.End;
var bytesRead = 0;
var readableBufferLength = readableBuffer.Length;
StopTimingRead(readableBufferLength);
try
{
@ -98,8 +114,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
// - The WriteAsync(ReadOnlyMemory<byte>) isn't overridden on the destination
// - We change the Kestrel Memory Pool to not use pinned arrays but instead use native memory
bytesRead += memory.Length;
#if NETCOREAPP2_1
await destination.WriteAsync(memory, cancellationToken);
#elif NETSTANDARD2_0
@ -113,30 +127,38 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
if (result.IsCompleted)
{
TryStop();
return;
}
}
finally
{
_context.RequestBodyPipe.Reader.AdvanceTo(consumed);
_context.RequestBodyPipe.Reader.AdvanceTo(readableBuffer.End);
// Update the flow-control window after advancing the pipe reader, so we don't risk overfilling
// the pipe despite the client being well-behaved.
OnDataRead(bytesRead);
OnDataRead(readableBufferLength);
}
}
}
public virtual Task ConsumeAsync()
{
TryInit();
TryStart();
return OnConsumeAsync();
}
protected abstract Task OnConsumeAsync();
public virtual Task StopAsync()
{
TryStop();
public abstract Task StopAsync();
return OnStopAsync();
}
protected virtual Task OnConsumeAsync() => Task.CompletedTask;
protected virtual Task OnStopAsync() => Task.CompletedTask;
protected void TryProduceContinue()
{
@ -147,13 +169,52 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
}
}
private void TryInit()
private void TryStart()
{
if (!_context.HasStartedConsumingRequestBody)
if (_context.HasStartedConsumingRequestBody)
{
OnReadStarting();
_context.HasStartedConsumingRequestBody = true;
OnReadStarted();
return;
}
OnReadStarting();
_context.HasStartedConsumingRequestBody = true;
if (!RequestUpgrade)
{
Log.RequestBodyStart(_context.ConnectionIdFeature, _context.TraceIdentifier);
if (_minRequestBodyDataRate != null)
{
_timingEnabled = true;
_context.TimeoutControl.StartRequestBody(_minRequestBodyDataRate);
}
}
OnReadStarted();
}
private void TryStop()
{
if (_stopped)
{
return;
}
_stopped = true;
if (!RequestUpgrade)
{
Log.RequestBodyDone(_context.ConnectionIdFeature, _context.TraceIdentifier);
if (_timingEnabled)
{
if (_backpressure)
{
_context.TimeoutControl.StopTimingRead();
}
_context.TimeoutControl.StopRequestBody();
}
}
}
@ -165,7 +226,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
}
protected virtual void OnDataRead(int bytesRead)
protected virtual void OnDataRead(long bytesRead)
{
}
@ -179,10 +240,35 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
}
}
private ValueTask<ReadResult> StartTimingReadAsync(CancellationToken cancellationToken)
{
var readAwaitable = _context.RequestBodyPipe.Reader.ReadAsync(cancellationToken);
if (!readAwaitable.IsCompleted && _timingEnabled)
{
_backpressure = true;
_context.TimeoutControl.StartTimingRead();
}
return readAwaitable;
}
private void StopTimingRead(long bytesRead)
{
_context.TimeoutControl.BytesRead(bytesRead - _alreadyTimedBytes);
_alreadyTimedBytes = 0;
if (_backpressure)
{
_backpressure = false;
_context.TimeoutControl.StopTimingRead();
}
}
private class ForZeroContentLength : MessageBody
{
public ForZeroContentLength(bool keepAlive)
: base(null)
: base(null, null)
{
RequestKeepAlive = keepAlive;
}
@ -196,8 +282,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
public override Task ConsumeAsync() => Task.CompletedTask;
public override Task StopAsync() => Task.CompletedTask;
protected override Task OnConsumeAsync() => Task.CompletedTask;
}
}
}

View File

@ -24,6 +24,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.FlowControl
_minWindowSizeIncrement = (int)minWindowSizeIncrement;
}
public bool IsAvailabilityLow => _flow.Available < _minWindowSizeIncrement;
public bool TryAdvance(int bytes)
{
lock (_flowLock)
@ -90,10 +92,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.FlowControl
public void StopWindowUpdates()
{
lock (_flowLock)
{
_windowUpdatesDisabled = true;
}
_windowUpdatesDisabled = true;
}
public int Abort()

View File

@ -149,6 +149,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
Abort(new ConnectionAbortedException(CoreStrings.BadRequest_RequestHeadersTimeout));
}
public void HandleReadDataRateTimeout()
{
Log.RequestBodyMinimumDataRateNotSatisfied(ConnectionId, null, Limits.MinRequestBodyDataRate.BytesPerSecond);
Abort(new ConnectionAbortedException(CoreStrings.BadRequest_RequestBodyTimeout));
}
public void StopProcessingNextRequest(bool sendGracefulGoAway = false)
{
lock (_stateLock)
@ -184,6 +190,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
try
{
ValidateTlsRequirements();
TimeoutControl.InitializeHttp2(_inputFlowControl);
TimeoutControl.SetTimeout(Limits.KeepAliveTimeout.Ticks, TimeoutReason.KeepAlive);
if (!await TryReadPrefaceAsync())

View File

@ -3,6 +3,7 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
{
@ -10,8 +11,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
{
private readonly Http2Stream _context;
private Http2MessageBody(Http2Stream context)
: base(context)
private Http2MessageBody(Http2Stream context, MinDataRate minRequestBodyDataRate)
: base(context, minRequestBodyDataRate)
{
_context = context;
}
@ -34,26 +35,21 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
}
}
protected override void OnDataRead(int bytesRead)
protected override void OnDataRead(long bytesRead)
{
_context.OnDataRead(bytesRead);
// The HTTP/2 flow control window cannot be larger than 2^31-1 which limits bytesRead.
_context.OnDataRead((int)bytesRead);
AddAndCheckConsumedBytes(bytesRead);
}
protected override Task OnConsumeAsync() => Task.CompletedTask;
public override Task StopAsync() => Task.CompletedTask;
public static MessageBody For(
HttpRequestHeaders headers,
Http2Stream context)
public static MessageBody For(Http2Stream context, MinDataRate minRequestBodyDataRate)
{
if (context.EndStreamReceived && !context.RequestBodyStarted)
{
return ZeroContentLengthClose;
}
return new Http2MessageBody(context);
return new Http2MessageBody(context, minRequestBodyDataRate);
}
}
}

View File

@ -24,6 +24,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
// This should only be accessed via the FrameWriter. The connection-level output flow control is protected by the
// FrameWriter's connection-level write lock.
private readonly StreamOutputFlowControl _flowControl;
private readonly MinDataRate _minResponseDataRate;
private readonly Http2Stream _stream;
private readonly object _dataWriterLock = new object();
private readonly Pipe _dataPipe;
@ -37,12 +38,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
Http2FrameWriter frameWriter,
StreamOutputFlowControl flowControl,
ITimeoutControl timeoutControl,
MinDataRate minResponseDataRate,
MemoryPool<byte> pool,
Http2Stream stream)
{
_streamId = streamId;
_frameWriter = frameWriter;
_flowControl = flowControl;
_minResponseDataRate = minResponseDataRate;
_stream = stream;
_dataPipe = CreateDataPipe(pool);
_flusher = new TimingPipeFlusher(_dataPipe.Writer, timeoutControl);
@ -209,14 +212,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
{
if (readResult.Buffer.Length > 0)
{
await _frameWriter.WriteDataAsync(_streamId, _flowControl, _stream.MinResponseDataRate, readResult.Buffer, endStream: false);
await _frameWriter.WriteDataAsync(_streamId, _flowControl, _minResponseDataRate, readResult.Buffer, endStream: false);
}
await _frameWriter.WriteResponseTrailers(_streamId, _stream.Trailers);
}
else
{
await _frameWriter.WriteDataAsync(_streamId, _flowControl, _stream.MinResponseDataRate, readResult.Buffer, endStream: readResult.IsCompleted);
await _frameWriter.WriteDataAsync(_streamId, _flowControl, _minResponseDataRate, readResult.Buffer, endStream: readResult.IsCompleted);
}
_dataPipe.Reader.AdvanceTo(readResult.Buffer.End);

View File

@ -35,16 +35,26 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
_context = context;
_inputFlowControl = new StreamInputFlowControl(
_context.StreamId,
_context.FrameWriter,
context.StreamId,
context.FrameWriter,
context.ConnectionInputFlowControl,
_context.ServerPeerSettings.InitialWindowSize,
_context.ServerPeerSettings.InitialWindowSize / 2);
context.ServerPeerSettings.InitialWindowSize,
context.ServerPeerSettings.InitialWindowSize / 2);
_outputFlowControl = new StreamOutputFlowControl(context.ConnectionOutputFlowControl, context.ClientPeerSettings.InitialWindowSize);
_http2Output = new Http2OutputProducer(context.StreamId, context.FrameWriter, _outputFlowControl, context.TimeoutControl, context.MemoryPool, this);
_outputFlowControl = new StreamOutputFlowControl(
context.ConnectionOutputFlowControl,
context.ClientPeerSettings.InitialWindowSize);
RequestBodyPipe = CreateRequestBodyPipe(_context.ServerPeerSettings.InitialWindowSize);
_http2Output = new Http2OutputProducer(
context.StreamId,
context.FrameWriter,
_outputFlowControl,
context.TimeoutControl,
context.ServiceContext.ServerOptions.Limits.MinResponseDataRate,
context.MemoryPool,
this);
RequestBodyPipe = CreateRequestBodyPipe(context.ServerPeerSettings.InitialWindowSize);
Output = _http2Output;
}
@ -106,7 +116,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
=> StringUtilities.ConcatAsHexSuffix(ConnectionId, ':', (uint)StreamId);
protected override MessageBody CreateMessageBody()
=> Http2MessageBody.For(HttpRequestHeaders, this);
=> Http2MessageBody.For(this, ServerOptions.Limits.MinRequestBodyDataRate);
// Compare to Http1Connection.OnStartLine
protected override bool TryParseRequest(ReadResult result, out bool endConnection)
@ -185,7 +195,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
// Approximate MaxRequestLineSize by totaling the required pseudo header field lengths.
var requestLineLength = _methodText.Length + Scheme.Length + hostText.Length + path.Length;
if (requestLineLength > ServiceContext.ServerOptions.Limits.MaxRequestLineSize)
if (requestLineLength > ServerOptions.Limits.MaxRequestLineSize)
{
ResetAndAbort(new ConnectionAbortedException(CoreStrings.BadRequest_RequestLineTooLong), Http2ErrorCode.PROTOCOL_ERROR);
return false;

View File

@ -384,8 +384,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
_requestProcessor.HandleRequestHeadersTimeout();
break;
case TimeoutReason.ReadDataRate:
Log.RequestBodyMinimumDataRateNotSatisfied(_context.ConnectionId, _http1Connection.TraceIdentifier, _http1Connection.MinRequestBodyDataRate.BytesPerSecond);
_http1Connection.SendTimeoutResponse();
_requestProcessor.HandleReadDataRateTimeout();
break;
case TimeoutReason.WriteDataRate:
Log.ResponseMinimumDataRateNotSatisfied(_context.ConnectionId, _http1Connection?.TraceIdentifier);

View File

@ -13,6 +13,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
Task ProcessRequestsAsync<TContext>(IHttpApplication<TContext> application);
void StopProcessingNextRequest();
void HandleRequestHeadersTimeout();
void HandleReadDataRateTimeout();
void OnInputOrOutputCompleted();
void Tick(DateTimeOffset now);
void Abort(ConnectionAbortedException ex);

View File

@ -1,6 +1,8 @@
// 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 Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.FlowControl;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
{
public interface ITimeoutControl
@ -11,10 +13,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
void ResetTimeout(long ticks, TimeoutReason timeoutReason);
void CancelTimeout();
void StartTimingReads(MinDataRate minRate);
void PauseTimingReads();
void ResumeTimingReads();
void StopTimingReads();
void InitializeHttp2(InputFlowControl connectionInputFlowControl);
void StartRequestBody(MinDataRate minRate);
void StopRequestBody();
void StartTimingRead();
void StopTimingRead();
void BytesRead(long count);
void StartTimingWrite(MinDataRate minRate, long size);

View File

@ -5,6 +5,7 @@ using System;
using System.Diagnostics;
using System.Threading;
using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.FlowControl;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
{
@ -21,9 +22,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
private bool _readTimingPauseRequested;
private long _readTimingElapsedTicks;
private long _readTimingBytesRead;
private InputFlowControl _connectionInputFlowControl;
// The following are always 0 or 1 for HTTP/1.x
private int _concurrentIncompleteRequestBodies;
private int _concurrentAwaitingReads;
private readonly object _writeTimingLock = new object();
private int _writeTimingWrites;
private int _cuncurrentAwaitingWrites;
private long _writeTimingTimeoutTimestamp;
public TimeoutControl(ITimeoutHandler timeoutHandler)
@ -76,6 +81,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
return;
}
// Don't enforce the rate timeout if there is back pressure due to HTTP/2 connection-level input
// flow control. We don't consider stream-level flow control, because we wouldn't be timing a read
// for any stream that didn't have a completely empty stream-level flow control window.
if (_connectionInputFlowControl?.IsAvailabilityLow == true)
{
return;
}
lock (_readTimingLock)
{
if (!_readTimingEnabled)
@ -88,7 +101,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
if (_minReadRate.BytesPerSecond > 0 && _readTimingElapsedTicks > _minReadRate.GracePeriod.Ticks)
{
var elapsedSeconds = (double)_readTimingElapsedTicks / TimeSpan.TicksPerSecond;
var rate = Interlocked.Read(ref _readTimingBytesRead) / elapsedSeconds;
var rate = _readTimingBytesRead / elapsedSeconds;
if (rate < _minReadRate.BytesPerSecond && !Debugger.IsAttached)
{
@ -111,7 +124,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
{
lock (_writeTimingLock)
{
if (_writeTimingWrites > 0 && timestamp > _writeTimingTimeoutTimestamp && !Debugger.IsAttached)
if (_cuncurrentAwaitingWrites > 0 && timestamp > _writeTimingTimeoutTimestamp && !Debugger.IsAttached)
{
_timeoutHandler.OnTimeout(TimeoutReason.WriteDataRate);
}
@ -142,40 +155,64 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
TimerReason = timeoutReason;
// Add Heartbeat.Interval since this can be called right before the next heartbeat.
Interlocked.Exchange(ref _timeoutTimestamp, _lastTimestamp + ticks + Heartbeat.Interval.Ticks);
Interlocked.Exchange(ref _timeoutTimestamp, Interlocked.Read(ref _lastTimestamp) + ticks + Heartbeat.Interval.Ticks);
}
public void StartTimingReads(MinDataRate minRate)
public void InitializeHttp2(InputFlowControl connectionInputFlowControl)
{
_connectionInputFlowControl = connectionInputFlowControl;
}
public void StartRequestBody(MinDataRate minRate)
{
lock (_readTimingLock)
{
// minRate is always KestrelServerLimits.MinRequestBodyDataRate for HTTP/2 which is the only protocol that supports concurrent request bodies.
Debug.Assert(_concurrentIncompleteRequestBodies == 0 || minRate == _minReadRate, "Multiple simultaneous read data rates are not supported.");
_minReadRate = minRate;
_readTimingElapsedTicks = 0;
_readTimingBytesRead = 0;
_readTimingEnabled = true;
_concurrentIncompleteRequestBodies++;
if (_concurrentIncompleteRequestBodies == 1)
{
_readTimingElapsedTicks = 0;
_readTimingBytesRead = 0;
}
}
}
public void StopTimingReads()
public void StopRequestBody()
{
lock (_readTimingLock)
{
_readTimingEnabled = false;
_concurrentIncompleteRequestBodies--;
if (_concurrentIncompleteRequestBodies == 0)
{
_readTimingEnabled = false;
}
}
}
public void PauseTimingReads()
public void StopTimingRead()
{
lock (_readTimingLock)
{
_readTimingPauseRequested = true;
_concurrentAwaitingReads--;
if (_concurrentAwaitingReads == 0)
{
_readTimingPauseRequested = true;
}
}
}
public void ResumeTimingReads()
public void StartTimingRead()
{
lock (_readTimingLock)
{
_concurrentAwaitingReads++;
_readTimingEnabled = true;
// In case pause and resume were both called between ticks
@ -185,7 +222,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
public void BytesRead(long count)
{
Interlocked.Add(ref _readTimingBytesRead, count);
Debug.Assert(count >= 0, "BytesRead count must not be negative.");
lock (_readTimingLock)
{
_readTimingBytesRead += count;
}
}
public void StartTimingWrite(MinDataRate minRate, long size)
@ -193,7 +235,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
lock (_writeTimingLock)
{
// Add Heartbeat.Interval since this can be called right before the next heartbeat.
var currentTimeUpperBound = _lastTimestamp + Heartbeat.Interval.Ticks;
var currentTimeUpperBound = Interlocked.Read(ref _lastTimestamp) + Heartbeat.Interval.Ticks;
var ticksToCompleteWriteAtMinRate = TimeSpan.FromSeconds(size / minRate.BytesPerSecond).Ticks;
// If ticksToCompleteWriteAtMinRate is less than the configured grace period,
@ -213,7 +255,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
var accumulatedWriteTimeoutTimestamp = _writeTimingTimeoutTimestamp + ticksToCompleteWriteAtMinRate;
_writeTimingTimeoutTimestamp = Math.Max(singleWriteTimeoutTimestamp, accumulatedWriteTimeoutTimestamp);
_writeTimingWrites++;
_cuncurrentAwaitingWrites++;
}
}
@ -221,7 +263,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
{
lock (_writeTimingLock)
{
_writeTimingWrites--;
_cuncurrentAwaitingWrites--;
}
}

View File

@ -853,7 +853,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
var buffer = new byte[10];
await context.Response.Body.WriteAsync(buffer, 0, 10);
});
var mockMessageBody = new Mock<MessageBody>(null);
var mockMessageBody = new Mock<MessageBody>(null, null);
_http1Connection.NextMessageBody = mockMessageBody.Object;
var requestProcessingTask = _http1Connection.ProcessRequestsAsync(httpApplication);

View File

@ -2,49 +2,34 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Buffers;
using System.IO.Pipelines;
using System.Linq;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal;
using Microsoft.AspNetCore.Testing;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
public class HttpProtocolFeatureCollectionTests : IDisposable
public class HttpProtocolFeatureCollectionTests
{
private readonly IDuplexPipe _transport;
private readonly IDuplexPipe _application;
private readonly TestHttp1Connection _http1Connection;
private readonly ServiceContext _serviceContext;
private readonly HttpConnectionContext _http1ConnectionContext;
private readonly MemoryPool<byte> _memoryPool;
private readonly IFeatureCollection _collection;
public HttpProtocolFeatureCollectionTests()
{
_memoryPool = KestrelMemoryPool.Create();
var options = new PipeOptions(_memoryPool, readerScheduler: PipeScheduler.Inline, writerScheduler: PipeScheduler.Inline, useSynchronizationContext: false);
var pair = DuplexPipe.CreateConnectionPair(options, options);
_transport = pair.Transport;
_application = pair.Application;
_serviceContext = new TestServiceContext();
_http1ConnectionContext = new HttpConnectionContext
{
ServiceContext = _serviceContext,
ServiceContext = new TestServiceContext(),
ConnectionFeatures = new FeatureCollection(),
MemoryPool = _memoryPool,
TimeoutControl = Mock.Of<ITimeoutControl>(),
Transport = pair.Transport
Transport = Mock.Of<IDuplexPipe>(),
};
_http1Connection = new TestHttp1Connection(_http1ConnectionContext);
@ -52,17 +37,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
_collection = _http1Connection;
}
public void Dispose()
{
_transport.Input.Complete();
_transport.Output.Complete();
_application.Input.Complete();
_application.Output.Complete();
_memoryPool.Dispose();
}
[Fact]
public int FeaturesStartAsSelf()
{
@ -166,6 +140,27 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
EachHttpProtocolFeatureSetAndUnique();
}
[Fact]
public void Http2StreamFeatureCollectionDoesNotIncludeMinRateFeatures()
{
var http2Stream = new Http2Stream(new Http2StreamContext
{
ServiceContext = new TestServiceContext(),
ConnectionFeatures = new FeatureCollection(),
TimeoutControl = Mock.Of<ITimeoutControl>(),
Transport = Mock.Of<IDuplexPipe>(),
ServerPeerSettings = new Http2PeerSettings(),
ClientPeerSettings = new Http2PeerSettings(),
});
var http2StreamCollection = (IFeatureCollection)http2Stream;
Assert.Null(http2StreamCollection.Get<IHttpMinRequestBodyDataRateFeature>());
Assert.Null(http2StreamCollection.Get<IHttpMinResponseDataRateFeature>());
Assert.NotNull(_collection.Get<IHttpMinRequestBodyDataRateFeature>());
Assert.NotNull(_collection.Get<IHttpMinResponseDataRateFeature>());
}
private void CompareGenericGetterToIndexer()
{
Assert.Same(_collection.Get<IHttpRequestFeature>(), _collection[typeof(IHttpRequestFeature)]);
@ -218,6 +213,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
return featureCount;
}
private HttpProtocol CreateHttp1Connection() => new TestHttp1Connection(_http1ConnectionContext);
private Http1Connection CreateHttp1Connection() => new TestHttp1Connection(_http1ConnectionContext);
}
}

View File

@ -118,7 +118,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
var allowSynchronousIO = false;
var mockBodyControl = new Mock<IHttpBodyControlFeature>();
mockBodyControl.Setup(m => m.AllowSynchronousIO).Returns(() => allowSynchronousIO);
var mockMessageBody = new Mock<MessageBody>((HttpProtocol)null);
var mockMessageBody = new Mock<MessageBody>(null, null);
mockMessageBody.Setup(m => m.ReadAsync(It.IsAny<Memory<byte>>(), CancellationToken.None)).Returns(new ValueTask<int>(0));
var stream = new HttpRequestStream(mockBodyControl.Object);

View File

@ -699,10 +699,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
input.Fin();
await logEvent.Task.DefaultTimeout();
input.Http1Connection.RequestBodyPipe.Reader.Complete();
await body.StopAsync();
await logEvent.Task.DefaultTimeout();
}
}
@ -717,46 +717,43 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
var body = Http1MessageBody.For(HttpVersion.Http11, new HttpRequestHeaders { HeaderContentLength = "12" }, input.Http1Connection);
// Add some input and read it to start PumpAsync
var readTask1 = body.ReadAsync(new ArraySegment<byte>(new byte[6]));
input.Add("hello,");
Assert.Equal(6, await body.ReadAsync(new ArraySegment<byte>(new byte[6])));
Assert.Equal(6, await readTask1);
var readTask2 = body.ReadAsync(new ArraySegment<byte>(new byte[6]));
input.Add(" world");
Assert.Equal(6, await body.ReadAsync(new ArraySegment<byte>(new byte[6])));
Assert.Equal(6, await readTask2);
// Due to the limits set on HttpProtocol.RequestBodyPipe, backpressure should be triggered on every write to that pipe.
mockTimeoutControl.Verify(timeoutControl => timeoutControl.PauseTimingReads(), Times.Exactly(2));
mockTimeoutControl.Verify(timeoutControl => timeoutControl.ResumeTimingReads(), Times.Exactly(2));
mockTimeoutControl.Verify(timeoutControl => timeoutControl.StopTimingRead(), Times.Exactly(2));
mockTimeoutControl.Verify(timeoutControl => timeoutControl.StartTimingRead(), Times.Exactly(2));
}
}
[Fact]
public async Task OnlyEnforcesRequestBodyTimeoutAfterSending100Continue()
public async Task OnlyEnforcesRequestBodyTimeoutAfterFirstRead()
{
using (var input = new TestInput())
{
var produceContinueCalled = false;
var startTimingReadsCalledAfterProduceContinue = false;
var mockHttpResponseControl = new Mock<IHttpResponseControl>();
mockHttpResponseControl
.Setup(httpResponseControl => httpResponseControl.ProduceContinue())
.Callback(() => produceContinueCalled = true);
input.Http1Connection.HttpResponseControl = mockHttpResponseControl.Object;
var startRequestBodyCalled = false;
var minReadRate = input.Http1Connection.MinRequestBodyDataRate;
var mockTimeoutControl = new Mock<ITimeoutControl>();
mockTimeoutControl
.Setup(timeoutControl => timeoutControl.StartTimingReads(minReadRate))
.Callback(() => startTimingReadsCalledAfterProduceContinue = produceContinueCalled);
.Setup(timeoutControl => timeoutControl.StartRequestBody(minReadRate))
.Callback(() => startRequestBodyCalled = true);
input.Http1ConnectionContext.TimeoutControl = mockTimeoutControl.Object;
var body = Http1MessageBody.For(HttpVersion.Http11, new HttpRequestHeaders { HeaderContentLength = "5" }, input.Http1Connection);
Assert.False(startRequestBodyCalled);
// Add some input and read it to start PumpAsync
var readTask = body.ReadAsync(new ArraySegment<byte>(new byte[1]));
Assert.True(startTimingReadsCalledAfterProduceContinue);
Assert.True(startRequestBodyCalled);
input.Add("a");
await readTask;
@ -785,14 +782,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
Assert.Equal(0, await body.ReadAsync(new ArraySegment<byte>(new byte[1])));
mockTimeoutControl.Verify(timeoutControl => timeoutControl.StartTimingReads(minReadRate), Times.Never);
mockTimeoutControl.Verify(timeoutControl => timeoutControl.StopTimingReads(), Times.Never);
mockTimeoutControl.Verify(timeoutControl => timeoutControl.StartRequestBody(minReadRate), Times.Never);
mockTimeoutControl.Verify(timeoutControl => timeoutControl.StopRequestBody(), Times.Never);
// Due to the limits set on HttpProtocol.RequestBodyPipe, backpressure should be triggered on every
// write to that pipe. Verify that read timing pause and resume are not called on upgrade
// requests.
mockTimeoutControl.Verify(timeoutControl => timeoutControl.PauseTimingReads(), Times.Never);
mockTimeoutControl.Verify(timeoutControl => timeoutControl.ResumeTimingReads(), Times.Never);
mockTimeoutControl.Verify(timeoutControl => timeoutControl.StopTimingRead(), Times.Never);
mockTimeoutControl.Verify(timeoutControl => timeoutControl.StartTimingRead(), Times.Never);
input.Http1Connection.RequestBodyPipe.Reader.Complete();
await body.StopAsync();

View File

@ -72,10 +72,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await upgrade.WriteAsync(new byte[1], 0, 1);
}
private class MockMessageBody : Http1MessageBody
private class MockMessageBody : MessageBody
{
public MockMessageBody(bool upgradeable = false)
: base(null)
: base(null, null)
{
RequestUpgrade = upgradeable;
}

View File

@ -66,7 +66,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
var now = DateTimeOffset.UtcNow;
_timeoutControl.Tick(now);
_timeoutControl.StartTimingReads(minRate);
_timeoutControl.StartRequestBody(minRate);
_timeoutControl.StartTimingRead();
// Tick during grace period w/ low data rate
now += TimeSpan.FromSeconds(1);
@ -95,7 +96,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
var now = DateTimeOffset.UtcNow;
_timeoutControl.Tick(now);
_timeoutControl.StartTimingReads(minRate);
_timeoutControl.StartRequestBody(minRate);
_timeoutControl.StartTimingRead();
// Set base data rate to 200 bytes/second
now += gracePeriod;
@ -153,7 +155,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
// Initialize timestamp
_timeoutControl.Tick(systemClock.UtcNow);
_timeoutControl.StartTimingReads(minRate);
_timeoutControl.StartRequestBody(minRate);
_timeoutControl.StartTimingRead();
// Tick at 3s, expected counted time is 3s, expected data rate is 200 bytes/second
systemClock.UtcNow += TimeSpan.FromSeconds(3);
@ -162,7 +165,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
// Pause at 3.5s
systemClock.UtcNow += TimeSpan.FromSeconds(0.5);
_timeoutControl.PauseTimingReads();
_timeoutControl.StopTimingRead();
// Tick at 4s, expected counted time is 4s (first tick after pause goes through), expected data rate is 150 bytes/second
systemClock.UtcNow += TimeSpan.FromSeconds(0.5);
@ -177,7 +180,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
// Resume at 6.5s
systemClock.UtcNow += TimeSpan.FromSeconds(0.5);
_timeoutControl.ResumeTimingReads();
_timeoutControl.StartTimingRead();
// Tick at 9s, expected counted time is 6s, expected data rate is 100 bytes/second
systemClock.UtcNow += TimeSpan.FromSeconds(1.5);
@ -203,7 +206,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
// Initialize timestamp
_timeoutControl.Tick(systemClock.UtcNow);
_timeoutControl.StartTimingReads(minRate);
_timeoutControl.StartRequestBody(minRate);
_timeoutControl.StartTimingRead();
// Tick at 2s, expected counted time is 2s, expected data rate is 100 bytes/second
systemClock.UtcNow += TimeSpan.FromSeconds(2);
@ -215,11 +219,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
// Pause at 2.25s
systemClock.UtcNow += TimeSpan.FromSeconds(0.25);
_timeoutControl.PauseTimingReads();
_timeoutControl.StopTimingRead();
// Resume at 2.5s
systemClock.UtcNow += TimeSpan.FromSeconds(0.25);
_timeoutControl.ResumeTimingReads();
_timeoutControl.StartTimingRead();
// Tick at 3s, expected counted time is 3s, expected data rate is 100 bytes/second
systemClock.UtcNow += TimeSpan.FromSeconds(0.5);
@ -249,7 +253,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
// Initialize timestamp
_timeoutControl.Tick(startTime);
_timeoutControl.StartTimingReads(minRate);
_timeoutControl.StartRequestBody(minRate);
_timeoutControl.StartTimingRead();
_timeoutControl.SetTimeout(timeout.Ticks, TimeoutReason.RequestBodyDrain);
@ -394,7 +399,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
var now = DateTimeOffset.UtcNow;
_timeoutControl.Tick(now);
_timeoutControl.StartTimingReads(minRate);
_timeoutControl.StartRequestBody(minRate);
_timeoutControl.StartTimingRead();
// Tick after grace period w/ low data rate
now += gracePeriod + TimeSpan.FromSeconds(1);

View File

@ -20,6 +20,7 @@ using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.FlowControl;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal;
@ -1196,24 +1197,29 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
}
public virtual void StartTimingReads(MinDataRate minRate)
public virtual void InitializeHttp2(InputFlowControl connectionInputFlowControl)
{
_realTimeoutControl.StartTimingReads(minRate);
_realTimeoutControl.InitializeHttp2(connectionInputFlowControl);
}
public virtual void PauseTimingReads()
public virtual void StartRequestBody(MinDataRate minRate)
{
_realTimeoutControl.PauseTimingReads();
_realTimeoutControl.StartRequestBody(minRate);
}
public virtual void ResumeTimingReads()
public virtual void StopTimingRead()
{
_realTimeoutControl.ResumeTimingReads();
_realTimeoutControl.StopTimingRead();
}
public virtual void StopTimingReads()
public virtual void StartTimingRead()
{
_realTimeoutControl.StopTimingReads();
_realTimeoutControl.StartTimingRead();
}
public virtual void StopRequestBody()
{
_realTimeoutControl.StopRequestBody();
}
public virtual void BytesRead(long count)

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
@ -40,6 +41,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
_mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.KeepAlive), Times.Once);
await WaitForConnectionStopAsync(expectedLastStreamId: 0, ignoreNonGoAwayFrames: false);
_mockTimeoutHandler.VerifyNoOtherCalls();
}
[Fact]
@ -63,6 +66,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
_mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.KeepAlive), Times.Once);
await WaitForConnectionStopAsync(expectedLastStreamId: 0, ignoreNonGoAwayFrames: false);
_mockTimeoutHandler.VerifyNoOtherCalls();
}
[Fact]
@ -119,6 +124,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
_mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.KeepAlive), Times.Once);
await WaitForConnectionStopAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
_mockTimeoutHandler.VerifyNoOtherCalls();
}
[Fact]
@ -155,6 +162,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
_mockConnectionContext.Verify(c =>c.Abort(It.Is<ConnectionAbortedException>(e =>
e.Message == CoreStrings.BadRequest_RequestHeadersTimeout)), Times.Once);
_mockTimeoutHandler.VerifyNoOtherCalls();
_mockConnectionContext.VerifyNoOtherCalls();
}
[Fact]
@ -186,6 +196,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
_mockConnectionContext.Verify(c =>c.Abort(It.Is<ConnectionAbortedException>(e =>
e.Message == CoreStrings.ConnectionTimedBecauseResponseMininumDataRateNotSatisfied)), Times.Once);
_mockTimeoutHandler.VerifyNoOtherCalls();
_mockConnectionContext.VerifyNoOtherCalls();
}
[Theory]
@ -249,6 +262,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
var mockSystemClock = _serviceContext.MockSystemClock;
var limits = _serviceContext.ServerOptions.Limits;
// Use non-default value to ensure the min request and response rates aren't mixed up.
limits.MinResponseDataRate = new MinDataRate(480, TimeSpan.FromSeconds(2.5));
// Disable response buffering so "socket" backpressure is observed immediately.
limits.MaxResponseBufferSize = 0;
@ -264,6 +280,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
// Complete timing of the request body so we don't induce any unexpected request body rate timeouts.
_timeoutControl.Tick(mockSystemClock.UtcNow);
// Don't read data frame to induce "socket" backpressure.
mockSystemClock.UtcNow += limits.MinResponseDataRate.GracePeriod + Heartbeat.Interval;
_timeoutControl.Tick(mockSystemClock.UtcNow);
@ -289,6 +308,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
_mockConnectionContext.Verify(c =>c.Abort(It.Is<ConnectionAbortedException>(e =>
e.Message == CoreStrings.ConnectionTimedBecauseResponseMininumDataRateNotSatisfied)), Times.Once);
_mockTimeoutHandler.VerifyNoOtherCalls();
_mockConnectionContext.VerifyNoOtherCalls();
}
[Fact]
@ -297,6 +319,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
var mockSystemClock = _serviceContext.MockSystemClock;
var limits = _serviceContext.ServerOptions.Limits;
// Use non-default value to ensure the min request and response rates aren't mixed up.
limits.MinResponseDataRate = new MinDataRate(480, TimeSpan.FromSeconds(2.5));
// Disable response buffering so "socket" backpressure is observed immediately.
limits.MaxResponseBufferSize = 0;
@ -312,6 +337,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
// Complete timing of the request body so we don't induce any unexpected request body rate timeouts.
_timeoutControl.Tick(mockSystemClock.UtcNow);
var timeToWriteMaxData = TimeSpan.FromSeconds(_maxData.Length / limits.MinResponseDataRate.BytesPerSecond);
// Don't read data frame to induce "socket" backpressure.
@ -325,7 +353,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
_mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.WriteDataRate), Times.Once);
// The "hello, world" bytes are buffered from before the timeout, but not an END_STREAM data frame.
// The _maxData bytes are buffered from before the timeout, but not an END_STREAM data frame.
await ExpectAsync(Http2FrameType.DATA,
withLength: _maxData.Length,
withFlags: (byte)Http2DataFrameFlags.NONE,
@ -339,6 +367,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
_mockConnectionContext.Verify(c => c.Abort(It.Is<ConnectionAbortedException>(e =>
e.Message == CoreStrings.ConnectionTimedBecauseResponseMininumDataRateNotSatisfied)), Times.Once);
_mockTimeoutHandler.VerifyNoOtherCalls();
_mockConnectionContext.VerifyNoOtherCalls();
}
[Fact]
@ -347,6 +378,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
var mockSystemClock = _serviceContext.MockSystemClock;
var limits = _serviceContext.ServerOptions.Limits;
// Use non-default value to ensure the min request and response rates aren't mixed up.
limits.MinResponseDataRate = new MinDataRate(480, TimeSpan.FromSeconds(2.5));
// This only affects the stream windows. The connection-level window is always initialized at 64KiB.
_clientSettings.InitialWindowSize = 6;
@ -366,6 +400,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
withFlags: (byte)Http2DataFrameFlags.NONE,
withStreamId: 1);
// Complete timing of the request body so we don't induce any unexpected request body rate timeouts.
_timeoutControl.Tick(mockSystemClock.UtcNow);
// Don't send WINDOW_UPDATE to induce flow-control backpressure
mockSystemClock.UtcNow += limits.MinResponseDataRate.GracePeriod + Heartbeat.Interval;
_timeoutControl.Tick(mockSystemClock.UtcNow);
@ -385,6 +422,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
_mockConnectionContext.Verify(c => c.Abort(It.Is<ConnectionAbortedException>(e =>
e.Message == CoreStrings.ConnectionTimedBecauseResponseMininumDataRateNotSatisfied)), Times.Once);
_mockTimeoutHandler.VerifyNoOtherCalls();
_mockConnectionContext.VerifyNoOtherCalls();
}
[Fact]
@ -393,6 +433,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
var mockSystemClock = _serviceContext.MockSystemClock;
var limits = _serviceContext.ServerOptions.Limits;
// Use non-default value to ensure the min request and response rates aren't mixed up.
limits.MinResponseDataRate = new MinDataRate(480, TimeSpan.FromSeconds(2.5));
// This only affects the stream windows. The connection-level window is always initialized at 64KiB.
_clientSettings.InitialWindowSize = (uint)_maxData.Length - 1;
@ -412,6 +455,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
withFlags: (byte)Http2DataFrameFlags.NONE,
withStreamId: 1);
// Complete timing of the request body so we don't induce any unexpected request body rate timeouts.
_timeoutControl.Tick(mockSystemClock.UtcNow);
var timeToWriteMaxData = TimeSpan.FromSeconds(_clientSettings.InitialWindowSize / limits.MinResponseDataRate.BytesPerSecond);
// Don't send WINDOW_UPDATE to induce flow-control backpressure
@ -433,6 +479,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
_mockConnectionContext.Verify(c => c.Abort(It.Is<ConnectionAbortedException>(e =>
e.Message == CoreStrings.ConnectionTimedBecauseResponseMininumDataRateNotSatisfied)), Times.Once);
_mockTimeoutHandler.VerifyNoOtherCalls();
_mockConnectionContext.VerifyNoOtherCalls();
}
[Fact]
@ -441,6 +490,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
var mockSystemClock = _serviceContext.MockSystemClock;
var limits = _serviceContext.ServerOptions.Limits;
// Use non-default value to ensure the min request and response rates aren't mixed up.
limits.MinResponseDataRate = new MinDataRate(480, TimeSpan.FromSeconds(2.5));
// This only affects the stream windows. The connection-level window is always initialized at 64KiB.
_clientSettings.InitialWindowSize = (uint)_maxData.Length - 1;
@ -472,6 +524,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
withFlags: (byte)Http2DataFrameFlags.NONE,
withStreamId: 3);
// Complete timing of the request bodies so we don't induce any unexpected request body rate timeouts.
_timeoutControl.Tick(mockSystemClock.UtcNow);
var timeToWriteMaxData = TimeSpan.FromSeconds(_clientSettings.InitialWindowSize / limits.MinResponseDataRate.BytesPerSecond);
// Double the timeout for the second stream.
timeToWriteMaxData += timeToWriteMaxData;
@ -495,6 +550,353 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
_mockConnectionContext.Verify(c => c.Abort(It.Is<ConnectionAbortedException>(e =>
e.Message == CoreStrings.ConnectionTimedBecauseResponseMininumDataRateNotSatisfied)), Times.Once);
_mockTimeoutHandler.VerifyNoOtherCalls();
_mockConnectionContext.VerifyNoOtherCalls();
}
[Fact]
public async Task DATA_Received_TooSlowlyOnSmallRead_AbortsConnectionAfterGracePeriod()
{
var mockSystemClock = _serviceContext.MockSystemClock;
var limits = _serviceContext.ServerOptions.Limits;
// Use non-default value to ensure the min request and response rates aren't mixed up.
limits.MinRequestBodyDataRate = new MinDataRate(480, TimeSpan.FromSeconds(2.5));
_timeoutControl.Initialize(mockSystemClock.UtcNow);
await InitializeConnectionAsync(_echoApplication);
// _helloWorldBytes is 12 bytes, and 12 bytes / 240 bytes/sec = .05 secs which is far below the grace period.
await StartStreamAsync(1, _browserRequestHeaders, endStream: false);
await SendDataAsync(1, _helloWorldBytes, endStream: false);
await ExpectAsync(Http2FrameType.HEADERS,
withLength: 37,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
await ExpectAsync(Http2FrameType.DATA,
withLength: _helloWorldBytes.Length,
withFlags: (byte)Http2DataFrameFlags.NONE,
withStreamId: 1);
// Don't send any more data and advance just to and then past the grace period.
mockSystemClock.UtcNow += limits.MinRequestBodyDataRate.GracePeriod;
_timeoutControl.Tick(mockSystemClock.UtcNow);
_mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny<TimeoutReason>()), Times.Never);
mockSystemClock.UtcNow += TimeSpan.FromTicks(1);
_timeoutControl.Tick(mockSystemClock.UtcNow);
_mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.ReadDataRate), Times.Once);
await WaitForConnectionErrorAsync<ConnectionAbortedException>(
ignoreNonGoAwayFrames: false,
expectedLastStreamId: 1,
Http2ErrorCode.INTERNAL_ERROR,
null);
_mockConnectionContext.Verify(c => c.Abort(It.Is<ConnectionAbortedException>(e =>
e.Message == CoreStrings.BadRequest_RequestBodyTimeout)), Times.Once);
_mockTimeoutHandler.VerifyNoOtherCalls();
_mockConnectionContext.VerifyNoOtherCalls();
}
[Fact]
public async Task DATA_Received_TooSlowlyOnLargeRead_AbortsConnectionAfterRateTimeout()
{
var mockSystemClock = _serviceContext.MockSystemClock;
var limits = _serviceContext.ServerOptions.Limits;
// Use non-default value to ensure the min request and response rates aren't mixed up.
limits.MinRequestBodyDataRate = new MinDataRate(480, TimeSpan.FromSeconds(2.5));
_timeoutControl.Initialize(mockSystemClock.UtcNow);
await InitializeConnectionAsync(_echoApplication);
// _maxData is 16 KiB, and 16 KiB / 240 bytes/sec ~= 68 secs which is far above the grace period.
await StartStreamAsync(1, _browserRequestHeaders, endStream: false);
await SendDataAsync(1, _maxData, endStream: false);
await ExpectAsync(Http2FrameType.HEADERS,
withLength: 37,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
await ExpectAsync(Http2FrameType.DATA,
withLength: _maxData.Length,
withFlags: (byte)Http2DataFrameFlags.NONE,
withStreamId: 1);
// Due to the imprecision of floating point math and the fact that TimeoutControl derives rate from elapsed
// time for reads instead of vice versa like for writes, use a half-second instead of single-tick cushion.
var timeToReadMaxData = TimeSpan.FromSeconds(_maxData.Length / limits.MinRequestBodyDataRate.BytesPerSecond) - TimeSpan.FromSeconds(.5);
// Don't send any more data and advance just to and then past the rate timeout.
mockSystemClock.UtcNow += timeToReadMaxData;
_timeoutControl.Tick(mockSystemClock.UtcNow);
_mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny<TimeoutReason>()), Times.Never);
mockSystemClock.UtcNow += TimeSpan.FromSeconds(1);
_timeoutControl.Tick(mockSystemClock.UtcNow);
_mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.ReadDataRate), Times.Once);
await WaitForConnectionErrorAsync<ConnectionAbortedException>(
ignoreNonGoAwayFrames: false,
expectedLastStreamId: 1,
Http2ErrorCode.INTERNAL_ERROR,
null);
_mockConnectionContext.Verify(c => c.Abort(It.Is<ConnectionAbortedException>(e =>
e.Message == CoreStrings.BadRequest_RequestBodyTimeout)), Times.Once);
_mockTimeoutHandler.VerifyNoOtherCalls();
_mockConnectionContext.VerifyNoOtherCalls();
}
[Fact]
public async Task DATA_Received_TooSlowlyOnMultipleStreams_AbortsConnectionAfterAdditiveRateTimeout()
{
var mockSystemClock = _serviceContext.MockSystemClock;
var limits = _serviceContext.ServerOptions.Limits;
// Use non-default value to ensure the min request and response rates aren't mixed up.
limits.MinRequestBodyDataRate = new MinDataRate(480, TimeSpan.FromSeconds(2.5));
_timeoutControl.Initialize(mockSystemClock.UtcNow);
await InitializeConnectionAsync(_echoApplication);
// _maxData is 16 KiB, and 16 KiB / 240 bytes/sec ~= 68 secs which is far above the grace period.
await StartStreamAsync(1, _browserRequestHeaders, endStream: false);
await SendDataAsync(1, _maxData, endStream: false);
await ExpectAsync(Http2FrameType.HEADERS,
withLength: 37,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
await ExpectAsync(Http2FrameType.DATA,
withLength: _maxData.Length,
withFlags: (byte)Http2DataFrameFlags.NONE,
withStreamId: 1);
await StartStreamAsync(3, _browserRequestHeaders, endStream: false);
await SendDataAsync(3, _maxData, endStream: false);
await ExpectAsync(Http2FrameType.HEADERS,
withLength: 37,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 3);
await ExpectAsync(Http2FrameType.DATA,
withLength: _maxData.Length,
withFlags: (byte)Http2DataFrameFlags.NONE,
withStreamId: 3);
var timeToReadMaxData = TimeSpan.FromSeconds(_maxData.Length / limits.MinRequestBodyDataRate.BytesPerSecond);
// Double the timeout for the second stream.
timeToReadMaxData += timeToReadMaxData;
// Due to the imprecision of floating point math and the fact that TimeoutControl derives rate from elapsed
// time for reads instead of vice versa like for writes, use a half-second instead of single-tick cushion.
timeToReadMaxData -= TimeSpan.FromSeconds(.5);
// Don't send any more data and advance just to and then past the rate timeout.
mockSystemClock.UtcNow += timeToReadMaxData;
_timeoutControl.Tick(mockSystemClock.UtcNow);
_mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny<TimeoutReason>()), Times.Never);
mockSystemClock.UtcNow += TimeSpan.FromSeconds(1);
_timeoutControl.Tick(mockSystemClock.UtcNow);
_mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.ReadDataRate), Times.Once);
await WaitForConnectionErrorAsync<ConnectionAbortedException>(
ignoreNonGoAwayFrames: false,
expectedLastStreamId: 3,
Http2ErrorCode.INTERNAL_ERROR,
null);
_mockConnectionContext.Verify(c => c.Abort(It.Is<ConnectionAbortedException>(e =>
e.Message == CoreStrings.BadRequest_RequestBodyTimeout)), Times.Once);
_mockTimeoutHandler.VerifyNoOtherCalls();
_mockConnectionContext.VerifyNoOtherCalls();
}
[Fact]
public async Task DATA_Received_TooSlowlyOnSecondStream_AbortsConnectionAfterNonAdditiveRateTimeout()
{
var mockSystemClock = _serviceContext.MockSystemClock;
var limits = _serviceContext.ServerOptions.Limits;
// Use non-default value to ensure the min request and response rates aren't mixed up.
limits.MinRequestBodyDataRate = new MinDataRate(480, TimeSpan.FromSeconds(2.5));
_timeoutControl.Initialize(mockSystemClock.UtcNow);
await InitializeConnectionAsync(_echoApplication);
// _maxData is 16 KiB, and 16 KiB / 240 bytes/sec ~= 68 secs which is far above the grace period.
await StartStreamAsync(1, _browserRequestHeaders, endStream: false);
await SendDataAsync(1, _maxData, endStream: true);
await ExpectAsync(Http2FrameType.HEADERS,
withLength: 37,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
await ExpectAsync(Http2FrameType.DATA,
withLength: _maxData.Length,
withFlags: (byte)Http2DataFrameFlags.NONE,
withStreamId: 1);
await ExpectAsync(Http2FrameType.DATA,
withLength: 0,
withFlags: (byte)Http2DataFrameFlags.END_STREAM,
withStreamId: 1);
await StartStreamAsync(3, _browserRequestHeaders, endStream: false);
await SendDataAsync(3, _maxData, endStream: false);
await ExpectAsync(Http2FrameType.HEADERS,
withLength: 37,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 3);
await ExpectAsync(Http2FrameType.DATA,
withLength: _maxData.Length,
withFlags: (byte)Http2DataFrameFlags.NONE,
withStreamId: 3);
// Due to the imprecision of floating point math and the fact that TimeoutControl derives rate from elapsed
// time for reads instead of vice versa like for writes, use a half-second instead of single-tick cushion.
var timeToReadMaxData = TimeSpan.FromSeconds(_maxData.Length / limits.MinRequestBodyDataRate.BytesPerSecond) - TimeSpan.FromSeconds(.5);
// Don't send any more data and advance just to and then past the rate timeout.
mockSystemClock.UtcNow += timeToReadMaxData;
_timeoutControl.Tick(mockSystemClock.UtcNow);
_mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny<TimeoutReason>()), Times.Never);
mockSystemClock.UtcNow += TimeSpan.FromSeconds(1);
_timeoutControl.Tick(mockSystemClock.UtcNow);
_mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.ReadDataRate), Times.Once);
await WaitForConnectionErrorAsync<ConnectionAbortedException>(
ignoreNonGoAwayFrames: false,
expectedLastStreamId: 3,
Http2ErrorCode.INTERNAL_ERROR,
null);
_mockConnectionContext.Verify(c => c.Abort(It.Is<ConnectionAbortedException>(e =>
e.Message == CoreStrings.BadRequest_RequestBodyTimeout)), Times.Once);
_mockTimeoutHandler.VerifyNoOtherCalls();
_mockConnectionContext.VerifyNoOtherCalls();
}
[Fact]
public async Task DATA_Received_SlowlyDueToConnectionFlowControl_DoesNotAbortConnection()
{
var initialConnectionWindowSize = _serviceContext.ServerOptions.Limits.Http2.InitialConnectionWindowSize;
var framesConnectionInWindow = initialConnectionWindowSize / Http2PeerSettings.DefaultMaxFrameSize;
var backpressureTcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
var mockSystemClock = _serviceContext.MockSystemClock;
var limits = _serviceContext.ServerOptions.Limits;
// Use non-default value to ensure the min request and response rates aren't mixed up.
limits.MinRequestBodyDataRate = new MinDataRate(480, TimeSpan.FromSeconds(2.5));
_timeoutControl.Initialize(mockSystemClock.UtcNow);
await InitializeConnectionAsync(async context =>
{
var streamId = context.Features.Get<IHttp2StreamIdFeature>().StreamId;
if (streamId == 1)
{
await backpressureTcs.Task;
}
else
{
await _echoApplication(context);
}
});
await StartStreamAsync(1, _browserRequestHeaders, endStream: false);
for (var i = 0; i < framesConnectionInWindow / 2; i++)
{
await SendDataAsync(1, _maxData, endStream: false);
}
await SendDataAsync(1, _maxData, endStream: true);
await StartStreamAsync(3, _browserRequestHeaders, endStream: false);
await SendDataAsync(3, _helloWorldBytes, endStream: false);
await ExpectAsync(Http2FrameType.HEADERS,
withLength: 37,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 3);
await ExpectAsync(Http2FrameType.DATA,
withLength: _helloWorldBytes.Length,
withFlags: (byte)Http2DataFrameFlags.NONE,
withStreamId: 3);
// No matter how much time elapses there is no read timeout because the connection window is too small.
mockSystemClock.UtcNow += TimeSpan.FromDays(365);
_timeoutControl.Tick(mockSystemClock.UtcNow);
_mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny<TimeoutReason>()), Times.Never);
// Opening the connection window starts the read rate timeout enforcement after that point.
backpressureTcs.SetResult(null);
await ExpectAsync(Http2FrameType.HEADERS,
withLength: 55,
withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
withStreamId: 1);
await ExpectAsync(Http2FrameType.DATA,
withLength: 0,
withFlags: (byte)Http2DataFrameFlags.END_STREAM,
withStreamId: 1);
await ExpectAsync(Http2FrameType.WINDOW_UPDATE,
withLength: 4,
withFlags: (byte)Http2DataFrameFlags.NONE,
withStreamId: 0);
mockSystemClock.UtcNow += limits.MinRequestBodyDataRate.GracePeriod;
_timeoutControl.Tick(mockSystemClock.UtcNow);
_mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny<TimeoutReason>()), Times.Never);
mockSystemClock.UtcNow += TimeSpan.FromTicks(1);
_timeoutControl.Tick(mockSystemClock.UtcNow);
_mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.ReadDataRate), Times.Once);
await WaitForConnectionErrorAsync<ConnectionAbortedException>(
ignoreNonGoAwayFrames: false,
expectedLastStreamId: 3,
Http2ErrorCode.INTERNAL_ERROR,
null);
_mockConnectionContext.Verify(c => c.Abort(It.Is<ConnectionAbortedException>(e =>
e.Message == CoreStrings.BadRequest_RequestBodyTimeout)), Times.Once);
_mockTimeoutHandler.VerifyNoOtherCalls();
_mockConnectionContext.VerifyNoOtherCalls();
}
}
}

View File

@ -63,8 +63,6 @@ namespace CodeGenerator
"IHttpRequestLifetimeFeature",
"IHttpConnectionFeature",
"IHttpMaxRequestBodySizeFeature",
"IHttpMinRequestBodyDataRateFeature",
"IHttpMinResponseDataRateFeature",
"IHttpBodyControlFeature",
};