From eca4bfe6c3254bef221be1f23b68a0112ce86618 Mon Sep 17 00:00:00 2001 From: Cesar Blum Silveira Date: Fri, 7 Jul 2017 22:37:25 -0700 Subject: [PATCH] Add response minimum data rate feature. --- .../CoreStrings.resx | 3 + .../IHttpMinRequestBodyDataRateFeature.cs | 4 +- .../IHttpMinResponseDataRateFeature.cs | 18 ++ .../Internal/FrameConnection.cs | 129 ++++++++++--- .../Internal/Http/Frame.FeatureCollection.cs | 9 +- .../Internal/Http/Frame.Generated.cs | 16 ++ .../Internal/Http/Frame.cs | 7 +- .../Internal/Http/OutputProducer.cs | 20 +- .../Internal/Infrastructure/IKestrelTrace.cs | 2 + .../Infrastructure/ITimeoutControl.cs | 3 + .../Internal/Infrastructure/KestrelTrace.cs | 10 +- .../KestrelServerLimits.cs | 24 +++ .../MinDataRate.cs | 4 +- .../Properties/CoreStrings.Designer.cs | 14 ++ .../Internal/LibuvAwaitable.cs | 2 +- .../Internal/LibuvConnection.cs | 18 +- .../Internal/LibuvOutputConsumer.cs | 31 +++- .../FrameConnectionTests.cs | 173 +++++++++++++++++- .../FrameTests.cs | 25 ++- .../KestrelServerLimitsTests.cs | 8 + .../MinDataRateTests.cs | 4 +- .../OutputProducerTests.cs | 15 +- .../RequestTests.cs | 8 +- .../ResponseTests.cs | 156 +++++++++++++++- .../Mocks/MockTimeoutControl.cs | 8 + .../Mocks/MockTrace.cs | 1 + .../LibuvOutputConsumerTests.cs | 76 ++++---- .../TestHelpers/MockLibuv.cs | 43 ++++- test/shared/TestServiceContext.cs | 1 + tools/CodeGenerator/FrameFeatureCollection.cs | 2 + 30 files changed, 708 insertions(+), 126 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Server.Kestrel.Core/Features/IHttpMinResponseDataRateFeature.cs diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/CoreStrings.resx b/src/Microsoft.AspNetCore.Server.Kestrel.Core/CoreStrings.resx index 16e59e4c3a..63f8986ab5 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/CoreStrings.resx +++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/CoreStrings.resx @@ -342,4 +342,7 @@ Synchronous operations are disallowed. Call WriteAsync or set AllowSynchronousIO to true instead. + + Value must be a positive number. To disable a minimum data rate, use null where a MinDataRate instance is expected. + \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Features/IHttpMinRequestBodyDataRateFeature.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Features/IHttpMinRequestBodyDataRateFeature.cs index af5ee4e66f..f80bd99772 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Features/IHttpMinRequestBodyDataRateFeature.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Features/IHttpMinRequestBodyDataRateFeature.cs @@ -4,12 +4,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Features { /// - /// 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. /// public interface IHttpMinRequestBodyDataRateFeature { /// - /// 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. /// diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Features/IHttpMinResponseDataRateFeature.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Features/IHttpMinResponseDataRateFeature.cs new file mode 100644 index 0000000000..f901a338d9 --- /dev/null +++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Features/IHttpMinResponseDataRateFeature.cs @@ -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 +{ + /// + /// Feature to set the minimum data rate at which the response must be received by the client. + /// + public interface IHttpMinResponseDataRateFeature + { + /// + /// 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. + /// + MinDataRate MinDataRate { get; set; } + } +} diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/FrameConnection.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/FrameConnection.cs index 6d053da559..1574d3bf7e 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/FrameConnection.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/FrameConnection.cs @@ -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--; + } + } } } diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/Frame.FeatureCollection.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/Frame.FeatureCollection.cs index cbc30427e5..3cf9d22fac 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/Frame.FeatureCollection.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/Frame.FeatureCollection.cs @@ -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); diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/Frame.Generated.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/Frame.Generated.cs index 6aba909708..c4403a60ce 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/Frame.Generated.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/Frame.Generated.cs @@ -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(IHttpMinRequestBodyDataRateFeatureType, _currentIHttpMinRequestBodyDataRateFeature as global::Microsoft.AspNetCore.Server.Kestrel.Core.Features.IHttpMinRequestBodyDataRateFeature); } + if (_currentIHttpMinResponseDataRateFeature != null) + { + yield return new KeyValuePair(IHttpMinResponseDataRateFeatureType, _currentIHttpMinResponseDataRateFeature as global::Microsoft.AspNetCore.Server.Kestrel.Core.Features.IHttpMinResponseDataRateFeature); + } if (_currentIHttpBodyControlFeature != null) { yield return new KeyValuePair(IHttpBodyControlFeatureType, _currentIHttpBodyControlFeature as global::Microsoft.AspNetCore.Http.Features.IHttpBodyControlFeature); diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/Frame.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/Frame.cs index c9e7ba118b..e6b1187438 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/Frame.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/Frame.cs @@ -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; } /// @@ -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); diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/OutputProducer.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/OutputProducer.cs index 5f58ef6ae4..d9fce78492 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/OutputProducer.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Http/OutputProducer.cs @@ -14,6 +14,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http private static readonly ArraySegment _emptyData = new ArraySegment(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(); } diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Infrastructure/IKestrelTrace.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Infrastructure/IKestrelTrace.cs index b7d68eb03a..456e132274 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Infrastructure/IKestrelTrace.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Infrastructure/IKestrelTrace.cs @@ -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); } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Infrastructure/ITimeoutControl.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Infrastructure/ITimeoutControl.cs index 6b23f37c94..4ed025ac72 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Infrastructure/ITimeoutControl.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Infrastructure/ITimeoutControl.cs @@ -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(); } } diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Infrastructure/KestrelTrace.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Infrastructure/KestrelTrace.cs index 115d44184d..ab6eb51293 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Infrastructure/KestrelTrace.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Internal/Infrastructure/KestrelTrace.cs @@ -61,7 +61,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal LoggerMessage.Define(LogLevel.Debug, 26, @"Connection id ""{ConnectionId}"", Request id ""{TraceIdentifier}"": done reading request body."); private static readonly Action _requestBodyMinimumDataRateNotSatisfied = - LoggerMessage.Define(LogLevel.Information, 27, @"Connection id ""{ConnectionId}"", Request id ""{TraceIdentifier}"": request body incoming data rate dropped below {Rate} bytes/second."); + LoggerMessage.Define(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 _responseMinimumDataRateNotSatisfied = + LoggerMessage.Define(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(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) => _logger.Log(logLevel, eventId, state, exception, formatter); diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/KestrelServerLimits.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Core/KestrelServerLimits.cs index 528f78be57..eb061cf500 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/KestrelServerLimits.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/KestrelServerLimits.cs @@ -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)); + + /// + /// 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 . + /// + /// + /// + /// Defaults to 240 bytes/second with a 5 second grace period. + /// + /// + /// Contrary to the request body minimum data rate, this rate applies to the response status line and headers as well. + /// + /// + /// 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. + /// + /// + public MinDataRate MinResponseDataRate { get; set; } = + // Matches the default IIS minBytesPerSecond + new MinDataRate(bytesPerSecond: 240, gracePeriod: TimeSpan.FromSeconds(5)); } } diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/MinDataRate.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Core/MinDataRate.cs index 34cbdc577d..0e320b37f1 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/MinDataRate.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/MinDataRate.cs @@ -16,9 +16,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core /// starting at the time data is first read or written. 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) diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Properties/CoreStrings.Designer.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Properties/CoreStrings.Designer.cs index aa43e6b644..4598adc6fa 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/Properties/CoreStrings.Designer.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/Properties/CoreStrings.Designer.cs @@ -1060,6 +1060,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core internal static string FormatSynchronousWritesDisallowed() => GetString("SynchronousWritesDisallowed"); + /// + /// Value must be a positive number. To disable a minimum data rate, use null where a MinDataRate instance is expected. + /// + internal static string PositiveNumberOrNullMinDataRateRequired + { + get => GetString("PositiveNumberOrNullMinDataRateRequired"); + } + + /// + /// Value must be a positive number. To disable a minimum data rate, use null where a MinDataRate instance is expected. + /// + internal static string FormatPositiveNumberOrNullMinDataRateRequired() + => GetString("PositiveNumberOrNullMinDataRateRequired"); + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv/Internal/LibuvAwaitable.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv/Internal/LibuvAwaitable.cs index 7029a1c32e..8ee11ff42e 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv/Internal/LibuvAwaitable.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv/Internal/LibuvAwaitable.cs @@ -79,4 +79,4 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal Error = error; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv/Internal/LibuvConnection.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv/Internal/LibuvConnection.cs index 2052ac45f2..2bac70080e 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv/Internal/LibuvConnection.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv/Internal/LibuvConnection.cs @@ -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(); diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv/Internal/LibuvOutputConsumer.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv/Internal/LibuvOutputConsumer.cs index c49b5bde5b..957ceb9595 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv/Internal/LibuvOutputConsumer.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv/Internal/LibuvOutputConsumer.cs @@ -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) diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/FrameConnectionTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/FrameConnectionTests.cs index 790ce56d7e..8c1d74eb24 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/FrameConnectionTests.cs +++ b/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/FrameConnectionTests.cs @@ -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(); _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(); _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(); _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(); _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(), It.IsAny(), It.IsAny()), 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(); + _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(); + _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(); + _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(); + _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))); + } } } diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/FrameTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/FrameTests.cs index bd368df0c6..9531abb48e 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/FrameTests.cs +++ b/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/FrameTests.cs @@ -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().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().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 MinRequestBodyDataRateData => new TheoryData + public static TheoryData MinDataRateData => new TheoryData { null, new MinDataRate(bytesPerSecond: 1, gracePeriod: TimeSpan.MaxValue) diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/KestrelServerLimitsTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/KestrelServerLimitsTests.cs index bab86a04e7..66de5d41e5 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/KestrelServerLimitsTests.cs +++ b/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/KestrelServerLimitsTests.cs @@ -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 TimeoutValidData => new TheoryData { TimeSpan.FromTicks(1), diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/MinDataRateTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/MinDataRateTests.cs index bd21c01f6d..a87bfa7709 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/MinDataRateTests.cs +++ b/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/MinDataRateTests.cs @@ -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(() => 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] diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/OutputProducerTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/OutputProducerTests.cs index 0298bbff95..027e26f44e 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/OutputProducerTests.cs +++ b/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests/OutputProducerTests.cs @@ -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(null, frameContext); - var socketOutput = new OutputProducer(pipe, "0", serviceContext.Log); + var socketOutput = new OutputProducer( + pipe, + "0", + serviceContext.Log, + Mock.Of()); return socketOutput; } diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/RequestTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/RequestTests.cs index 4fb7a4bc86..8d8f997b03 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/RequestTests.cs +++ b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/RequestTests.cs @@ -1519,10 +1519,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests // Synchronous reads now throw. var ioEx = Assert.Throws(() => 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(() => 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(() => 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(() => 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; diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/ResponseTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/ResponseTests.cs index f43a1e5c2b..7f26aec3bf 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/ResponseTests.cs +++ b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/ResponseTests.cs @@ -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(() => 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(() => 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(); + mockKestrelTrace + .Setup(trace => trace.ResponseMininumDataRateNotSatisfied(It.IsAny(), It.IsAny())) + .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(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(); + mockKestrelTrace + .Setup(trace => trace.ResponseMininumDataRateNotSatisfied(It.IsAny(), It.IsAny())) + .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 NullHeaderData { get diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.Performance/Mocks/MockTimeoutControl.cs b/test/Microsoft.AspNetCore.Server.Kestrel.Performance/Mocks/MockTimeoutControl.cs index 8bfd08387e..e0a54ef642 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.Performance/Mocks/MockTimeoutControl.cs +++ b/test/Microsoft.AspNetCore.Server.Kestrel.Performance/Mocks/MockTimeoutControl.cs @@ -40,5 +40,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance.Mocks public void BytesRead(int count) { } + + public void StartTimingWrite(int size) + { + } + + public void StopTimingWrite() + { + } } } diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.Performance/Mocks/MockTrace.cs b/test/Microsoft.AspNetCore.Server.Kestrel.Performance/Mocks/MockTrace.cs index 480ec400a3..77290ee718 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.Performance/Mocks/MockTrace.cs +++ b/test/Microsoft.AspNetCore.Server.Kestrel.Performance/Mocks/MockTrace.cs @@ -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) { } } } diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests/LibuvOutputConsumerTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests/LibuvOutputConsumerTests.cs index 2d565d7168..c91ef7994f 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests/LibuvOutputConsumerTests.cs +++ b/test/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests/LibuvOutputConsumerTests.cs @@ -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(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(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(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(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(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(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(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(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(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 } } } -} +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests/TestHelpers/MockLibuv.cs b/test/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests/TestHelpers/MockLibuv.cs index 8906dbd872..4f334bcfea 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests/TestHelpers/MockLibuv.cs +++ b/test/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests/TestHelpers/MockLibuv.cs @@ -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(); - _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(); + _completedOnPostTcs = false; + } + + PostCount++; + + _sendCalled = true; + _loopWh.Set(); + } } } diff --git a/test/shared/TestServiceContext.cs b/test/shared/TestServiceContext.cs index 696178c0fa..2d9b772e43 100644 --- a/test/shared/TestServiceContext.cs +++ b/test/shared/TestServiceContext.cs @@ -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; diff --git a/tools/CodeGenerator/FrameFeatureCollection.cs b/tools/CodeGenerator/FrameFeatureCollection.cs index 5d5f83c11a..5fd8d0a8f5 100644 --- a/tools/CodeGenerator/FrameFeatureCollection.cs +++ b/tools/CodeGenerator/FrameFeatureCollection.cs @@ -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), };