Add response minimum data rate feature.

This commit is contained in:
Cesar Blum Silveira 2017-07-07 22:37:25 -07:00 committed by GitHub
parent 5185ebe45f
commit eca4bfe6c3
30 changed files with 708 additions and 126 deletions

View File

@ -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>

View File

@ -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>

View File

@ -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; }
}
}

View File

@ -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--;
}
}
}
}

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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();
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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);

View File

@ -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));
}
}

View File

@ -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)

View File

@ -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);

View File

@ -79,4 +79,4 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal
Error = error;
}
}
}
}

View File

@ -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();

View File

@ -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)

View File

@ -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)));
}
}
}

View File

@ -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)

View File

@ -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),

View File

@ -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]

View File

@ -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;
}

View File

@ -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;

View File

@ -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

View File

@ -40,5 +40,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance.Mocks
public void BytesRead(int count)
{
}
public void StartTimingWrite(int size)
{
}
public void StopTimingWrite()
{
}
}
}

View File

@ -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) { }
}
}

View File

@ -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
}
}
}
}
}

View File

@ -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();
}
}
}

View File

@ -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;

View File

@ -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),
};