Add response minimum data rate feature.
This commit is contained in:
parent
5185ebe45f
commit
eca4bfe6c3
|
|
@ -342,4 +342,7 @@
|
|||
<data name="SynchronousWritesDisallowed" xml:space="preserve">
|
||||
<value>Synchronous operations are disallowed. Call WriteAsync or set AllowSynchronousIO to true instead.</value>
|
||||
</data>
|
||||
<data name="PositiveNumberOrNullMinDataRateRequired" xml:space="preserve">
|
||||
<value>Value must be a positive number. To disable a minimum data rate, use null where a MinDataRate instance is expected.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
@ -4,12 +4,12 @@
|
|||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Features
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a minimum data rate for the request body of an HTTP request.
|
||||
/// Feature to set the minimum data rate at which the the request body must be sent by the client.
|
||||
/// </summary>
|
||||
public interface IHttpMinRequestBodyDataRateFeature
|
||||
{
|
||||
/// <summary>
|
||||
/// The minimum data rate in bytes/second at which the request body should be received.
|
||||
/// 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.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
// 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.Features
|
||||
{
|
||||
/// <summary>
|
||||
/// Feature to set the minimum data rate at which the response must be received by the client.
|
||||
/// </summary>
|
||||
public interface IHttpMinResponseDataRateFeature
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
MinDataRate MinDataRate { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -7,8 +7,8 @@ using System.Diagnostics;
|
|||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.Hosting.Server;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Adapter.Internal;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
||||
|
|
@ -35,6 +35,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
|
|||
private long _readTimingElapsedTicks;
|
||||
private long _readTimingBytesRead;
|
||||
|
||||
private object _writeTimingLock = new object();
|
||||
private int _writeTimingWrites;
|
||||
private long _writeTimingTimeoutTimestamp;
|
||||
|
||||
private Task _lifetimeTask;
|
||||
|
||||
public FrameConnection(FrameConnectionContext context)
|
||||
|
|
@ -46,7 +50,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
|
|||
internal Frame Frame => _frame;
|
||||
internal IDebugger Debugger { get; set; } = DebuggerWrapper.Singleton;
|
||||
|
||||
|
||||
public bool TimedOut { get; private set; }
|
||||
|
||||
public string ConnectionId => _context.ConnectionId;
|
||||
|
|
@ -207,7 +210,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
|
|||
Debug.Assert(_frame != null, $"{nameof(_frame)} is null");
|
||||
|
||||
TimedOut = true;
|
||||
_readTimingEnabled = false;
|
||||
_frame.Stop();
|
||||
}
|
||||
|
||||
|
|
@ -262,6 +264,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
|
|||
|
||||
var timestamp = now.Ticks;
|
||||
|
||||
CheckForTimeout(timestamp);
|
||||
CheckForReadDataRateTimeout(timestamp);
|
||||
CheckForWriteDataRateTimeout(timestamp);
|
||||
|
||||
Interlocked.Exchange(ref _lastTimestamp, timestamp);
|
||||
}
|
||||
|
||||
private void CheckForTimeout(long timestamp)
|
||||
{
|
||||
if (TimedOut)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Use PlatformApis.VolatileRead equivalent again
|
||||
if (timestamp > Interlocked.Read(ref _timeoutTimestamp))
|
||||
{
|
||||
|
|
@ -277,42 +293,67 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
|
|||
Timeout();
|
||||
}
|
||||
}
|
||||
else
|
||||
}
|
||||
|
||||
private void CheckForReadDataRateTimeout(long timestamp)
|
||||
{
|
||||
// The only time when both a timeout is set and the read data rate could be enforced is
|
||||
// when draining the request body. Since there's already a (short) timeout set for draining,
|
||||
// it's safe to not check the data rate at this point.
|
||||
if (TimedOut || Interlocked.Read(ref _timeoutTimestamp) != long.MaxValue)
|
||||
{
|
||||
lock (_readTimingLock)
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_readTimingLock)
|
||||
{
|
||||
if (_readTimingEnabled)
|
||||
{
|
||||
if (_readTimingEnabled)
|
||||
// Reference in local var to avoid torn reads in case the min rate is changed via IHttpMinRequestBodyDataRateFeature
|
||||
var minRequestBodyDataRate = _frame.MinRequestBodyDataRate;
|
||||
|
||||
_readTimingElapsedTicks += timestamp - _lastTimestamp;
|
||||
|
||||
if (minRequestBodyDataRate?.BytesPerSecond > 0 && _readTimingElapsedTicks > minRequestBodyDataRate.GracePeriod.Ticks)
|
||||
{
|
||||
// Reference in local var to avoid torn reads in case the min rate is changed via IHttpMinRequestBodyDataRateFeature
|
||||
var minRequestBodyDataRate = _frame.MinRequestBodyDataRate;
|
||||
var elapsedSeconds = (double)_readTimingElapsedTicks / TimeSpan.TicksPerSecond;
|
||||
var rate = Interlocked.Read(ref _readTimingBytesRead) / elapsedSeconds;
|
||||
|
||||
_readTimingElapsedTicks += timestamp - _lastTimestamp;
|
||||
|
||||
if (minRequestBodyDataRate?.BytesPerSecond > 0 && _readTimingElapsedTicks > minRequestBodyDataRate.GracePeriod.Ticks)
|
||||
if (rate < minRequestBodyDataRate.BytesPerSecond && !Debugger.IsAttached)
|
||||
{
|
||||
var elapsedSeconds = (double)_readTimingElapsedTicks / TimeSpan.TicksPerSecond;
|
||||
var rate = Interlocked.Read(ref _readTimingBytesRead) / elapsedSeconds;
|
||||
|
||||
if (rate < minRequestBodyDataRate.BytesPerSecond && !Debugger.IsAttached)
|
||||
{
|
||||
Log.RequestBodyMininumDataRateNotSatisfied(_context.ConnectionId, _frame.TraceIdentifier, minRequestBodyDataRate.BytesPerSecond);
|
||||
Timeout();
|
||||
}
|
||||
Log.RequestBodyMininumDataRateNotSatisfied(_context.ConnectionId, _frame.TraceIdentifier, minRequestBodyDataRate.BytesPerSecond);
|
||||
Timeout();
|
||||
}
|
||||
}
|
||||
|
||||
// PauseTimingReads() cannot just set _timingReads to false. It needs to go through at least one tick
|
||||
// before pausing, otherwise _readTimingElapsed might never be updated if PauseTimingReads() is always
|
||||
// called before the next tick.
|
||||
if (_readTimingPauseRequested)
|
||||
{
|
||||
_readTimingEnabled = false;
|
||||
_readTimingPauseRequested = false;
|
||||
}
|
||||
// PauseTimingReads() cannot just set _timingReads to false. It needs to go through at least one tick
|
||||
// before pausing, otherwise _readTimingElapsed might never be updated if PauseTimingReads() is always
|
||||
// called before the next tick.
|
||||
if (_readTimingPauseRequested)
|
||||
{
|
||||
_readTimingEnabled = false;
|
||||
_readTimingPauseRequested = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Interlocked.Exchange(ref _lastTimestamp, timestamp);
|
||||
private void CheckForWriteDataRateTimeout(long timestamp)
|
||||
{
|
||||
if (TimedOut)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_writeTimingLock)
|
||||
{
|
||||
if (_writeTimingWrites > 0 && timestamp > _writeTimingTimeoutTimestamp && !Debugger.IsAttached)
|
||||
{
|
||||
TimedOut = true;
|
||||
Log.ResponseMininumDataRateNotSatisfied(_frame.ConnectionIdFeature, _frame.TraceIdentifier);
|
||||
Abort(new TimeoutException());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void SetTimeout(long ticks, TimeoutAction timeoutAction)
|
||||
|
|
@ -381,5 +422,37 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
|
|||
{
|
||||
Interlocked.Add(ref _readTimingBytesRead, count);
|
||||
}
|
||||
|
||||
public void StartTimingWrite(int size)
|
||||
{
|
||||
lock (_writeTimingLock)
|
||||
{
|
||||
var minResponseDataRate = _frame.MinResponseDataRate;
|
||||
|
||||
if (minResponseDataRate != null)
|
||||
{
|
||||
var timeoutTicks = Math.Max(
|
||||
minResponseDataRate.GracePeriod.Ticks,
|
||||
TimeSpan.FromSeconds(size / minResponseDataRate.BytesPerSecond).Ticks);
|
||||
|
||||
if (_writeTimingWrites == 0)
|
||||
{
|
||||
// Add Heartbeat.Interval since this can be called right before the next heartbeat.
|
||||
_writeTimingTimeoutTimestamp = _lastTimestamp + Heartbeat.Interval.Ticks;
|
||||
}
|
||||
|
||||
_writeTimingTimeoutTimestamp += timeoutTicks;
|
||||
_writeTimingWrites++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void StopTimingWrite()
|
||||
{
|
||||
lock (_writeTimingLock)
|
||||
{
|
||||
_writeTimingWrites--;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
IHttpRequestIdentifierFeature,
|
||||
IHttpBodyControlFeature,
|
||||
IHttpMaxRequestBodySizeFeature,
|
||||
IHttpMinRequestBodyDataRateFeature
|
||||
IHttpMinRequestBodyDataRateFeature,
|
||||
IHttpMinResponseDataRateFeature
|
||||
{
|
||||
// NOTE: When feature interfaces are added to or removed from this Frame class implementation,
|
||||
// then the list of `implementedFeatures` in the generated code project MUST also be updated.
|
||||
|
|
@ -242,6 +243,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
set => MinRequestBodyDataRate = value;
|
||||
}
|
||||
|
||||
MinDataRate IHttpMinResponseDataRateFeature.MinDataRate
|
||||
{
|
||||
get => MinResponseDataRate;
|
||||
set => MinResponseDataRate = value;
|
||||
}
|
||||
|
||||
object IFeatureCollection.this[Type key]
|
||||
{
|
||||
get => FastFeatureGet(key);
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
private static readonly Type ISessionFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.ISessionFeature);
|
||||
private static readonly Type IHttpMaxRequestBodySizeFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpMaxRequestBodySizeFeature);
|
||||
private static readonly Type IHttpMinRequestBodyDataRateFeatureType = typeof(global::Microsoft.AspNetCore.Server.Kestrel.Core.Features.IHttpMinRequestBodyDataRateFeature);
|
||||
private static readonly Type IHttpMinResponseDataRateFeatureType = typeof(global::Microsoft.AspNetCore.Server.Kestrel.Core.Features.IHttpMinResponseDataRateFeature);
|
||||
private static readonly Type IHttpBodyControlFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpBodyControlFeature);
|
||||
private static readonly Type IHttpSendFileFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpSendFileFeature);
|
||||
|
||||
|
|
@ -45,6 +46,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
private object _currentISessionFeature;
|
||||
private object _currentIHttpMaxRequestBodySizeFeature;
|
||||
private object _currentIHttpMinRequestBodyDataRateFeature;
|
||||
private object _currentIHttpMinResponseDataRateFeature;
|
||||
private object _currentIHttpBodyControlFeature;
|
||||
private object _currentIHttpSendFileFeature;
|
||||
|
||||
|
|
@ -58,6 +60,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
_currentIHttpConnectionFeature = this;
|
||||
_currentIHttpMaxRequestBodySizeFeature = this;
|
||||
_currentIHttpMinRequestBodyDataRateFeature = this;
|
||||
_currentIHttpMinResponseDataRateFeature = this;
|
||||
_currentIHttpBodyControlFeature = this;
|
||||
|
||||
_currentIServiceProvidersFeature = null;
|
||||
|
|
@ -142,6 +145,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
{
|
||||
return _currentIHttpMinRequestBodyDataRateFeature;
|
||||
}
|
||||
if (key == IHttpMinResponseDataRateFeatureType)
|
||||
{
|
||||
return _currentIHttpMinResponseDataRateFeature;
|
||||
}
|
||||
if (key == IHttpBodyControlFeatureType)
|
||||
{
|
||||
return _currentIHttpBodyControlFeature;
|
||||
|
|
@ -242,6 +249,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
_currentIHttpMinRequestBodyDataRateFeature = feature;
|
||||
return;
|
||||
}
|
||||
if (key == IHttpMinResponseDataRateFeatureType)
|
||||
{
|
||||
_currentIHttpMinResponseDataRateFeature = feature;
|
||||
return;
|
||||
}
|
||||
if (key == IHttpBodyControlFeatureType)
|
||||
{
|
||||
_currentIHttpBodyControlFeature = feature;
|
||||
|
|
@ -325,6 +337,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
{
|
||||
yield return new KeyValuePair<Type, object>(IHttpMinRequestBodyDataRateFeatureType, _currentIHttpMinRequestBodyDataRateFeature as global::Microsoft.AspNetCore.Server.Kestrel.Core.Features.IHttpMinRequestBodyDataRateFeature);
|
||||
}
|
||||
if (_currentIHttpMinResponseDataRateFeature != null)
|
||||
{
|
||||
yield return new KeyValuePair<Type, object>(IHttpMinResponseDataRateFeatureType, _currentIHttpMinResponseDataRateFeature as global::Microsoft.AspNetCore.Server.Kestrel.Core.Features.IHttpMinResponseDataRateFeature);
|
||||
}
|
||||
if (_currentIHttpBodyControlFeature != null)
|
||||
{
|
||||
yield return new KeyValuePair<Type, object>(IHttpBodyControlFeatureType, _currentIHttpBodyControlFeature as global::Microsoft.AspNetCore.Http.Features.IHttpBodyControlFeature);
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
_keepAliveTicks = ServerOptions.Limits.KeepAliveTimeout.Ticks;
|
||||
_requestHeadersTimeoutTicks = ServerOptions.Limits.RequestHeadersTimeout.Ticks;
|
||||
|
||||
Output = new OutputProducer(frameContext.Output, frameContext.ConnectionId, frameContext.ServiceContext.Log);
|
||||
Output = new OutputProducer(frameContext.Output, frameContext.ConnectionId, frameContext.ServiceContext.Log, TimeoutControl);
|
||||
RequestBodyPipe = CreateRequestBodyPipe();
|
||||
}
|
||||
|
||||
|
|
@ -302,6 +302,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
|
||||
public MinDataRate MinRequestBodyDataRate { get; set; }
|
||||
|
||||
public MinDataRate MinResponseDataRate { get; set; }
|
||||
|
||||
public void InitializeStreams(MessageBody messageBody)
|
||||
{
|
||||
if (_frameStreams == null)
|
||||
|
|
@ -381,6 +383,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
_requestCount++;
|
||||
|
||||
MinRequestBodyDataRate = ServerOptions.Limits.MinRequestBodyDataRate;
|
||||
MinResponseDataRate = ServerOptions.Limits.MinResponseDataRate;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -418,7 +421,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
|
||||
_frameStreams?.Abort(error);
|
||||
|
||||
Output.Abort();
|
||||
Output.Abort(error);
|
||||
|
||||
// Potentially calling user code. CancelRequestAbortedToken logs any exceptions.
|
||||
ServiceContext.ThreadPool.UnsafeRun(state => ((Frame)state).CancelRequestAbortedToken(), this);
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
private static readonly ArraySegment<byte> _emptyData = new ArraySegment<byte>(new byte[0]);
|
||||
|
||||
private readonly string _connectionId;
|
||||
private readonly ITimeoutControl _timeoutControl;
|
||||
private readonly IKestrelTrace _log;
|
||||
|
||||
// This locks access to to all of the below fields
|
||||
|
|
@ -30,10 +31,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
private readonly object _flushLock = new object();
|
||||
private Action _flushCompleted;
|
||||
|
||||
public OutputProducer(IPipe pipe, string connectionId, IKestrelTrace log)
|
||||
public OutputProducer(
|
||||
IPipe pipe,
|
||||
string connectionId,
|
||||
IKestrelTrace log,
|
||||
ITimeoutControl timeoutControl)
|
||||
{
|
||||
_pipe = pipe;
|
||||
_connectionId = connectionId;
|
||||
_timeoutControl = timeoutControl;
|
||||
_log = log;
|
||||
_flushCompleted = OnFlushCompleted;
|
||||
}
|
||||
|
|
@ -83,7 +89,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
}
|
||||
}
|
||||
|
||||
public void Abort()
|
||||
public void Abort(Exception error)
|
||||
{
|
||||
lock (_contextLock)
|
||||
{
|
||||
|
|
@ -94,8 +100,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
|
||||
_log.ConnectionDisconnect(_connectionId);
|
||||
_completed = true;
|
||||
|
||||
_pipe.Reader.CancelPendingRead();
|
||||
_pipe.Writer.Complete();
|
||||
_pipe.Writer.Complete(error);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -145,10 +152,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
// The flush task can't fail today
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
return FlushAsyncAwaited(awaitable, cancellationToken);
|
||||
return FlushAsyncAwaited(awaitable, writableBuffer.BytesWritten, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task FlushAsyncAwaited(WritableBufferAwaitable awaitable, CancellationToken cancellationToken)
|
||||
private async Task FlushAsyncAwaited(WritableBufferAwaitable awaitable, int count, CancellationToken cancellationToken)
|
||||
{
|
||||
// https://github.com/dotnet/corefxlab/issues/1334
|
||||
// Since the flush awaitable doesn't currently support multiple awaiters
|
||||
|
|
@ -163,7 +170,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
awaitable.OnCompleted(_flushCompleted);
|
||||
}
|
||||
}
|
||||
|
||||
_timeoutControl.StartTimingWrite(count);
|
||||
await _flushTcs.Task;
|
||||
_timeoutControl.StopTimingWrite();
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,5 +43,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
|
|||
void RequestBodyDone(string connectionId, string traceIdentifier);
|
||||
|
||||
void RequestBodyMininumDataRateNotSatisfied(string connectionId, string traceIdentifier, double rate);
|
||||
|
||||
void ResponseMininumDataRateNotSatisfied(string connectionId, string traceIdentifier);
|
||||
}
|
||||
}
|
||||
|
|
@ -16,5 +16,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
|
|||
void ResumeTimingReads();
|
||||
void StopTimingReads();
|
||||
void BytesRead(int count);
|
||||
|
||||
void StartTimingWrite(int size);
|
||||
void StopTimingWrite();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,7 +61,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
|
|||
LoggerMessage.Define<string, string>(LogLevel.Debug, 26, @"Connection id ""{ConnectionId}"", Request id ""{TraceIdentifier}"": done reading request body.");
|
||||
|
||||
private static readonly Action<ILogger, string, string, double, Exception> _requestBodyMinimumDataRateNotSatisfied =
|
||||
LoggerMessage.Define<string, string, double>(LogLevel.Information, 27, @"Connection id ""{ConnectionId}"", Request id ""{TraceIdentifier}"": request body incoming data rate dropped below {Rate} bytes/second.");
|
||||
LoggerMessage.Define<string, string, double>(LogLevel.Information, 27, @"Connection id ""{ConnectionId}"", Request id ""{TraceIdentifier}"": the request timed out because it was not sent by the client at a minimum of {Rate} bytes/second.");
|
||||
|
||||
private static readonly Action<ILogger, string, string, Exception> _responseMinimumDataRateNotSatisfied =
|
||||
LoggerMessage.Define<string, string>(LogLevel.Information, 28, @"Connection id ""{ConnectionId}"", Request id ""{TraceIdentifier}"": the connection was closed becuase the response was not read by the client at the specified minimum data rate.");
|
||||
|
||||
protected readonly ILogger _logger;
|
||||
|
||||
|
|
@ -160,6 +163,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
|
|||
_requestBodyMinimumDataRateNotSatisfied(_logger, connectionId, traceIdentifier, rate, null);
|
||||
}
|
||||
|
||||
public void ResponseMininumDataRateNotSatisfied(string connectionId, string traceIdentifier)
|
||||
{
|
||||
_responseMinimumDataRateNotSatisfied(_logger, connectionId, traceIdentifier, null);
|
||||
}
|
||||
|
||||
public virtual void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
|
||||
=> _logger.Log(logLevel, eventId, state, exception, formatter);
|
||||
|
||||
|
|
|
|||
|
|
@ -263,5 +263,29 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
|
|||
public MinDataRate MinRequestBodyDataRate { get; set; } =
|
||||
// Matches the default IIS minBytesPerSecond
|
||||
new MinDataRate(bytesPerSecond: 240, gracePeriod: TimeSpan.FromSeconds(5));
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the response minimum data rate in bytes/second.
|
||||
/// 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 can be overridden per-request via <see cref="IHttpMinResponseDataRateFeature"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Defaults to 240 bytes/second with a 5 second grace period.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Contrary to the request body minimum data rate, this rate applies to the response status line and headers as well.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This rate is enforced per write operation instead of being averaged over the life of the response. Whenever the server
|
||||
/// writes a chunk of data, a timer is set to the maximum of the grace period set in this property or the length of the write in
|
||||
/// bytes divided by the data rate (i.e. the maximum amount of time that write should take to complete with the specified data rate).
|
||||
/// The connection is aborted if the write has not completed by the time that timer expires.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public MinDataRate MinResponseDataRate { get; set; } =
|
||||
// Matches the default IIS minBytesPerSecond
|
||||
new MinDataRate(bytesPerSecond: 240, gracePeriod: TimeSpan.FromSeconds(5));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,9 +16,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
|
|||
/// starting at the time data is first read or written.</param>
|
||||
public MinDataRate(double bytesPerSecond, TimeSpan gracePeriod)
|
||||
{
|
||||
if (bytesPerSecond < 0)
|
||||
if (bytesPerSecond <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(bytesPerSecond), CoreStrings.NonNegativeNumberRequired);
|
||||
throw new ArgumentOutOfRangeException(nameof(bytesPerSecond), CoreStrings.PositiveNumberOrNullMinDataRateRequired);
|
||||
}
|
||||
|
||||
if (gracePeriod <= Heartbeat.Interval)
|
||||
|
|
|
|||
|
|
@ -1060,6 +1060,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
|
|||
internal static string FormatSynchronousWritesDisallowed()
|
||||
=> GetString("SynchronousWritesDisallowed");
|
||||
|
||||
/// <summary>
|
||||
/// Value must be a positive number. To disable a minimum data rate, use null where a MinDataRate instance is expected.
|
||||
/// </summary>
|
||||
internal static string PositiveNumberOrNullMinDataRateRequired
|
||||
{
|
||||
get => GetString("PositiveNumberOrNullMinDataRateRequired");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Value must be a positive number. To disable a minimum data rate, use null where a MinDataRate instance is expected.
|
||||
/// </summary>
|
||||
internal static string FormatPositiveNumberOrNullMinDataRateRequired()
|
||||
=> GetString("PositiveNumberOrNullMinDataRateRequired");
|
||||
|
||||
private static string GetString(string name, params string[] formatterNames)
|
||||
{
|
||||
var value = _resourceManager.GetString(name);
|
||||
|
|
|
|||
|
|
@ -79,4 +79,4 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal
|
|||
Error = error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,28 +60,26 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal
|
|||
|
||||
StartReading();
|
||||
|
||||
Exception error = null;
|
||||
|
||||
try
|
||||
{
|
||||
// This *must* happen after socket.ReadStart
|
||||
// The socket output consumer is the only thing that can close the connection. If the
|
||||
// output pipe is already closed by the time we start then it's fine since, it'll close gracefully afterwards.
|
||||
await Output.WriteOutputAsync();
|
||||
|
||||
// Now, complete the input so that no more reads can happen
|
||||
Input.Complete(new ConnectionAbortedException());
|
||||
_connectionContext.Output.Complete();
|
||||
_connectionContext.OnConnectionClosed(ex: null);
|
||||
}
|
||||
catch (UvException ex)
|
||||
{
|
||||
var ioEx = new IOException(ex.Message, ex);
|
||||
|
||||
Input.Complete(ioEx);
|
||||
_connectionContext.Output.Complete(ioEx);
|
||||
_connectionContext.OnConnectionClosed(ioEx);
|
||||
error = new IOException(ex.Message, ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Now, complete the input so that no more reads can happen
|
||||
Input.Complete(error ?? new ConnectionAbortedException());
|
||||
_connectionContext.Output.Complete(error);
|
||||
_connectionContext.OnConnectionClosed(error);
|
||||
|
||||
// Make sure it isn't possible for a paused read to resume reading after calling uv_close
|
||||
// on the stream handle
|
||||
Input.CancelPendingFlush();
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Internal.System.IO.Pipelines;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal.Networking;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal
|
||||
|
|
@ -29,6 +28,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal
|
|||
_socket = socket;
|
||||
_connectionId = connectionId;
|
||||
_log = log;
|
||||
|
||||
_pipe.OnWriterCompleted(OnWriterCompleted, this);
|
||||
}
|
||||
|
||||
public async Task WriteOutputAsync()
|
||||
|
|
@ -37,7 +38,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal
|
|||
|
||||
while (true)
|
||||
{
|
||||
var result = await _pipe.ReadAsync();
|
||||
ReadResult result;
|
||||
|
||||
try
|
||||
{
|
||||
result = await _pipe.ReadAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Handled in OnWriterCompleted
|
||||
return;
|
||||
}
|
||||
|
||||
var buffer = result.Buffer;
|
||||
var consumed = buffer.End;
|
||||
|
||||
|
|
@ -54,6 +66,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal
|
|||
|
||||
try
|
||||
{
|
||||
if (_socket.IsClosed)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var writeResult = await writeReq.WriteAsync(_socket, buffer);
|
||||
|
||||
LogWriteInfo(writeResult.Status, writeResult.Error);
|
||||
|
|
@ -82,6 +99,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal
|
|||
}
|
||||
}
|
||||
|
||||
private static void OnWriterCompleted(Exception ex, object state)
|
||||
{
|
||||
// Cut off writes if the writer is completed with an error. If a write request is pending, this will cancel it.
|
||||
if (ex != null)
|
||||
{
|
||||
var libuvOutputConsumer = (LibuvOutputConsumer)state;
|
||||
libuvOutputConsumer._socket.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private void LogWriteInfo(int status, Exception error)
|
||||
{
|
||||
if (error == null)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Adapter.Internal;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
|
||||
|
|
@ -129,7 +130,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
var mockLogger = new Mock<IKestrelTrace>();
|
||||
_frameConnectionContext.ServiceContext.Log = mockLogger.Object;
|
||||
|
||||
_frameConnection.CreateFrame(new DummyApplication(context => Task.CompletedTask), _frameConnectionContext.Input.Reader, _frameConnectionContext.Output);
|
||||
_frameConnection.CreateFrame(new DummyApplication(), _frameConnectionContext.Input.Reader, _frameConnectionContext.Output);
|
||||
_frameConnection.Frame.Reset();
|
||||
|
||||
// Initialize timestamp
|
||||
|
|
@ -171,7 +172,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
var mockLogger = new Mock<IKestrelTrace>();
|
||||
_frameConnectionContext.ServiceContext.Log = mockLogger.Object;
|
||||
|
||||
_frameConnection.CreateFrame(new DummyApplication(context => Task.CompletedTask), _frameConnectionContext.Input.Reader, _frameConnectionContext.Output);
|
||||
_frameConnection.CreateFrame(new DummyApplication(), _frameConnectionContext.Input.Reader, _frameConnectionContext.Output);
|
||||
_frameConnection.Frame.Reset();
|
||||
|
||||
// Initialize timestamp
|
||||
|
|
@ -248,7 +249,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
var mockLogger = new Mock<IKestrelTrace>();
|
||||
_frameConnectionContext.ServiceContext.Log = mockLogger.Object;
|
||||
|
||||
_frameConnection.CreateFrame(new DummyApplication(context => Task.CompletedTask), _frameConnectionContext.Input.Reader, _frameConnectionContext.Output);
|
||||
_frameConnection.CreateFrame(new DummyApplication(), _frameConnectionContext.Input.Reader, _frameConnectionContext.Output);
|
||||
_frameConnection.Frame.Reset();
|
||||
|
||||
// Initialize timestamp
|
||||
|
|
@ -316,7 +317,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
var mockLogger = new Mock<IKestrelTrace>();
|
||||
_frameConnectionContext.ServiceContext.Log = mockLogger.Object;
|
||||
|
||||
_frameConnection.CreateFrame(new DummyApplication(context => Task.CompletedTask), _frameConnectionContext.Input.Reader, _frameConnectionContext.Output);
|
||||
_frameConnection.CreateFrame(new DummyApplication(), _frameConnectionContext.Input.Reader, _frameConnectionContext.Output);
|
||||
_frameConnection.Frame.Reset();
|
||||
|
||||
// Initialize timestamp
|
||||
|
|
@ -364,5 +365,169 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
logger => logger.RequestBodyMininumDataRateNotSatisfied(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<double>()),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadTimingNotEnforcedWhenTimeoutIsSet()
|
||||
{
|
||||
var systemClock = new MockSystemClock();
|
||||
var timeout = TimeSpan.FromSeconds(5);
|
||||
|
||||
_frameConnectionContext.ServiceContext.ServerOptions.Limits.MinRequestBodyDataRate =
|
||||
new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(2));
|
||||
_frameConnectionContext.ServiceContext.SystemClock = systemClock;
|
||||
|
||||
var mockLogger = new Mock<IKestrelTrace>();
|
||||
_frameConnectionContext.ServiceContext.Log = mockLogger.Object;
|
||||
|
||||
_frameConnection.CreateFrame(new DummyApplication(), _frameConnectionContext.Input.Reader, _frameConnectionContext.Output);
|
||||
_frameConnection.Frame.Reset();
|
||||
|
||||
var startTime = systemClock.UtcNow;
|
||||
|
||||
// Initialize timestamp
|
||||
_frameConnection.Tick(startTime);
|
||||
|
||||
_frameConnection.StartTimingReads();
|
||||
|
||||
_frameConnection.SetTimeout(timeout.Ticks, TimeoutAction.CloseConnection);
|
||||
|
||||
// Tick beyond grace period with low data rate
|
||||
systemClock.UtcNow += TimeSpan.FromSeconds(3);
|
||||
_frameConnection.BytesRead(1);
|
||||
_frameConnection.Tick(systemClock.UtcNow);
|
||||
|
||||
// Not timed out
|
||||
Assert.False(_frameConnection.TimedOut);
|
||||
|
||||
// Tick just past timeout period, adjusted by Heartbeat.Interval
|
||||
systemClock.UtcNow = startTime + timeout + Heartbeat.Interval + TimeSpan.FromTicks(1);
|
||||
_frameConnection.Tick(systemClock.UtcNow);
|
||||
|
||||
// Timed out
|
||||
Assert.True(_frameConnection.TimedOut);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteTimingAbortsConnectionWhenWriteDoesNotCompleteWithMinimumDataRate()
|
||||
{
|
||||
var systemClock = new MockSystemClock();
|
||||
var aborted = new ManualResetEventSlim();
|
||||
|
||||
_frameConnectionContext.ServiceContext.ServerOptions.Limits.MinResponseDataRate =
|
||||
new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(2));
|
||||
_frameConnectionContext.ServiceContext.SystemClock = systemClock;
|
||||
|
||||
var mockLogger = new Mock<IKestrelTrace>();
|
||||
_frameConnectionContext.ServiceContext.Log = mockLogger.Object;
|
||||
|
||||
_frameConnection.CreateFrame(new DummyApplication(), _frameConnectionContext.Input.Reader, _frameConnectionContext.Output);
|
||||
_frameConnection.Frame.Reset();
|
||||
_frameConnection.Frame.RequestAborted.Register(() =>
|
||||
{
|
||||
aborted.Set();
|
||||
});
|
||||
|
||||
// Initialize timestamp
|
||||
_frameConnection.Tick(systemClock.UtcNow);
|
||||
|
||||
// Should complete within 4 seconds, but the timeout is adjusted by adding Heartbeat.Interval
|
||||
_frameConnection.StartTimingWrite(400);
|
||||
|
||||
// Tick just past 4s plus Heartbeat.Interval
|
||||
systemClock.UtcNow += TimeSpan.FromSeconds(4) + Heartbeat.Interval + TimeSpan.FromTicks(1);
|
||||
_frameConnection.Tick(systemClock.UtcNow);
|
||||
|
||||
Assert.True(_frameConnection.TimedOut);
|
||||
Assert.True(aborted.Wait(TimeSpan.FromSeconds(10)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteTimingAbortsConnectionWhenSmallWriteDoesNotCompleteWithinGracePeriod()
|
||||
{
|
||||
var systemClock = new MockSystemClock();
|
||||
var minResponseDataRate = new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(5));
|
||||
var aborted = new ManualResetEventSlim();
|
||||
|
||||
_frameConnectionContext.ServiceContext.ServerOptions.Limits.MinResponseDataRate = minResponseDataRate;
|
||||
_frameConnectionContext.ServiceContext.SystemClock = systemClock;
|
||||
|
||||
var mockLogger = new Mock<IKestrelTrace>();
|
||||
_frameConnectionContext.ServiceContext.Log = mockLogger.Object;
|
||||
|
||||
_frameConnection.CreateFrame(new DummyApplication(), _frameConnectionContext.Input.Reader, _frameConnectionContext.Output);
|
||||
_frameConnection.Frame.Reset();
|
||||
_frameConnection.Frame.RequestAborted.Register(() =>
|
||||
{
|
||||
aborted.Set();
|
||||
});
|
||||
|
||||
// Initialize timestamp
|
||||
var startTime = systemClock.UtcNow;
|
||||
_frameConnection.Tick(startTime);
|
||||
|
||||
// Should complete within 1 second, but the timeout is adjusted by adding Heartbeat.Interval
|
||||
_frameConnection.StartTimingWrite(100);
|
||||
|
||||
// Tick just past 1s plus Heartbeat.Interval
|
||||
systemClock.UtcNow += TimeSpan.FromSeconds(1) + Heartbeat.Interval + TimeSpan.FromTicks(1);
|
||||
_frameConnection.Tick(systemClock.UtcNow);
|
||||
|
||||
// Still within grace period, not timed out
|
||||
Assert.False(_frameConnection.TimedOut);
|
||||
|
||||
// Tick just past grace period (adjusted by Heartbeat.Interval)
|
||||
systemClock.UtcNow = startTime + minResponseDataRate.GracePeriod + Heartbeat.Interval + TimeSpan.FromTicks(1);
|
||||
_frameConnection.Tick(systemClock.UtcNow);
|
||||
|
||||
Assert.True(_frameConnection.TimedOut);
|
||||
Assert.True(aborted.Wait(TimeSpan.FromSeconds(10)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteTimingTimeoutPushedOnConcurrentWrite()
|
||||
{
|
||||
var systemClock = new MockSystemClock();
|
||||
var aborted = new ManualResetEventSlim();
|
||||
|
||||
_frameConnectionContext.ServiceContext.ServerOptions.Limits.MinResponseDataRate =
|
||||
new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(2));
|
||||
_frameConnectionContext.ServiceContext.SystemClock = systemClock;
|
||||
|
||||
var mockLogger = new Mock<IKestrelTrace>();
|
||||
_frameConnectionContext.ServiceContext.Log = mockLogger.Object;
|
||||
|
||||
_frameConnection.CreateFrame(new DummyApplication(), _frameConnectionContext.Input.Reader, _frameConnectionContext.Output);
|
||||
_frameConnection.Frame.Reset();
|
||||
_frameConnection.Frame.RequestAborted.Register(() =>
|
||||
{
|
||||
aborted.Set();
|
||||
});
|
||||
|
||||
// Initialize timestamp
|
||||
_frameConnection.Tick(systemClock.UtcNow);
|
||||
|
||||
// Should complete within 5 seconds, but the timeout is adjusted by adding Heartbeat.Interval
|
||||
_frameConnection.StartTimingWrite(500);
|
||||
|
||||
// Start a concurrent write after 3 seconds, which should complete within 3 seconds (adjusted by Heartbeat.Interval)
|
||||
_frameConnection.StartTimingWrite(300);
|
||||
|
||||
// Tick just past 5s plus Heartbeat.Interval, when the first write should have completed
|
||||
systemClock.UtcNow += TimeSpan.FromSeconds(5) + Heartbeat.Interval + TimeSpan.FromTicks(1);
|
||||
_frameConnection.Tick(systemClock.UtcNow);
|
||||
|
||||
// Not timed out because the timeout was pushed by the second write
|
||||
Assert.False(_frameConnection.TimedOut);
|
||||
|
||||
// Complete the first write, this should have no effect on the timeout
|
||||
_frameConnection.StopTimingWrite();
|
||||
|
||||
// Tick just past +3s, when the second write should have completed
|
||||
systemClock.UtcNow += TimeSpan.FromSeconds(3) + TimeSpan.FromTicks(1);
|
||||
_frameConnection.Tick(systemClock.UtcNow);
|
||||
|
||||
Assert.True(_frameConnection.TimedOut);
|
||||
Assert.True(aborted.Wait(TimeSpan.FromSeconds(10)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -147,7 +147,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
|
||||
_frame.Reset();
|
||||
|
||||
Assert.Equal(_serviceContext.ServerOptions.Limits.MinRequestBodyDataRate, _frame.MinRequestBodyDataRate);
|
||||
Assert.Same(_serviceContext.ServerOptions.Limits.MinRequestBodyDataRate, _frame.MinRequestBodyDataRate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResetResetsMinResponseDataRate()
|
||||
{
|
||||
_frame.MinResponseDataRate = new MinDataRate(bytesPerSecond: 1, gracePeriod: TimeSpan.MaxValue);
|
||||
|
||||
_frame.Reset();
|
||||
|
||||
Assert.Same(_serviceContext.ServerOptions.Limits.MinResponseDataRate, _frame.MinResponseDataRate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -254,7 +264,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(MinRequestBodyDataRateData))]
|
||||
[MemberData(nameof(MinDataRateData))]
|
||||
public void ConfiguringIHttpMinRequestBodyDataRateFeatureSetsMinRequestBodyDataRate(MinDataRate minDataRate)
|
||||
{
|
||||
((IFeatureCollection)_frame).Get<IHttpMinRequestBodyDataRateFeature>().MinDataRate = minDataRate;
|
||||
|
|
@ -262,6 +272,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
Assert.Same(minDataRate, _frame.MinRequestBodyDataRate);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(MinDataRateData))]
|
||||
public void ConfiguringIHttpMinResponseDataRateFeatureSetsMinResponseDataRate(MinDataRate minDataRate)
|
||||
{
|
||||
((IFeatureCollection)_frame).Get<IHttpMinResponseDataRateFeature>().MinDataRate = minDataRate;
|
||||
|
||||
Assert.Same(minDataRate, _frame.MinResponseDataRate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResetResetsRequestHeaders()
|
||||
{
|
||||
|
|
@ -878,7 +897,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
TimeSpan.Zero
|
||||
};
|
||||
|
||||
public static TheoryData<MinDataRate> MinRequestBodyDataRateData => new TheoryData<MinDataRate>
|
||||
public static TheoryData<MinDataRate> MinDataRateData => new TheoryData<MinDataRate>
|
||||
{
|
||||
null,
|
||||
new MinDataRate(bytesPerSecond: 1, gracePeriod: TimeSpan.MaxValue)
|
||||
|
|
|
|||
|
|
@ -300,6 +300,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
Assert.Equal(TimeSpan.FromSeconds(5), new KestrelServerLimits().MinRequestBodyDataRate.GracePeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinResponseBodyDataRateDefault()
|
||||
{
|
||||
Assert.NotNull(new KestrelServerLimits().MinResponseDataRate);
|
||||
Assert.Equal(240, new KestrelServerLimits().MinResponseDataRate.BytesPerSecond);
|
||||
Assert.Equal(TimeSpan.FromSeconds(5), new KestrelServerLimits().MinResponseDataRate.GracePeriod);
|
||||
}
|
||||
|
||||
public static TheoryData<TimeSpan> TimeoutValidData => new TheoryData<TimeSpan>
|
||||
{
|
||||
TimeSpan.FromTicks(1),
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
public class MinDataRateTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(double.Epsilon)]
|
||||
[InlineData(double.MaxValue)]
|
||||
public void BytesPerSecondValid(double value)
|
||||
|
|
@ -21,12 +20,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
[Theory]
|
||||
[InlineData(double.MinValue)]
|
||||
[InlineData(-double.Epsilon)]
|
||||
[InlineData(0)]
|
||||
public void BytesPerSecondInvalid(double value)
|
||||
{
|
||||
var exception = Assert.Throws<ArgumentOutOfRangeException>(() => new MinDataRate(bytesPerSecond: value, gracePeriod: TimeSpan.MaxValue));
|
||||
|
||||
Assert.Equal("bytesPerSecond", exception.ParamName);
|
||||
Assert.StartsWith(CoreStrings.NonNegativeNumberRequired, exception.Message);
|
||||
Assert.StartsWith(CoreStrings.PositiveNumberOrNullMinDataRateRequired, exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
|
|
|||
|
|
@ -54,16 +54,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
{
|
||||
var pipe = _pipeFactory.Create(pipeOptions);
|
||||
var serviceContext = new TestServiceContext();
|
||||
var frameContext = new FrameContext
|
||||
{
|
||||
ServiceContext = serviceContext,
|
||||
ConnectionInformation = new MockConnectionInformation
|
||||
{
|
||||
PipeFactory = _pipeFactory
|
||||
}
|
||||
};
|
||||
var frame = new Frame<object>(null, frameContext);
|
||||
var socketOutput = new OutputProducer(pipe, "0", serviceContext.Log);
|
||||
var socketOutput = new OutputProducer(
|
||||
pipe,
|
||||
"0",
|
||||
serviceContext.Log,
|
||||
Mock.Of<ITimeoutControl>());
|
||||
|
||||
return socketOutput;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1519,10 +1519,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
|
|||
|
||||
// Synchronous reads now throw.
|
||||
var ioEx = Assert.Throws<InvalidOperationException>(() => context.Request.Body.Read(new byte[1], 0, 1));
|
||||
Assert.Equal("Synchronous operations are disallowed. Call ReadAsync or set AllowSynchronousIO to true instead.", ioEx.Message);
|
||||
Assert.Equal(CoreStrings.SynchronousReadsDisallowed, ioEx.Message);
|
||||
|
||||
var ioEx2 = Assert.Throws<InvalidOperationException>(() => context.Request.Body.CopyTo(Stream.Null));
|
||||
Assert.Equal("Synchronous operations are disallowed. Call ReadAsync or set AllowSynchronousIO to true instead.", ioEx2.Message);
|
||||
Assert.Equal(CoreStrings.SynchronousReadsDisallowed, ioEx2.Message);
|
||||
|
||||
while (offset < 5)
|
||||
{
|
||||
|
|
@ -1578,10 +1578,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
|
|||
|
||||
// Synchronous reads now throw.
|
||||
var ioEx = Assert.Throws<InvalidOperationException>(() => context.Request.Body.Read(new byte[1], 0, 1));
|
||||
Assert.Equal("Synchronous operations are disallowed. Call ReadAsync or set AllowSynchronousIO to true instead.", ioEx.Message);
|
||||
Assert.Equal(CoreStrings.SynchronousReadsDisallowed, ioEx.Message);
|
||||
|
||||
var ioEx2 = Assert.Throws<InvalidOperationException>(() => context.Request.Body.CopyTo(Stream.Null));
|
||||
Assert.Equal("Synchronous operations are disallowed. Call ReadAsync or set AllowSynchronousIO to true instead.", ioEx2.Message);
|
||||
Assert.Equal(CoreStrings.SynchronousReadsDisallowed, ioEx2.Message);
|
||||
|
||||
var buffer = new byte[5];
|
||||
var offset = 0;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,10 @@ using System.IO;
|
|||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Security;
|
||||
using System.Net.Sockets;
|
||||
using System.Security.Authentication;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
|
@ -19,12 +22,15 @@ using Microsoft.AspNetCore.Server.Kestrel.Core;
|
|||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Https;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Https.Internal;
|
||||
using Microsoft.AspNetCore.Testing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
using Xunit.Sdk;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
|
||||
{
|
||||
|
|
@ -2371,7 +2377,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
|
|||
|
||||
// Synchronous writes now throw.
|
||||
var ioEx = Assert.Throws<InvalidOperationException>(() => context.Response.Body.Write(Encoding.ASCII.GetBytes("What!?"), 0, 6));
|
||||
Assert.Equal("Synchronous operations are disallowed. Call WriteAsync or set AllowSynchronousIO to true instead.", ioEx.Message);
|
||||
Assert.Equal(CoreStrings.SynchronousWritesDisallowed, ioEx.Message);
|
||||
|
||||
await context.Response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello2"), 0, 6);
|
||||
}
|
||||
|
|
@ -2415,7 +2421,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
|
|||
|
||||
// Synchronous writes now throw.
|
||||
var ioEx = Assert.Throws<InvalidOperationException>(() => context.Response.Body.Write(Encoding.ASCII.GetBytes("What!?"), 0, 6));
|
||||
Assert.Equal("Synchronous operations are disallowed. Call WriteAsync or set AllowSynchronousIO to true instead.", ioEx.Message);
|
||||
Assert.Equal(CoreStrings.SynchronousWritesDisallowed, ioEx.Message);
|
||||
|
||||
return context.Response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello!"), 0, 6);
|
||||
}, testContext))
|
||||
|
|
@ -2437,6 +2443,152 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
|
|||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConnectionClosedWhenResponseDoesNotSatisfyMinimumDataRate()
|
||||
{
|
||||
var chunkSize = 64 * 1024;
|
||||
var chunks = 128;
|
||||
|
||||
var messageLogged = new ManualResetEventSlim();
|
||||
|
||||
var mockKestrelTrace = new Mock<IKestrelTrace>();
|
||||
mockKestrelTrace
|
||||
.Setup(trace => trace.ResponseMininumDataRateNotSatisfied(It.IsAny<string>(), It.IsAny<string>()))
|
||||
.Callback(() => messageLogged.Set());
|
||||
|
||||
var testContext = new TestServiceContext
|
||||
{
|
||||
Log = mockKestrelTrace.Object,
|
||||
SystemClock = new SystemClock(),
|
||||
ServerOptions =
|
||||
{
|
||||
Limits =
|
||||
{
|
||||
MinResponseDataRate = new MinDataRate(bytesPerSecond: double.MaxValue, gracePeriod: TimeSpan.FromSeconds(2))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var aborted = new ManualResetEventSlim();
|
||||
|
||||
using (var server = new TestServer(async context =>
|
||||
{
|
||||
context.RequestAborted.Register(() =>
|
||||
{
|
||||
aborted.Set();
|
||||
});
|
||||
|
||||
context.Response.ContentLength = chunks * chunkSize;
|
||||
|
||||
for (var i = 0; i < chunks; i++)
|
||||
{
|
||||
await context.Response.WriteAsync(new string('a', chunkSize), context.RequestAborted);
|
||||
}
|
||||
}, testContext))
|
||||
{
|
||||
using (var connection = server.CreateConnection())
|
||||
{
|
||||
await connection.Send(
|
||||
"GET / HTTP/1.1",
|
||||
"Host:",
|
||||
"",
|
||||
"");
|
||||
|
||||
Assert.True(aborted.Wait(TimeSpan.FromSeconds(60)));
|
||||
|
||||
await connection.Receive(
|
||||
"HTTP/1.1 200 OK",
|
||||
"");
|
||||
await connection.ReceiveStartsWith("Date: ");
|
||||
await connection.Receive(
|
||||
$"Content-Length: {chunks * chunkSize}",
|
||||
"",
|
||||
"");
|
||||
|
||||
await Assert.ThrowsAsync<EqualException>(async () => await connection.ReceiveForcedEnd(
|
||||
new string('a', chunks * chunkSize)));
|
||||
|
||||
Assert.True(messageLogged.Wait(TimeSpan.FromSeconds(10)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HttpsConnectionClosedWhenResponseDoesNotSatisfyMinimumDataRate()
|
||||
{
|
||||
const int chunkSize = 64 * 1024;
|
||||
const int chunks = 128;
|
||||
|
||||
var certificate = new X509Certificate2(TestResources.TestCertificatePath, "testPassword");
|
||||
|
||||
var messageLogged = new ManualResetEventSlim();
|
||||
var aborted = new ManualResetEventSlim();
|
||||
|
||||
var mockKestrelTrace = new Mock<IKestrelTrace>();
|
||||
mockKestrelTrace
|
||||
.Setup(trace => trace.ResponseMininumDataRateNotSatisfied(It.IsAny<string>(), It.IsAny<string>()))
|
||||
.Callback(() => messageLogged.Set());
|
||||
|
||||
var testContext = new TestServiceContext
|
||||
{
|
||||
Log = mockKestrelTrace.Object,
|
||||
SystemClock = new SystemClock(),
|
||||
ServerOptions =
|
||||
{
|
||||
Limits =
|
||||
{
|
||||
MinResponseDataRate = new MinDataRate(bytesPerSecond: double.MaxValue, gracePeriod: TimeSpan.FromSeconds(2))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0))
|
||||
{
|
||||
ConnectionAdapters =
|
||||
{
|
||||
new HttpsConnectionAdapter(new HttpsConnectionAdapterOptions { ServerCertificate = certificate })
|
||||
}
|
||||
};
|
||||
|
||||
using (var server = new TestServer(async context =>
|
||||
{
|
||||
context.RequestAborted.Register(() =>
|
||||
{
|
||||
aborted.Set();
|
||||
});
|
||||
|
||||
context.Response.ContentLength = chunks * chunkSize;
|
||||
|
||||
for (var i = 0; i < chunks; i++)
|
||||
{
|
||||
await context.Response.WriteAsync(new string('a', chunkSize), context.RequestAborted);
|
||||
}
|
||||
}, testContext, listenOptions))
|
||||
{
|
||||
using (var client = new TcpClient())
|
||||
{
|
||||
await client.ConnectAsync(IPAddress.Loopback, server.Port);
|
||||
|
||||
using (var sslStream = new SslStream(client.GetStream(), false, (sender, cert, chain, errors) => true, null))
|
||||
{
|
||||
await sslStream.AuthenticateAsClientAsync("localhost", new X509CertificateCollection(), SslProtocols.Tls12 | SslProtocols.Tls11, false);
|
||||
|
||||
var request = Encoding.ASCII.GetBytes("GET / HTTP/1.1\r\nHost:\r\n\r\n");
|
||||
await sslStream.WriteAsync(request, 0, request.Length);
|
||||
|
||||
Assert.True(aborted.Wait(TimeSpan.FromSeconds(60)));
|
||||
|
||||
using (var reader = new StreamReader(sslStream, encoding: Encoding.ASCII, detectEncodingFromByteOrderMarks: false, bufferSize: 1024, leaveOpen: false))
|
||||
{
|
||||
await reader.ReadToEndAsync().TimeoutAfter(TimeSpan.FromSeconds(30));
|
||||
}
|
||||
|
||||
Assert.True(messageLogged.Wait(TimeSpan.FromSeconds(10)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static TheoryData<string, StringValues, string> NullHeaderData
|
||||
{
|
||||
get
|
||||
|
|
|
|||
|
|
@ -40,5 +40,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance.Mocks
|
|||
public void BytesRead(int count)
|
||||
{
|
||||
}
|
||||
|
||||
public void StartTimingWrite(int size)
|
||||
{
|
||||
}
|
||||
|
||||
public void StopTimingWrite()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,5 +39,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
|
|||
public void RequestBodyStart(string connectionId, string traceIdentifier) { }
|
||||
public void RequestBodyDone(string connectionId, string traceIdentifier) { }
|
||||
public void RequestBodyMininumDataRateNotSatisfied(string connectionId, string traceIdentifier, double rate) { }
|
||||
public void ResponseMininumDataRateNotSatisfied(string connectionId, string traceIdentifier) { }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,14 +67,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests
|
|||
MaximumSizeLow = maxResponseBufferSize ?? 0,
|
||||
};
|
||||
|
||||
using (var socketOutput = CreateOutputProducer(pipeOptions))
|
||||
using (var outputProducer = CreateOutputProducer(pipeOptions))
|
||||
{
|
||||
// At least one run of this test should have a MaxResponseBufferSize < 1 MB.
|
||||
var bufferSize = 1024 * 1024;
|
||||
var buffer = new ArraySegment<byte>(new byte[bufferSize], 0, bufferSize);
|
||||
|
||||
// Act
|
||||
var writeTask = socketOutput.WriteAsync(buffer);
|
||||
var writeTask = outputProducer.WriteAsync(buffer);
|
||||
|
||||
// Assert
|
||||
await writeTask.TimeoutAfter(TimeSpan.FromSeconds(5));
|
||||
|
|
@ -102,20 +102,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests
|
|||
MaximumSizeLow = 0,
|
||||
};
|
||||
|
||||
using (var socketOutput = CreateOutputProducer(pipeOptions))
|
||||
using (var outputProducer = CreateOutputProducer(pipeOptions))
|
||||
{
|
||||
// Don't want to allocate anything too huge for perf. This is at least larger than the default buffer.
|
||||
var bufferSize = 1024 * 1024;
|
||||
var buffer = new ArraySegment<byte>(new byte[bufferSize], 0, bufferSize);
|
||||
|
||||
// Act
|
||||
var writeTask = socketOutput.WriteAsync(buffer);
|
||||
var writeTask = outputProducer.WriteAsync(buffer);
|
||||
|
||||
// Assert
|
||||
await writeTask.TimeoutAfter(TimeSpan.FromSeconds(5));
|
||||
|
||||
// Cleanup
|
||||
socketOutput.Dispose();
|
||||
outputProducer.Dispose();
|
||||
|
||||
// Wait for all writes to complete so the completeQueue isn't modified during enumeration.
|
||||
await _mockLibuv.OnPostTask;
|
||||
|
|
@ -149,13 +149,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests
|
|||
MaximumSizeLow = 1,
|
||||
};
|
||||
|
||||
using (var socketOutput = CreateOutputProducer(pipeOptions))
|
||||
using (var outputProducer = CreateOutputProducer(pipeOptions))
|
||||
{
|
||||
var bufferSize = 1;
|
||||
var buffer = new ArraySegment<byte>(new byte[bufferSize], 0, bufferSize);
|
||||
|
||||
// Act
|
||||
var writeTask = socketOutput.WriteAsync(buffer);
|
||||
var writeTask = outputProducer.WriteAsync(buffer);
|
||||
|
||||
// Assert
|
||||
Assert.False(writeTask.IsCompleted);
|
||||
|
|
@ -171,7 +171,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests
|
|||
await writeTask.TimeoutAfter(TimeSpan.FromSeconds(5));
|
||||
|
||||
// Cleanup
|
||||
socketOutput.Dispose();
|
||||
outputProducer.Dispose();
|
||||
|
||||
// Wait for all writes to complete so the completeQueue isn't modified during enumeration.
|
||||
await _mockLibuv.OnPostTask;
|
||||
|
|
@ -204,20 +204,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests
|
|||
MaximumSizeLow = maxResponseBufferSize,
|
||||
};
|
||||
|
||||
using (var socketOutput = CreateOutputProducer(pipeOptions))
|
||||
using (var outputProducer = CreateOutputProducer(pipeOptions))
|
||||
{
|
||||
var bufferSize = maxResponseBufferSize - 1;
|
||||
var buffer = new ArraySegment<byte>(new byte[bufferSize], 0, bufferSize);
|
||||
|
||||
// Act
|
||||
var writeTask1 = socketOutput.WriteAsync(buffer);
|
||||
var writeTask1 = outputProducer.WriteAsync(buffer);
|
||||
|
||||
// Assert
|
||||
// The first write should pre-complete since it is <= _maxBytesPreCompleted.
|
||||
Assert.Equal(TaskStatus.RanToCompletion, writeTask1.Status);
|
||||
|
||||
// Act
|
||||
var writeTask2 = socketOutput.WriteAsync(buffer);
|
||||
var writeTask2 = outputProducer.WriteAsync(buffer);
|
||||
await _mockLibuv.OnPostTask;
|
||||
|
||||
// Assert
|
||||
|
|
@ -232,7 +232,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests
|
|||
await writeTask2.TimeoutAfter(TimeSpan.FromSeconds(5));
|
||||
|
||||
// Cleanup
|
||||
socketOutput.Dispose();
|
||||
outputProducer.Dispose();
|
||||
|
||||
// Wait for all writes to complete so the completeQueue isn't modified during enumeration.
|
||||
await _mockLibuv.OnPostTask;
|
||||
|
|
@ -267,14 +267,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests
|
|||
MaximumSizeLow = maxResponseBufferSize,
|
||||
};
|
||||
|
||||
using (var socketOutput = CreateOutputProducer(pipeOptions))
|
||||
using (var outputProducer = CreateOutputProducer(pipeOptions))
|
||||
{
|
||||
var bufferSize = maxResponseBufferSize / 2;
|
||||
var data = new byte[bufferSize];
|
||||
var halfWriteBehindBuffer = new ArraySegment<byte>(data, 0, bufferSize);
|
||||
|
||||
// Act
|
||||
var writeTask1 = socketOutput.WriteAsync(halfWriteBehindBuffer);
|
||||
var writeTask1 = outputProducer.WriteAsync(halfWriteBehindBuffer);
|
||||
|
||||
// Assert
|
||||
// The first write should pre-complete since it is <= _maxBytesPreCompleted.
|
||||
|
|
@ -283,17 +283,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests
|
|||
Assert.NotEmpty(completeQueue);
|
||||
|
||||
// Add more bytes to the write-behind buffer to prevent the next write from
|
||||
socketOutput.Write((writableBuffer, state) =>
|
||||
outputProducer.Write((writableBuffer, state) =>
|
||||
{
|
||||
writableBuffer.Write(state);
|
||||
},
|
||||
halfWriteBehindBuffer);
|
||||
|
||||
// Act
|
||||
var writeTask2 = socketOutput.WriteAsync(halfWriteBehindBuffer);
|
||||
var writeTask2 = outputProducer.WriteAsync(halfWriteBehindBuffer);
|
||||
Assert.False(writeTask2.IsCompleted);
|
||||
|
||||
var writeTask3 = socketOutput.WriteAsync(halfWriteBehindBuffer);
|
||||
var writeTask3 = outputProducer.WriteAsync(halfWriteBehindBuffer);
|
||||
Assert.False(writeTask3.IsCompleted);
|
||||
|
||||
// Drain the write queue
|
||||
|
|
@ -336,7 +336,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests
|
|||
MaximumSizeLow = maxResponseBufferSize,
|
||||
};
|
||||
|
||||
using (var socketOutput = CreateOutputProducer(pipeOptions, abortedSource))
|
||||
using (var outputProducer = CreateOutputProducer(pipeOptions, abortedSource))
|
||||
{
|
||||
var bufferSize = maxResponseBufferSize - 1;
|
||||
|
||||
|
|
@ -344,7 +344,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests
|
|||
var fullBuffer = new ArraySegment<byte>(data, 0, bufferSize);
|
||||
|
||||
// Act
|
||||
var task1Success = socketOutput.WriteAsync(fullBuffer, cancellationToken: abortedSource.Token);
|
||||
var task1Success = outputProducer.WriteAsync(fullBuffer, cancellationToken: abortedSource.Token);
|
||||
// task1 should complete successfully as < _maxBytesPreCompleted
|
||||
|
||||
// First task is completed and successful
|
||||
|
|
@ -353,8 +353,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests
|
|||
Assert.False(task1Success.IsFaulted);
|
||||
|
||||
// following tasks should wait.
|
||||
var task2Success = socketOutput.WriteAsync(fullBuffer);
|
||||
var task3Canceled = socketOutput.WriteAsync(fullBuffer, cancellationToken: abortedSource.Token);
|
||||
var task2Success = outputProducer.WriteAsync(fullBuffer);
|
||||
var task3Canceled = outputProducer.WriteAsync(fullBuffer, cancellationToken: abortedSource.Token);
|
||||
|
||||
// Give time for tasks to percolate
|
||||
await _mockLibuv.OnPostTask;
|
||||
|
|
@ -382,7 +382,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests
|
|||
|
||||
// A final write guarantees that the error is observed by OutputProducer,
|
||||
// but doesn't return a canceled/faulted task.
|
||||
var task4Success = socketOutput.WriteAsync(fullBuffer, cancellationToken: default(CancellationToken));
|
||||
var task4Success = outputProducer.WriteAsync(fullBuffer, cancellationToken: default(CancellationToken));
|
||||
Assert.True(task4Success.IsCompleted);
|
||||
Assert.False(task4Success.IsCanceled);
|
||||
Assert.False(task4Success.IsFaulted);
|
||||
|
|
@ -428,7 +428,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests
|
|||
MaximumSizeLow = maxResponseBufferSize,
|
||||
};
|
||||
|
||||
using (var socketOutput = CreateOutputProducer(pipeOptions))
|
||||
using (var outputProducer = CreateOutputProducer(pipeOptions))
|
||||
{
|
||||
var bufferSize = maxResponseBufferSize - 1;
|
||||
|
||||
|
|
@ -436,7 +436,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests
|
|||
var fullBuffer = new ArraySegment<byte>(data, 0, bufferSize);
|
||||
|
||||
// Act
|
||||
var task1Success = socketOutput.WriteAsync(fullBuffer, cancellationToken: abortedSource.Token);
|
||||
var task1Success = outputProducer.WriteAsync(fullBuffer, cancellationToken: abortedSource.Token);
|
||||
// task1 should complete successfully as < _maxBytesPreCompleted
|
||||
|
||||
// First task is completed and successful
|
||||
|
|
@ -445,7 +445,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests
|
|||
Assert.False(task1Success.IsFaulted);
|
||||
|
||||
// following tasks should wait.
|
||||
var task3Canceled = socketOutput.WriteAsync(fullBuffer, cancellationToken: abortedSource.Token);
|
||||
var task3Canceled = outputProducer.WriteAsync(fullBuffer, cancellationToken: abortedSource.Token);
|
||||
|
||||
// Give time for tasks to percolate
|
||||
await _mockLibuv.OnPostTask;
|
||||
|
|
@ -465,7 +465,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests
|
|||
|
||||
// A final write guarantees that the error is observed by OutputProducer,
|
||||
// but doesn't return a canceled/faulted task.
|
||||
var task4Success = socketOutput.WriteAsync(fullBuffer);
|
||||
var task4Success = outputProducer.WriteAsync(fullBuffer);
|
||||
Assert.True(task4Success.IsCompleted);
|
||||
Assert.False(task4Success.IsCanceled);
|
||||
Assert.False(task4Success.IsFaulted);
|
||||
|
|
@ -511,7 +511,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests
|
|||
MaximumSizeLow = maxResponseBufferSize,
|
||||
};
|
||||
|
||||
using (var socketOutput = CreateOutputProducer(pipeOptions))
|
||||
using (var outputProducer = CreateOutputProducer(pipeOptions))
|
||||
{
|
||||
var bufferSize = maxResponseBufferSize;
|
||||
|
||||
|
|
@ -519,7 +519,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests
|
|||
var fullBuffer = new ArraySegment<byte>(data, 0, bufferSize);
|
||||
|
||||
// Act
|
||||
var task1Waits = socketOutput.WriteAsync(fullBuffer);
|
||||
var task1Waits = outputProducer.WriteAsync(fullBuffer);
|
||||
|
||||
// First task is not completed
|
||||
Assert.False(task1Waits.IsCompleted);
|
||||
|
|
@ -527,7 +527,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests
|
|||
Assert.False(task1Waits.IsFaulted);
|
||||
|
||||
// following tasks should wait.
|
||||
var task3Canceled = socketOutput.WriteAsync(fullBuffer, cancellationToken: abortedSource.Token);
|
||||
var task3Canceled = outputProducer.WriteAsync(fullBuffer, cancellationToken: abortedSource.Token);
|
||||
|
||||
// Give time for tasks to percolate
|
||||
await _mockLibuv.OnPostTask;
|
||||
|
|
@ -552,7 +552,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests
|
|||
|
||||
// A final write guarantees that the error is observed by OutputProducer,
|
||||
// but doesn't return a canceled/faulted task.
|
||||
var task4Success = socketOutput.WriteAsync(fullBuffer);
|
||||
var task4Success = outputProducer.WriteAsync(fullBuffer);
|
||||
Assert.True(task4Success.IsCompleted);
|
||||
Assert.False(task4Success.IsCanceled);
|
||||
Assert.False(task4Success.IsFaulted);
|
||||
|
|
@ -592,13 +592,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests
|
|||
MaximumSizeLow = maxResponseBufferSize,
|
||||
};
|
||||
|
||||
using (var socketOutput = CreateOutputProducer(pipeOptions))
|
||||
using (var outputProducer = CreateOutputProducer(pipeOptions))
|
||||
{
|
||||
var bufferSize = maxResponseBufferSize - 1;
|
||||
var buffer = new ArraySegment<byte>(new byte[bufferSize], 0, bufferSize);
|
||||
|
||||
// Act (Pre-complete the maximum number of bytes in preparation for the rest of the test)
|
||||
var writeTask1 = socketOutput.WriteAsync(buffer);
|
||||
var writeTask1 = outputProducer.WriteAsync(buffer);
|
||||
|
||||
// Assert
|
||||
// The first write should pre-complete since it is < _maxBytesPreCompleted.
|
||||
|
|
@ -607,8 +607,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests
|
|||
Assert.NotEmpty(completeQueue);
|
||||
|
||||
// Act
|
||||
var writeTask2 = socketOutput.WriteAsync(buffer);
|
||||
var writeTask3 = socketOutput.WriteAsync(buffer);
|
||||
var writeTask2 = outputProducer.WriteAsync(buffer);
|
||||
var writeTask3 = outputProducer.WriteAsync(buffer);
|
||||
|
||||
await _mockLibuv.OnPostTask;
|
||||
|
||||
|
|
@ -652,7 +652,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests
|
|||
MaximumSizeLow = maxResponseBufferSize ?? 0,
|
||||
};
|
||||
|
||||
using (var socketOutput = CreateOutputProducer(pipeOptions))
|
||||
using (var outputProducer = CreateOutputProducer(pipeOptions))
|
||||
{
|
||||
_mockLibuv.KestrelThreadBlocker.Reset();
|
||||
|
||||
|
|
@ -660,8 +660,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests
|
|||
|
||||
// Two calls to WriteAsync trigger uv_write once if both calls
|
||||
// are made before write is scheduled
|
||||
var ignore = socketOutput.WriteAsync(buffer);
|
||||
ignore = socketOutput.WriteAsync(buffer);
|
||||
var ignore = outputProducer.WriteAsync(buffer);
|
||||
ignore = outputProducer.WriteAsync(buffer);
|
||||
|
||||
_mockLibuv.KestrelThreadBlocker.Set();
|
||||
|
||||
|
|
@ -736,4 +736,4 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -38,18 +38,29 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests.TestHelpers
|
|||
|
||||
_uv_async_send = postHandle =>
|
||||
{
|
||||
lock (_postLock)
|
||||
// Attempt to run the async send logic inline; this should succeed most of the time.
|
||||
// In the rare cases where it fails to acquire the lock, use Task.Run() so this call
|
||||
// never blocks, since the real libuv never blocks.
|
||||
if (Monitor.TryEnter(_postLock))
|
||||
{
|
||||
if (_completedOnPostTcs)
|
||||
try
|
||||
{
|
||||
_onPostTcs = new TaskCompletionSource<object>();
|
||||
_completedOnPostTcs = false;
|
||||
UvAsyncSend();
|
||||
}
|
||||
|
||||
PostCount++;
|
||||
|
||||
_sendCalled = true;
|
||||
_loopWh.Set();
|
||||
finally
|
||||
{
|
||||
Monitor.Exit(_postLock);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Task.Run(() =>
|
||||
{
|
||||
lock (_postLock)
|
||||
{
|
||||
UvAsyncSend();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
|
@ -160,5 +171,19 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests.TestHelpers
|
|||
{
|
||||
return OnWrite(handle, nbufs, status => cb(req.InternalGetHandle(), status));
|
||||
}
|
||||
|
||||
private void UvAsyncSend()
|
||||
{
|
||||
if (_completedOnPostTcs)
|
||||
{
|
||||
_onPostTcs = new TaskCompletionSource<object>();
|
||||
_completedOnPostTcs = false;
|
||||
}
|
||||
|
||||
PostCount++;
|
||||
|
||||
_sendCalled = true;
|
||||
_loopWh.Set();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// 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;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ namespace CodeGenerator
|
|||
typeof(ISessionFeature),
|
||||
typeof(IHttpMaxRequestBodySizeFeature),
|
||||
typeof(IHttpMinRequestBodyDataRateFeature),
|
||||
typeof(IHttpMinResponseDataRateFeature),
|
||||
typeof(IHttpBodyControlFeature),
|
||||
};
|
||||
|
||||
|
|
@ -70,6 +71,7 @@ namespace CodeGenerator
|
|||
typeof(IHttpConnectionFeature),
|
||||
typeof(IHttpMaxRequestBodySizeFeature),
|
||||
typeof(IHttpMinRequestBodyDataRateFeature),
|
||||
typeof(IHttpMinResponseDataRateFeature),
|
||||
typeof(IHttpBodyControlFeature),
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue