Add HTTP/2 response draining timeout (#2956)

* Move drain timeout to Http1Connection
* Add TimeoutControl class
This commit is contained in:
Stephen Halter 2018-09-27 17:59:12 -07:00 committed by GitHub
parent 04ec102e96
commit e9c2acf4dc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
60 changed files with 1120 additions and 1215 deletions

View File

@ -37,7 +37,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
HttpParser = NullParser<Http1ParsingHandler>.Instance HttpParser = NullParser<Http1ParsingHandler>.Instance
}; };
var http1Connection = new Http1Connection(context: new Http1ConnectionContext var http1Connection = new Http1Connection(context: new HttpConnectionContext
{ {
ServiceContext = serviceContext, ServiceContext = serviceContext,
ConnectionFeatures = new FeatureCollection(), ConnectionFeatures = new FeatureCollection(),

View File

@ -33,7 +33,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
HttpParser = NullParser<Http1ParsingHandler>.Instance HttpParser = NullParser<Http1ParsingHandler>.Instance
}; };
var http1Connection = new Http1Connection(new Http1ConnectionContext var http1Connection = new Http1Connection(new HttpConnectionContext
{ {
ServiceContext = serviceContext, ServiceContext = serviceContext,
ConnectionFeatures = new FeatureCollection(), ConnectionFeatures = new FeatureCollection(),

View File

@ -108,7 +108,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
HttpParser = new HttpParser<Http1ParsingHandler>() HttpParser = new HttpParser<Http1ParsingHandler>()
}; };
var http1Connection = new TestHttp1Connection(new Http1ConnectionContext var http1Connection = new TestHttp1Connection(new HttpConnectionContext
{ {
ServiceContext = serviceContext, ServiceContext = serviceContext,
ConnectionFeatures = new FeatureCollection(), ConnectionFeatures = new FeatureCollection(),

View File

@ -91,7 +91,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
HttpParser = new HttpParser<Http1ParsingHandler>() HttpParser = new HttpParser<Http1ParsingHandler>()
}; };
var http1Connection = new Http1Connection(new Http1ConnectionContext var http1Connection = new Http1Connection(new HttpConnectionContext
{ {
ServiceContext = serviceContext, ServiceContext = serviceContext,
ConnectionFeatures = new FeatureCollection(), ConnectionFeatures = new FeatureCollection(),

View File

@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved. // Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
namespace Microsoft.AspNetCore.Server.Kestrel.Performance.Mocks namespace Microsoft.AspNetCore.Server.Kestrel.Performance.Mocks
@ -11,15 +12,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance.Mocks
{ {
} }
public void ResetTimeout(long ticks, TimeoutAction timeoutAction) public void ResetTimeout(long ticks, TimeoutReason timeoutReason)
{ {
} }
public void SetTimeout(long ticks, TimeoutAction timeoutAction) public void SetTimeout(long ticks, TimeoutReason timeoutReason)
{ {
} }
public void StartTimingReads() public void StartTimingReads(MinDataRate minRate)
{ {
} }
@ -39,7 +40,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance.Mocks
{ {
} }
public void StartTimingWrite(long size) public void StartTimingWrite(MinDataRate rate, long size)
{ {
} }

View File

@ -36,7 +36,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
HttpParser = new HttpParser<Http1ParsingHandler>() HttpParser = new HttpParser<Http1ParsingHandler>()
}; };
var http1Connection = new Http1Connection(new Http1ConnectionContext var http1Connection = new Http1Connection(new HttpConnectionContext
{ {
ServiceContext = serviceContext, ServiceContext = serviceContext,
ConnectionFeatures = new FeatureCollection(), ConnectionFeatures = new FeatureCollection(),

View File

@ -184,7 +184,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
HttpParser = new HttpParser<Http1ParsingHandler>() HttpParser = new HttpParser<Http1ParsingHandler>()
}; };
var http1Connection = new Http1Connection(new Http1ConnectionContext var http1Connection = new Http1Connection(new HttpConnectionContext
{ {
ServiceContext = serviceContext, ServiceContext = serviceContext,
ConnectionFeatures = new FeatureCollection(), ConnectionFeatures = new FeatureCollection(),

View File

@ -127,7 +127,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
HttpParser = new HttpParser<Http1ParsingHandler>() HttpParser = new HttpParser<Http1ParsingHandler>()
}; };
var http1Connection = new TestHttp1Connection(new Http1ConnectionContext var http1Connection = new TestHttp1Connection(new HttpConnectionContext
{ {
ServiceContext = serviceContext, ServiceContext = serviceContext,
ConnectionFeatures = new FeatureCollection(), ConnectionFeatures = new FeatureCollection(),

View File

@ -3,7 +3,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO.Pipelines; using System.IO.Pipelines;
using System.Threading;
using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Connections.Features;
using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Features;

View File

@ -584,4 +584,7 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
<data name="HPackErrorIntegerTooBig" xml:space="preserve"> <data name="HPackErrorIntegerTooBig" xml:space="preserve">
<value>The decoded integer exceeds the maximum value of Int32.MaxValue.</value> <value>The decoded integer exceeds the maximum value of Int32.MaxValue.</value>
</data> </data>
<data name="ConnectionAbortedByClient" xml:space="preserve">
<value>The client closed the connection.</value>
</data>
</root> </root>

View File

@ -6,9 +6,7 @@ using System.Buffers;
using System.IO.Pipelines; using System.IO.Pipelines;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Connections.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -84,12 +82,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
} }
finally finally
{ {
Log.ConnectionStop(connectionContext.ConnectionId);
KestrelEventSource.Log.ConnectionStop(connectionContext);
connection.Complete(); connection.Complete();
_serviceContext.ConnectionManager.RemoveConnection(id); _serviceContext.ConnectionManager.RemoveConnection(id);
Log.ConnectionStop(connectionContext.ConnectionId);
KestrelEventSource.Log.ConnectionStop(connectionContext);
} }
} }

View File

@ -21,8 +21,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
private const byte ByteForwardSlash = (byte)'/'; private const byte ByteForwardSlash = (byte)'/';
private const string Asterisk = "*"; private const string Asterisk = "*";
private readonly Http1ConnectionContext _context; private readonly HttpConnectionContext _context;
private readonly IHttpParser<Http1ParsingHandler> _parser; private readonly IHttpParser<Http1ParsingHandler> _parser;
private readonly Http1OutputProducer _http1Output;
protected readonly long _keepAliveTicks; protected readonly long _keepAliveTicks;
private readonly long _requestHeadersTimeoutTicks; private readonly long _requestHeadersTimeoutTicks;
@ -35,7 +36,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
private int _remainingRequestHeadersBytesAllowed; private int _remainingRequestHeadersBytesAllowed;
public Http1Connection(Http1ConnectionContext context) public Http1Connection(HttpConnectionContext context)
: base(context) : base(context)
{ {
_context = context; _context = context;
@ -44,13 +45,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
_requestHeadersTimeoutTicks = ServerOptions.Limits.RequestHeadersTimeout.Ticks; _requestHeadersTimeoutTicks = ServerOptions.Limits.RequestHeadersTimeout.Ticks;
RequestBodyPipe = CreateRequestBodyPipe(); RequestBodyPipe = CreateRequestBodyPipe();
Output = new Http1OutputProducer(
_http1Output = new Http1OutputProducer(
_context.Transport.Output, _context.Transport.Output,
_context.ConnectionId, _context.ConnectionId,
_context.ConnectionContext, _context.ConnectionContext,
_context.ServiceContext.Log, _context.ServiceContext.Log,
_context.TimeoutControl, _context.TimeoutControl,
_context.ConnectionFeatures.Get<IBytesWrittenFeature>()); this);
Output = _http1Output;
} }
public PipeReader Input => _context.Transport.Input; public PipeReader Input => _context.Transport.Input;
@ -60,6 +64,24 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
public override bool IsUpgradableRequest => _upgradeAvailable; public override bool IsUpgradableRequest => _upgradeAvailable;
protected override void OnRequestProcessingEnded()
{
Input.Complete();
TimeoutControl.StartDrainTimeout(MinResponseDataRate, ServerOptions.Limits.MaxResponseBufferSize);
// Prevent RequestAborted from firing. Free up unneeded feature references.
Reset();
_http1Output.Dispose();
}
public void OnInputOrOutputCompleted()
{
_http1Output.Abort(new ConnectionAbortedException(CoreStrings.ConnectionAbortedByClient));
AbortRequest();
}
/// <summary> /// <summary>
/// Immediately kill the connection and poison the request body stream with an error. /// Immediately kill the connection and poison the request body stream with an error.
/// </summary> /// </summary>
@ -70,11 +92,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
return; return;
} }
// Abort output prior to calling OnIOCompleted() to give the transport the chance to complete the input _http1Output.Abort(abortReason);
// with the correct error and message.
Output.Abort(abortReason);
OnInputOrOutputCompleted(); AbortRequest();
PoisonRequestBodyStream(abortReason); PoisonRequestBodyStream(abortReason);
} }
@ -115,7 +135,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
break; break;
} }
TimeoutControl.ResetTimeout(_requestHeadersTimeoutTicks, TimeoutAction.SendTimeoutResponse); TimeoutControl.ResetTimeout(_requestHeadersTimeoutTicks, TimeoutReason.RequestHeaders);
_requestProcessingStatus = RequestProcessingStatus.ParsingRequestLine; _requestProcessingStatus = RequestProcessingStatus.ParsingRequestLine;
goto case RequestProcessingStatus.ParsingRequestLine; goto case RequestProcessingStatus.ParsingRequestLine;
@ -411,7 +431,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
protected override void OnRequestProcessingEnding() protected override void OnRequestProcessingEnding()
{ {
Input.Complete();
} }
protected override string CreateRequestId() protected override string CreateRequestId()
@ -424,7 +443,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{ {
// Reset the features and timeout. // Reset the features and timeout.
Reset(); Reset();
TimeoutControl.SetTimeout(_keepAliveTicks, TimeoutAction.StopProcessingNextRequest); TimeoutControl.SetTimeout(_keepAliveTicks, TimeoutReason.KeepAlive);
} }
protected override bool BeginRead(out ValueTask<ReadResult> awaitable) protected override bool BeginRead(out ValueTask<ReadResult> awaitable)

View File

@ -1,25 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Buffers;
using System.IO.Pipelines;
using System.Net;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
public class Http1ConnectionContext : IHttpProtocolContext
{
public string ConnectionId { get; set; }
public ServiceContext ServiceContext { get; set; }
public ConnectionContext ConnectionContext { get; set; }
public IFeatureCollection ConnectionFeatures { get; set; }
public MemoryPool<byte> MemoryPool { get; set; }
public IPEndPoint RemoteEndPoint { get; set; }
public IPEndPoint LocalEndPoint { get; set; }
public ITimeoutControl TimeoutControl { get; set; }
public IDuplexPipe Transport { get; set; }
}
}

View File

@ -18,6 +18,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
private volatile bool _canceled; private volatile bool _canceled;
private Task _pumpTask; private Task _pumpTask;
private bool _timingReads;
protected Http1MessageBody(Http1Connection context) protected Http1MessageBody(Http1Connection context)
: base(context) : base(context)
@ -175,7 +176,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{ {
Log.RequestBodyNotEntirelyRead(_context.ConnectionIdFeature, _context.TraceIdentifier); Log.RequestBodyNotEntirelyRead(_context.ConnectionIdFeature, _context.TraceIdentifier);
_context.TimeoutControl.SetTimeout(Constants.RequestBodyDrainTimeout.Ticks, TimeoutAction.AbortConnection); _context.TimeoutControl.SetTimeout(Constants.RequestBodyDrainTimeout.Ticks, TimeoutReason.RequestBodyDrain);
try try
{ {
@ -232,13 +233,22 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
if (!RequestUpgrade) if (!RequestUpgrade)
{ {
Log.RequestBodyStart(_context.ConnectionIdFeature, _context.TraceIdentifier); Log.RequestBodyStart(_context.ConnectionIdFeature, _context.TraceIdentifier);
_context.TimeoutControl.StartTimingReads();
// REVIEW: This makes it no longer effective to change the min rate after the app starts reading.
// Is this OK? Should we throw from the MinRequestBodyDataRate setter in this case?
var minRate = _context.MinRequestBodyDataRate;
if (minRate != null)
{
_timingReads = true;
_context.TimeoutControl.StartTimingReads(minRate);
}
} }
} }
private void TryPauseTimingReads() private void TryPauseTimingReads()
{ {
if (!RequestUpgrade) if (_timingReads)
{ {
_context.TimeoutControl.PauseTimingReads(); _context.TimeoutControl.PauseTimingReads();
} }
@ -246,7 +256,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
private void TryResumeTimingReads() private void TryResumeTimingReads()
{ {
if (!RequestUpgrade) if (_timingReads)
{ {
_context.TimeoutControl.ResumeTimingReads(); _context.TimeoutControl.ResumeTimingReads();
} }
@ -257,7 +267,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
if (!RequestUpgrade) if (!RequestUpgrade)
{ {
Log.RequestBodyDone(_context.ConnectionIdFeature, _context.TraceIdentifier); Log.RequestBodyDone(_context.ConnectionIdFeature, _context.TraceIdentifier);
_context.TimeoutControl.StopTimingReads();
if (_timingReads)
{
_context.TimeoutControl.StopTimingReads();
}
} }
} }

View File

@ -8,12 +8,12 @@ using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{ {
public class Http1OutputProducer : IHttpOutputProducer public class Http1OutputProducer : IHttpOutputProducer, IHttpOutputAborter, IDisposable
{ {
private static readonly ReadOnlyMemory<byte> _continueBytes = new ReadOnlyMemory<byte>(Encoding.ASCII.GetBytes("HTTP/1.1 100 Continue\r\n\r\n")); private static readonly ReadOnlyMemory<byte> _continueBytes = new ReadOnlyMemory<byte>(Encoding.ASCII.GetBytes("HTTP/1.1 100 Continue\r\n\r\n"));
private static readonly byte[] _bytesHttpVersion11 = Encoding.ASCII.GetBytes("HTTP/1.1 "); private static readonly byte[] _bytesHttpVersion11 = Encoding.ASCII.GetBytes("HTTP/1.1 ");
@ -22,10 +22,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
private readonly string _connectionId; private readonly string _connectionId;
private readonly ConnectionContext _connectionContext; private readonly ConnectionContext _connectionContext;
private readonly ITimeoutControl _timeoutControl;
private readonly IKestrelTrace _log; private readonly IKestrelTrace _log;
private readonly IBytesWrittenFeature _transportBytesWrittenFeature; private readonly IHttpMinResponseDataRateFeature _minResponseDataRateFeature;
private readonly StreamSafePipeFlusher _flusher; private readonly TimingPipeFlusher _flusher;
// This locks access to to all of the below fields // This locks access to to all of the below fields
private readonly object _contextLock = new object(); private readonly object _contextLock = new object();
@ -33,24 +32,23 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
private bool _completed = false; private bool _completed = false;
private bool _aborted; private bool _aborted;
private long _unflushedBytes; private long _unflushedBytes;
private long _totalBytesCommitted;
private readonly PipeWriter _pipeWriter; private readonly PipeWriter _pipeWriter;
public Http1OutputProducer( public Http1OutputProducer(
PipeWriter pipeWriter, PipeWriter pipeWriter,
string connectionId, string connectionId,
ConnectionContext connectionContext, ConnectionContext connectionContext,
IKestrelTrace log, IKestrelTrace log,
ITimeoutControl timeoutControl, ITimeoutControl timeoutControl,
IBytesWrittenFeature transportBytesWrittenFeature) IHttpMinResponseDataRateFeature minResponseDataRateFeature)
{ {
_pipeWriter = pipeWriter; _pipeWriter = pipeWriter;
_connectionId = connectionId; _connectionId = connectionId;
_connectionContext = connectionContext; _connectionContext = connectionContext;
_timeoutControl = timeoutControl;
_log = log; _log = log;
_transportBytesWrittenFeature = transportBytesWrittenFeature; _minResponseDataRateFeature = minResponseDataRateFeature;
_flusher = new StreamSafePipeFlusher(pipeWriter, timeoutControl); _flusher = new TimingPipeFlusher(pipeWriter, timeoutControl);
} }
public Task WriteDataAsync(ReadOnlySpan<byte> buffer, CancellationToken cancellationToken = default) public Task WriteDataAsync(ReadOnlySpan<byte> buffer, CancellationToken cancellationToken = default)
@ -85,7 +83,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
var buffer = _pipeWriter; var buffer = _pipeWriter;
var bytesCommitted = callback(buffer, state); var bytesCommitted = callback(buffer, state);
_unflushedBytes += bytesCommitted; _unflushedBytes += bytesCommitted;
_totalBytesCommitted += bytesCommitted;
} }
return FlushAsync(cancellationToken); return FlushAsync(cancellationToken);
@ -112,7 +109,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
writer.Commit(); writer.Commit();
_unflushedBytes += writer.BytesCommitted; _unflushedBytes += writer.BytesCommitted;
_totalBytesCommitted += writer.BytesCommitted;
} }
} }
@ -128,15 +124,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
_log.ConnectionDisconnect(_connectionId); _log.ConnectionDisconnect(_connectionId);
_completed = true; _completed = true;
_pipeWriter.Complete(); _pipeWriter.Complete();
var unsentBytes = _totalBytesCommitted - _transportBytesWrittenFeature.TotalBytesWritten;
if (unsentBytes > 0)
{
// unsentBytes should never be over 64KB in the default configuration.
_timeoutControl.StartTimingWrite((int)Math.Min(unsentBytes, int.MaxValue));
_pipeWriter.OnReaderCompleted((ex, state) => ((ITimeoutControl)state).StopTimingWrite(), _timeoutControl);
}
} }
} }
@ -154,13 +141,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
_aborted = true; _aborted = true;
_connectionContext.Abort(error); _connectionContext.Abort(error);
Dispose();
if (!_completed)
{
_log.ConnectionDisconnect(_connectionId);
_completed = true;
_pipeWriter.Complete();
}
} }
} }
@ -186,14 +167,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
writer.Write(buffer); writer.Write(buffer);
_unflushedBytes += buffer.Length; _unflushedBytes += buffer.Length;
_totalBytesCommitted += buffer.Length;
} }
writer.Commit(); writer.Commit();
var bytesWritten = _unflushedBytes; var bytesWritten = _unflushedBytes;
_unflushedBytes = 0; _unflushedBytes = 0;
return _flusher.FlushAsync(bytesWritten, this, cancellationToken); return _flusher.FlushAsync(
_minResponseDataRateFeature.MinDataRate,
bytesWritten,
this,
cancellationToken);
} }
} }
} }

View File

@ -21,8 +21,6 @@ using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives; using Microsoft.Extensions.Primitives;
// ReSharper disable AccessToModifiedClosure
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{ {
public abstract partial class HttpProtocol : IHttpResponseControl public abstract partial class HttpProtocol : IHttpResponseControl
@ -60,12 +58,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
private long _responseBytesWritten; private long _responseBytesWritten;
private readonly IHttpProtocolContext _context; private readonly HttpConnectionContext _context;
protected string _methodText = null; protected string _methodText = null;
private string _scheme = null; private string _scheme = null;
public HttpProtocol(IHttpProtocolContext context) public HttpProtocol(HttpConnectionContext context)
{ {
_context = context; _context = context;
@ -412,7 +410,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
} }
} }
public void OnInputOrOutputCompleted() protected void AbortRequest()
{ {
if (Interlocked.Exchange(ref _ioCompleted, 1) != 0) if (Interlocked.Exchange(ref _ioCompleted, 1) != 0)
{ {
@ -421,8 +419,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
_keepAlive = false; _keepAlive = false;
Output.Dispose();
// Potentially calling user code. CancelRequestAbortedToken logs any exceptions. // Potentially calling user code. CancelRequestAbortedToken logs any exceptions.
ServiceContext.Scheduler.Schedule(state => ((HttpProtocol)state).CancelRequestAbortedToken(), this); ServiceContext.Scheduler.Schedule(state => ((HttpProtocol)state).CancelRequestAbortedToken(), this);
} }
@ -476,13 +472,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{ {
try try
{ {
OnRequestProcessingEnding();
await TryProduceInvalidRequestResponse(); await TryProduceInvalidRequestResponse();
// Prevent RequestAborted from firing.
Reset();
Output.Dispose();
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@ -0,0 +1,12 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Connections;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
public interface IHttpOutputAborter
{
void Abort(ConnectionAbortedException abortReason);
}
}

View File

@ -5,20 +5,16 @@ using System;
using System.IO.Pipelines; using System.IO.Pipelines;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Connections;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{ {
public interface IHttpOutputProducer : IDisposable public interface IHttpOutputProducer
{ {
void Abort(ConnectionAbortedException abortReason);
Task WriteAsync<T>(Func<PipeWriter, T, long> callback, T state, CancellationToken cancellationToken); Task WriteAsync<T>(Func<PipeWriter, T, long> callback, T state, CancellationToken cancellationToken);
Task FlushAsync(CancellationToken cancellationToken); Task FlushAsync(CancellationToken cancellationToken);
Task Write100ContinueAsync(); Task Write100ContinueAsync();
void WriteResponseHeaders(int statusCode, string ReasonPhrase, HttpResponseHeaders responseHeaders); void WriteResponseHeaders(int statusCode, string ReasonPhrase, HttpResponseHeaders responseHeaders);
// The reason this is ReadOnlySpan and not ReadOnlyMemory is because writes are always // This takes ReadOnlySpan instead of ReadOnlyMemory because it always synchronously copies data before flushing.
// synchronous. Flushing to get back pressure is the only time we truly go async but
// that's after the buffer is copied
Task WriteDataAsync(ReadOnlySpan<byte> data, CancellationToken cancellationToken); Task WriteDataAsync(ReadOnlySpan<byte> data, CancellationToken cancellationToken);
Task WriteStreamSuffixAsync(); Task WriteStreamSuffixAsync();
} }

View File

@ -1,19 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Buffers;
using System.Net;
using Microsoft.AspNetCore.Http.Features;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
public interface IHttpProtocolContext
{
string ConnectionId { get; set; }
ServiceContext ServiceContext { get; set; }
IFeatureCollection ConnectionFeatures { get; set; }
MemoryPool<byte> MemoryPool { get; set; }
IPEndPoint RemoteEndPoint { get; set; }
IPEndPoint LocalEndPoint { get; set; }
}
}

View File

@ -23,7 +23,7 @@ using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
{ {
public class Http2Connection : ITimeoutControl, IHttp2StreamLifetimeHandler, IHttpHeadersHandler, IRequestProcessor public class Http2Connection : IHttp2StreamLifetimeHandler, IHttpHeadersHandler, IRequestProcessor
{ {
private enum RequestHeaderParsingState private enum RequestHeaderParsingState
{ {
@ -60,7 +60,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
private static readonly byte[] _trailersBytes = Encoding.ASCII.GetBytes("trailers"); private static readonly byte[] _trailersBytes = Encoding.ASCII.GetBytes("trailers");
private static readonly byte[] _connectBytes = Encoding.ASCII.GetBytes("CONNECT"); private static readonly byte[] _connectBytes = Encoding.ASCII.GetBytes("CONNECT");
private readonly Http2ConnectionContext _context; private readonly HttpConnectionContext _context;
private readonly Http2FrameWriter _frameWriter; private readonly Http2FrameWriter _frameWriter;
private readonly HPackDecoder _hpackDecoder; private readonly HPackDecoder _hpackDecoder;
private readonly InputFlowControl _inputFlowControl; private readonly InputFlowControl _inputFlowControl;
@ -84,13 +84,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
private readonly ConcurrentDictionary<int, Http2Stream> _streams = new ConcurrentDictionary<int, Http2Stream>(); private readonly ConcurrentDictionary<int, Http2Stream> _streams = new ConcurrentDictionary<int, Http2Stream>();
public Http2Connection(Http2ConnectionContext context) public Http2Connection(HttpConnectionContext context)
{ {
var httpLimits = context.ServiceContext.ServerOptions.Limits; var httpLimits = context.ServiceContext.ServerOptions.Limits;
var http2Limits = httpLimits.Http2; var http2Limits = httpLimits.Http2;
_context = context; _context = context;
_frameWriter = new Http2FrameWriter(context.Transport.Output, context.ConnectionContext, _outputFlowControl, this, context.ConnectionId, context.ServiceContext.Log); _frameWriter = new Http2FrameWriter(context.Transport.Output, context.ConnectionContext, _outputFlowControl, context.TimeoutControl, context.ConnectionId, context.ServiceContext.Log);
_serverSettings.MaxConcurrentStreams = (uint)http2Limits.MaxStreamsPerConnection; _serverSettings.MaxConcurrentStreams = (uint)http2Limits.MaxStreamsPerConnection;
_serverSettings.MaxFrameSize = (uint)http2Limits.MaxFrameSize; _serverSettings.MaxFrameSize = (uint)http2Limits.MaxFrameSize;
_serverSettings.HeaderTableSize = (uint)http2Limits.HeaderTableSize; _serverSettings.HeaderTableSize = (uint)http2Limits.HeaderTableSize;
@ -102,12 +102,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
} }
public string ConnectionId => _context.ConnectionId; public string ConnectionId => _context.ConnectionId;
public PipeReader Input => _context.Transport.Input; public PipeReader Input => _context.Transport.Input;
public IKestrelTrace Log => _context.ServiceContext.Log; public IKestrelTrace Log => _context.ServiceContext.Log;
public IFeatureCollection ConnectionFeatures => _context.ConnectionFeatures; public IFeatureCollection ConnectionFeatures => _context.ConnectionFeatures;
public KestrelServerOptions ServerOptions => _context.ServiceContext.ServerOptions;
internal Http2PeerSettings ServerSettings => _serverSettings; internal Http2PeerSettings ServerSettings => _serverSettings;
@ -117,12 +115,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
{ {
if (_state != Http2ConnectionState.Closed) if (_state != Http2ConnectionState.Closed)
{ {
_frameWriter.WriteGoAwayAsync(_highestOpenedStreamId, Http2ErrorCode.NO_ERROR);
UpdateState(Http2ConnectionState.Closed); UpdateState(Http2ConnectionState.Closed);
} }
} }
_frameWriter.Complete(); _frameWriter.Abort(new ConnectionAbortedException(CoreStrings.ConnectionAbortedByClient));
} }
public void Abort(ConnectionAbortedException ex) public void Abort(ConnectionAbortedException ex)
@ -300,6 +297,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
await _streamsCompleted.Task; await _streamsCompleted.Task;
_context.TimeoutControl.StartDrainTimeout(ServerOptions.Limits.MinResponseDataRate, ServerOptions.Limits.MaxResponseBufferSize);
_frameWriter.Complete(); _frameWriter.Complete();
} }
catch catch
@ -556,7 +555,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
FrameWriter = _frameWriter, FrameWriter = _frameWriter,
ConnectionInputFlowControl = _inputFlowControl, ConnectionInputFlowControl = _inputFlowControl,
ConnectionOutputFlowControl = _outputFlowControl, ConnectionOutputFlowControl = _outputFlowControl,
TimeoutControl = this, TimeoutControl = _context.TimeoutControl,
}); });
_currentHeadersStream.Reset(); _currentHeadersStream.Reset();
@ -1137,45 +1136,5 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
Log.Http2ConnectionClosed(_context.ConnectionId, _highestOpenedStreamId); Log.Http2ConnectionClosed(_context.ConnectionId, _highestOpenedStreamId);
} }
} }
void ITimeoutControl.SetTimeout(long ticks, TimeoutAction timeoutAction)
{
}
void ITimeoutControl.ResetTimeout(long ticks, TimeoutAction timeoutAction)
{
}
void ITimeoutControl.CancelTimeout()
{
}
void ITimeoutControl.StartTimingReads()
{
}
void ITimeoutControl.PauseTimingReads()
{
}
void ITimeoutControl.ResumeTimingReads()
{
}
void ITimeoutControl.StopTimingReads()
{
}
void ITimeoutControl.BytesRead(long count)
{
}
void ITimeoutControl.StartTimingWrite(long size)
{
}
void ITimeoutControl.StopTimingWrite()
{
}
} }
} }

View File

@ -1,23 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Buffers;
using System.IO.Pipelines;
using System.Net;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Http.Features;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
{
public class Http2ConnectionContext
{
public string ConnectionId { get; set; }
public ConnectionContext ConnectionContext { get; set; }
public ServiceContext ServiceContext { get; set; }
public IFeatureCollection ConnectionFeatures { get; set; }
public MemoryPool<byte> MemoryPool { get; set; }
public IPEndPoint LocalEndPoint { get; set; }
public IPEndPoint RemoteEndPoint { get; set; }
public IDuplexPipe Transport { get; set; }
}
}

View File

@ -34,7 +34,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
private readonly OutputFlowControl _connectionOutputFlowControl; private readonly OutputFlowControl _connectionOutputFlowControl;
private readonly string _connectionId; private readonly string _connectionId;
private readonly IKestrelTrace _log; private readonly IKestrelTrace _log;
private readonly StreamSafePipeFlusher _flusher; private readonly TimingPipeFlusher _flusher;
private bool _completed; private bool _completed;
@ -51,7 +51,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
_connectionOutputFlowControl = connectionOutputFlowControl; _connectionOutputFlowControl = connectionOutputFlowControl;
_connectionId = connectionId; _connectionId = connectionId;
_log = log; _log = log;
_flusher = new StreamSafePipeFlusher(_outputWriter, timeoutControl); _flusher = new TimingPipeFlusher(_outputWriter, timeoutControl);
_outgoingFrame = new Http2Frame(); _outgoingFrame = new Http2Frame();
_headerEncodingBuffer = new byte[_maxFrameSize]; _headerEncodingBuffer = new byte[_maxFrameSize];
} }
@ -99,7 +99,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
} }
} }
public Task FlushAsync(IHttpOutputProducer outputProducer, CancellationToken cancellationToken) public Task FlushAsync(IHttpOutputAborter outputAborter, CancellationToken cancellationToken)
{ {
lock (_writeLock) lock (_writeLock)
{ {
@ -108,7 +108,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
return Task.CompletedTask; return Task.CompletedTask;
} }
return _flusher.FlushAsync(0, outputProducer, cancellationToken); return _flusher.FlushAsync(outputAborter, cancellationToken);
} }
} }

View File

@ -15,11 +15,11 @@ using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
{ {
public class Http2OutputProducer : IHttpOutputProducer public class Http2OutputProducer : IHttpOutputProducer, IHttpOutputAborter
{ {
private readonly int _streamId; private readonly int _streamId;
private readonly Http2FrameWriter _frameWriter; private readonly Http2FrameWriter _frameWriter;
private readonly StreamSafePipeFlusher _flusher; private readonly TimingPipeFlusher _flusher;
// This should only be accessed via the FrameWriter. The connection-level output flow control is protected by the // This should only be accessed via the FrameWriter. The connection-level output flow control is protected by the
// FrameWriter's connection-level write lock. // FrameWriter's connection-level write lock.
@ -43,7 +43,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
_frameWriter = frameWriter; _frameWriter = frameWriter;
_flowControl = flowControl; _flowControl = flowControl;
_dataPipe = CreateDataPipe(pool); _dataPipe = CreateDataPipe(pool);
_flusher = new StreamSafePipeFlusher(_dataPipe.Writer, timeoutControl); _flusher = new TimingPipeFlusher(_dataPipe.Writer, timeoutControl);
_dataWriteProcessingTask = ProcessDataWrites(); _dataWriteProcessingTask = ProcessDataWrites();
} }
@ -65,7 +65,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
// Complete with an exception to prevent an end of stream data frame from being sent without an // Complete with an exception to prevent an end of stream data frame from being sent without an
// explicit call to WriteStreamSuffixAsync. ConnectionAbortedExceptions are swallowed, so the // explicit call to WriteStreamSuffixAsync. ConnectionAbortedExceptions are swallowed, so the
// message doesn't matter // message doesn't matter
_dataPipe.Writer.Complete(new ConnectionAbortedException()); _dataPipe.Writer.Complete(new OperationCanceledException());
} }
_frameWriter.AbortPendingStreamDataWrites(_flowControl); _frameWriter.AbortPendingStreamDataWrites(_flowControl);
@ -100,7 +100,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
{ {
// If there's already been response data written to the stream, just wait for that. Any header // If there's already been response data written to the stream, just wait for that. Any header
// should be in front of the data frames in the connection pipe. Trailers could change things. // should be in front of the data frames in the connection pipe. Trailers could change things.
return _flusher.FlushAsync(0, this, cancellationToken); return _flusher.FlushAsync(this, cancellationToken);
} }
else else
{ {
@ -158,7 +158,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
_startedWritingDataFrames = true; _startedWritingDataFrames = true;
_dataPipe.Writer.Write(data); _dataPipe.Writer.Write(data);
return _flusher.FlushAsync(data.Length, this, cancellationToken); return _flusher.FlushAsync(this, cancellationToken);
} }
} }
@ -173,9 +173,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
_completed = true; _completed = true;
// Even if there's no actual data, completing the writer gracefully sends an END_STREAM DATA frame.
_startedWritingDataFrames = true;
_dataPipe.Writer.Complete(); _dataPipe.Writer.Complete();
return _dataWriteProcessingTask; return _dataWriteProcessingTask;
} }
@ -208,9 +205,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
_dataPipe.Reader.AdvanceTo(readResult.Buffer.End); _dataPipe.Reader.AdvanceTo(readResult.Buffer.End);
} while (!readResult.IsCompleted); } while (!readResult.IsCompleted);
} }
catch (ConnectionAbortedException) catch (OperationCanceledException)
{ {
// Writes should not throw for aborted connections. // Writes should not throw for aborted streams/connections.
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@ -67,11 +67,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
try try
{ {
_http2Output.Dispose();
RequestBodyPipe.Reader.Complete(); RequestBodyPipe.Reader.Complete();
// The app can no longer read any more of the request body, so return any bytes that weren't read to the // The app can no longer read any more of the request body, so return any bytes that weren't read to the
// connection's flow-control window. // connection's flow-control window.
_inputFlowControl.Abort(); _inputFlowControl.Abort();
Reset();
} }
finally finally
{ {
@ -435,9 +439,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
private void AbortCore(Exception abortReason) private void AbortCore(Exception abortReason)
{ {
// Call OnIOCompleted() which closes the output prior to poisoning the request body stream or pipe to // Call _http2Output.Dispose() prior to poisoning the request body stream or pipe to
// ensure that an app that completes early due to the abort doesn't result in header frames being sent. // ensure that an app that completes early due to the abort doesn't result in header frames being sent.
OnInputOrOutputCompleted(); _http2Output.Dispose();
AbortRequest();
// Unblock the request body. // Unblock the request body.
PoisonRequestBodyStream(abortReason); PoisonRequestBodyStream(abortReason);

View File

@ -1,30 +1,18 @@
// Copyright (c) .NET Foundation. All rights reserved. // Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Buffers;
using System.Net;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.FlowControl; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.FlowControl;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
{ {
public class Http2StreamContext : IHttpProtocolContext public class Http2StreamContext : HttpConnectionContext
{ {
public string ConnectionId { get; set; }
public int StreamId { get; set; } public int StreamId { get; set; }
public ServiceContext ServiceContext { get; set; }
public IFeatureCollection ConnectionFeatures { get; set; }
public MemoryPool<byte> MemoryPool { get; set; }
public IPEndPoint RemoteEndPoint { get; set; }
public IPEndPoint LocalEndPoint { get; set; }
public IHttp2StreamLifetimeHandler StreamLifetimeHandler { get; set; } public IHttp2StreamLifetimeHandler StreamLifetimeHandler { get; set; }
public Http2PeerSettings ClientPeerSettings { get; set; } public Http2PeerSettings ClientPeerSettings { get; set; }
public Http2PeerSettings ServerPeerSettings { get; set; } public Http2PeerSettings ServerPeerSettings { get; set; }
public Http2FrameWriter FrameWriter { get; set; } public Http2FrameWriter FrameWriter { get; set; }
public InputFlowControl ConnectionInputFlowControl { get; set; } public InputFlowControl ConnectionInputFlowControl { get; set; }
public OutputFlowControl ConnectionOutputFlowControl { get; set; } public OutputFlowControl ConnectionOutputFlowControl { get; set; }
public ITimeoutControl TimeoutControl { get; set; }
} }
} }

View File

@ -8,7 +8,6 @@ using System.Diagnostics;
using System.IO; using System.IO;
using System.IO.Pipelines; using System.IO.Pipelines;
using System.Net; using System.Net;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Connections.Features;
@ -24,12 +23,13 @@ using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
{ {
public class HttpConnection : ITimeoutControl, IConnectionTimeoutFeature public class HttpConnection : ITimeoutHandler
{ {
private static readonly ReadOnlyMemory<byte> Http2Id = new[] { (byte)'h', (byte)'2' }; private static readonly ReadOnlyMemory<byte> Http2Id = new[] { (byte)'h', (byte)'2' };
private readonly HttpConnectionContext _context; private readonly HttpConnectionContext _context;
private readonly ISystemClock _systemClock; private readonly ISystemClock _systemClock;
private readonly TimeoutControl _timeoutControl;
private IList<IAdaptedConnection> _adaptedConnections; private IList<IAdaptedConnection> _adaptedConnections;
private IDuplexPipe _adaptedTransport; private IDuplexPipe _adaptedTransport;
@ -39,33 +39,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
private IRequestProcessor _requestProcessor; private IRequestProcessor _requestProcessor;
private Http1Connection _http1Connection; private Http1Connection _http1Connection;
private long _lastTimestamp;
private long _timeoutTimestamp = long.MaxValue;
private TimeoutAction _timeoutAction;
private readonly object _readTimingLock = new object();
private bool _readTimingEnabled;
private bool _readTimingPauseRequested;
private long _readTimingElapsedTicks;
private long _readTimingBytesRead;
private readonly object _writeTimingLock = new object();
private int _writeTimingWrites;
private long _writeTimingTimeoutTimestamp;
public HttpConnection(HttpConnectionContext context) public HttpConnection(HttpConnectionContext context)
{ {
_context = context; _context = context;
_systemClock = _context.ServiceContext.SystemClock; _systemClock = _context.ServiceContext.SystemClock;
_timeoutControl = new TimeoutControl(this);
} }
// For testing // For testing
internal HttpProtocol Http1Connection => _http1Connection; internal HttpProtocol Http1Connection => _http1Connection;
internal IDebugger Debugger { get; set; } = DebuggerWrapper.Singleton; internal IDebugger Debugger { get; set; } = DebuggerWrapper.Singleton;
// For testing
internal bool RequestTimedOut { get; private set; }
public string ConnectionId => _context.ConnectionId; public string ConnectionId => _context.ConnectionId;
public IPEndPoint LocalEndPoint => _context.LocalEndPoint; public IPEndPoint LocalEndPoint => _context.LocalEndPoint;
public IPEndPoint RemoteEndPoint => _context.RemoteEndPoint; public IPEndPoint RemoteEndPoint => _context.RemoteEndPoint;
@ -131,9 +116,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
using (connectionLifetimeNotificationFeature?.ConnectionClosedRequested.Register(state => ((HttpConnection)state).StopProcessingNextRequest(), this)) using (connectionLifetimeNotificationFeature?.ConnectionClosedRequested.Register(state => ((HttpConnection)state).StopProcessingNextRequest(), this))
{ {
_lastTimestamp = _context.ServiceContext.SystemClock.UtcNow.Ticks; // Ensure TimeoutControl._lastTimestamp is intialized before anything that could set timeouts runs.
_timeoutControl.Initialize(_systemClock.UtcNow);
_context.ConnectionFeatures.Set<IConnectionTimeoutFeature>(this); _context.ConnectionFeatures.Set<IConnectionTimeoutFeature>(_timeoutControl);
if (adaptedPipeline != null) if (adaptedPipeline != null)
{ {
@ -149,18 +135,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
// Ensure that the connection hasn't already been stopped. // Ensure that the connection hasn't already been stopped.
if (_protocolSelectionState == ProtocolSelectionState.Initializing) if (_protocolSelectionState == ProtocolSelectionState.Initializing)
{ {
var derivedContext = CreateDerivedContext(_adaptedTransport);
switch (SelectProtocol()) switch (SelectProtocol())
{ {
case HttpProtocols.Http1: case HttpProtocols.Http1:
// _http1Connection must be initialized before adding the connection to the connection manager // _http1Connection must be initialized before adding the connection to the connection manager
requestProcessor = _http1Connection = CreateHttp1Connection(_adaptedTransport); requestProcessor = _http1Connection = new Http1Connection(derivedContext);
_protocolSelectionState = ProtocolSelectionState.Selected; _protocolSelectionState = ProtocolSelectionState.Selected;
break; break;
case HttpProtocols.Http2: case HttpProtocols.Http2:
// _http2Connection must be initialized before yielding control to the transport thread, // _http2Connection must be initialized before yielding control to the transport thread,
// to prevent a race condition where _http2Connection.Abort() is called just as // to prevent a race condition where _http2Connection.Abort() is called just as
// _http2Connection is about to be initialized. // _http2Connection is about to be initialized.
requestProcessor = CreateHttp2Connection(_adaptedTransport); requestProcessor = new Http2Connection(derivedContext);
_protocolSelectionState = ProtocolSelectionState.Selected; _protocolSelectionState = ProtocolSelectionState.Selected;
break; break;
case HttpProtocols.None: case HttpProtocols.None:
@ -210,39 +198,24 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
// For testing only // For testing only
internal void Initialize(IDuplexPipe transport) internal void Initialize(IDuplexPipe transport)
{ {
_requestProcessor = _http1Connection = CreateHttp1Connection(transport); _requestProcessor = _http1Connection = new Http1Connection(CreateDerivedContext(transport));
_protocolSelectionState = ProtocolSelectionState.Selected; _protocolSelectionState = ProtocolSelectionState.Selected;
} }
private Http1Connection CreateHttp1Connection(IDuplexPipe transport) private HttpConnectionContext CreateDerivedContext(IDuplexPipe transport)
{ {
return new Http1Connection(new Http1ConnectionContext return new HttpConnectionContext
{ {
ConnectionId = _context.ConnectionId, ConnectionId = _context.ConnectionId,
ConnectionFeatures = _context.ConnectionFeatures, ConnectionFeatures = _context.ConnectionFeatures,
MemoryPool = MemoryPool, MemoryPool = _context.MemoryPool,
LocalEndPoint = LocalEndPoint, LocalEndPoint = _context.LocalEndPoint,
RemoteEndPoint = RemoteEndPoint, RemoteEndPoint = _context.RemoteEndPoint,
ServiceContext = _context.ServiceContext, ServiceContext = _context.ServiceContext,
ConnectionContext = _context.ConnectionContext, ConnectionContext = _context.ConnectionContext,
TimeoutControl = this, TimeoutControl = _timeoutControl,
Transport = transport Transport = transport
}); };
}
private Http2Connection CreateHttp2Connection(IDuplexPipe transport)
{
return new Http2Connection(new Http2ConnectionContext
{
ConnectionId = _context.ConnectionId,
ConnectionContext = _context.ConnectionContext,
ServiceContext = _context.ServiceContext,
ConnectionFeatures = _context.ConnectionFeatures,
MemoryPool = MemoryPool,
LocalEndPoint = LocalEndPoint,
RemoteEndPoint = RemoteEndPoint,
Transport = transport
});
} }
private void StopProcessingNextRequest() private void StopProcessingNextRequest()
@ -378,11 +351,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
} }
private void Tick() private void Tick()
{
Tick(_systemClock.UtcNow);
}
public void Tick(DateTimeOffset now)
{ {
if (_protocolSelectionState == ProtocolSelectionState.Aborted) if (_protocolSelectionState == ProtocolSelectionState.Aborted)
{ {
@ -391,246 +359,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
return; return;
} }
var timestamp = now.Ticks; _timeoutControl.Tick(_systemClock.UtcNow);
CheckForTimeout(timestamp);
// HTTP/2 rate timeouts are not yet supported.
if (_http1Connection != null)
{
CheckForReadDataRateTimeout(timestamp);
CheckForWriteDataRateTimeout(timestamp);
}
Interlocked.Exchange(ref _lastTimestamp, timestamp);
}
private void CheckForTimeout(long timestamp)
{
// TODO: Use PlatformApis.VolatileRead equivalent again
if (timestamp > Interlocked.Read(ref _timeoutTimestamp))
{
if (!Debugger.IsAttached)
{
CancelTimeout();
switch (_timeoutAction)
{
case TimeoutAction.StopProcessingNextRequest:
// Http/2 keep-alive timeouts are not yet supported.
_http1Connection?.StopProcessingNextRequest();
break;
case TimeoutAction.SendTimeoutResponse:
// HTTP/2 timeout responses are not yet supported.
if (_http1Connection != null)
{
RequestTimedOut = true;
_http1Connection.SendTimeoutResponse();
}
break;
case TimeoutAction.AbortConnection:
// This is actually supported with HTTP/2!
Abort(new ConnectionAbortedException(CoreStrings.ConnectionTimedOutByServer));
break;
}
}
}
}
private void CheckForReadDataRateTimeout(long timestamp)
{
Debug.Assert(_http1Connection != null);
// 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 (Interlocked.Read(ref _timeoutTimestamp) != long.MaxValue)
{
return;
}
lock (_readTimingLock)
{
if (_readTimingEnabled)
{
// Reference in local var to avoid torn reads in case the min rate is changed via IHttpMinRequestBodyDataRateFeature
var minRequestBodyDataRate = _http1Connection.MinRequestBodyDataRate;
_readTimingElapsedTicks += timestamp - _lastTimestamp;
if (minRequestBodyDataRate?.BytesPerSecond > 0 && _readTimingElapsedTicks > minRequestBodyDataRate.GracePeriod.Ticks)
{
var elapsedSeconds = (double)_readTimingElapsedTicks / TimeSpan.TicksPerSecond;
var rate = Interlocked.Read(ref _readTimingBytesRead) / elapsedSeconds;
if (rate < minRequestBodyDataRate.BytesPerSecond && !Debugger.IsAttached)
{
Log.RequestBodyMinimumDataRateNotSatisfied(_context.ConnectionId, _http1Connection.TraceIdentifier, minRequestBodyDataRate.BytesPerSecond);
RequestTimedOut = true;
_http1Connection.SendTimeoutResponse();
}
}
// 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;
}
}
}
}
private void CheckForWriteDataRateTimeout(long timestamp)
{
Debug.Assert(_http1Connection != null);
lock (_writeTimingLock)
{
if (_writeTimingWrites > 0 && timestamp > _writeTimingTimeoutTimestamp && !Debugger.IsAttached)
{
RequestTimedOut = true;
Log.ResponseMinimumDataRateNotSatisfied(_http1Connection.ConnectionIdFeature, _http1Connection.TraceIdentifier);
Abort(new ConnectionAbortedException(CoreStrings.ConnectionTimedBecauseResponseMininumDataRateNotSatisfied));
}
}
}
public void SetTimeout(long ticks, TimeoutAction timeoutAction)
{
Debug.Assert(_timeoutTimestamp == long.MaxValue, "Concurrent timeouts are not supported");
AssignTimeout(ticks, timeoutAction);
}
public void ResetTimeout(long ticks, TimeoutAction timeoutAction)
{
AssignTimeout(ticks, timeoutAction);
}
public void CancelTimeout()
{
Interlocked.Exchange(ref _timeoutTimestamp, long.MaxValue);
}
private void AssignTimeout(long ticks, TimeoutAction timeoutAction)
{
_timeoutAction = timeoutAction;
// Add Heartbeat.Interval since this can be called right before the next heartbeat.
Interlocked.Exchange(ref _timeoutTimestamp, _lastTimestamp + ticks + Heartbeat.Interval.Ticks);
}
public void StartTimingReads()
{
lock (_readTimingLock)
{
_readTimingElapsedTicks = 0;
_readTimingBytesRead = 0;
_readTimingEnabled = true;
}
}
public void StopTimingReads()
{
lock (_readTimingLock)
{
_readTimingEnabled = false;
}
}
public void PauseTimingReads()
{
lock (_readTimingLock)
{
_readTimingPauseRequested = true;
}
}
public void ResumeTimingReads()
{
lock (_readTimingLock)
{
_readTimingEnabled = true;
// In case pause and resume were both called between ticks
_readTimingPauseRequested = false;
}
}
public void BytesRead(long count)
{
Interlocked.Add(ref _readTimingBytesRead, count);
}
public void StartTimingWrite(long size)
{
Debug.Assert(_http1Connection != null);
lock (_writeTimingLock)
{
var minResponseDataRate = _http1Connection.MinResponseDataRate;
if (minResponseDataRate != null)
{
// Add Heartbeat.Interval since this can be called right before the next heartbeat.
var currentTimeUpperBound = _lastTimestamp + Heartbeat.Interval.Ticks;
var ticksToCompleteWriteAtMinRate = TimeSpan.FromSeconds(size / minResponseDataRate.BytesPerSecond).Ticks;
// If ticksToCompleteWriteAtMinRate is less than the configured grace period,
// allow that write to take up to the grace period to complete. Only add the grace period
// to the current time and not to any accumulated timeout.
var singleWriteTimeoutTimestamp = currentTimeUpperBound + Math.Max(
minResponseDataRate.GracePeriod.Ticks,
ticksToCompleteWriteAtMinRate);
// Don't penalize a connection for completing previous writes more quickly than required.
// We don't want to kill a connection when flushing the chunk terminator just because the previous
// chunk was large if the previous chunk was flushed quickly.
// Don't add any grace period to this accumulated timeout because the grace period could
// get accumulated repeatedly making the timeout for a bunch of consecutive small writes
// far too conservative.
var accumulatedWriteTimeoutTimestamp = _writeTimingTimeoutTimestamp + ticksToCompleteWriteAtMinRate;
_writeTimingTimeoutTimestamp = Math.Max(singleWriteTimeoutTimestamp, accumulatedWriteTimeoutTimestamp);
_writeTimingWrites++;
}
}
}
public void StopTimingWrite()
{
lock (_writeTimingLock)
{
_writeTimingWrites--;
}
}
void IConnectionTimeoutFeature.SetTimeout(TimeSpan timeSpan)
{
if (timeSpan < TimeSpan.Zero)
{
throw new ArgumentException(CoreStrings.PositiveFiniteTimeSpanRequired, nameof(timeSpan));
}
if (_timeoutTimestamp != long.MaxValue)
{
throw new InvalidOperationException(CoreStrings.ConcurrentTimeoutsNotSupported);
}
SetTimeout(timeSpan.Ticks, TimeoutAction.AbortConnection);
}
void IConnectionTimeoutFeature.ResetTimeout(TimeSpan timeSpan)
{
if (timeSpan < TimeSpan.Zero)
{
throw new ArgumentException(CoreStrings.PositiveFiniteTimeSpanRequired, nameof(timeSpan));
}
ResetTimeout(timeSpan.Ticks, TimeoutAction.AbortConnection);
} }
private void CloseUninitializedConnection(ConnectionAbortedException abortReason) private void CloseUninitializedConnection(ConnectionAbortedException abortReason)
@ -643,6 +372,36 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
_adaptedTransport.Output.Complete(); _adaptedTransport.Output.Complete();
} }
public void OnTimeout(TimeoutReason reason)
{
// In the cases that don't log directly here, we expect the setter of the timeout to also be the input
// reader, so when the read is canceled or arborted, the reader should write the appropriate log.
switch (reason)
{
case TimeoutReason.KeepAlive:
_http1Connection.StopProcessingNextRequest();
break;
case TimeoutReason.RequestHeaders:
_http1Connection.SendTimeoutResponse();
break;
case TimeoutReason.ReadDataRate:
Log.RequestBodyMinimumDataRateNotSatisfied(_context.ConnectionId, _http1Connection.TraceIdentifier, _http1Connection.MinRequestBodyDataRate.BytesPerSecond);
_http1Connection.SendTimeoutResponse();
break;
case TimeoutReason.WriteDataRate:
Log.ResponseMinimumDataRateNotSatisfied(_http1Connection.ConnectionIdFeature, _http1Connection.TraceIdentifier);
Abort(new ConnectionAbortedException(CoreStrings.ConnectionTimedBecauseResponseMininumDataRateNotSatisfied));
break;
case TimeoutReason.RequestBodyDrain:
case TimeoutReason.TimeoutFeature:
Abort(new ConnectionAbortedException(CoreStrings.ConnectionTimedOutByServer));
break;
default:
Debug.Assert(false, "Invalid TimeoutReason");
break;
}
}
private enum ProtocolSelectionState private enum ProtocolSelectionState
{ {
Initializing, Initializing,

View File

@ -8,6 +8,7 @@ using System.Net;
using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core.Adapter.Internal; using Microsoft.AspNetCore.Server.Kestrel.Core.Adapter.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
{ {
@ -22,6 +23,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
public MemoryPool<byte> MemoryPool { get; set; } public MemoryPool<byte> MemoryPool { get; set; }
public IPEndPoint LocalEndPoint { get; set; } public IPEndPoint LocalEndPoint { get; set; }
public IPEndPoint RemoteEndPoint { get; set; } public IPEndPoint RemoteEndPoint { get; set; }
public ITimeoutControl TimeoutControl { get; set; }
public IDuplexPipe Transport { get; set; } public IDuplexPipe Transport { get; set; }
} }
} }

View File

@ -5,17 +5,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
{ {
public interface ITimeoutControl public interface ITimeoutControl
{ {
void SetTimeout(long ticks, TimeoutAction timeoutAction); void SetTimeout(long ticks, TimeoutReason timeoutReason);
void ResetTimeout(long ticks, TimeoutAction timeoutAction); void ResetTimeout(long ticks, TimeoutReason timeoutReason);
void CancelTimeout(); void CancelTimeout();
void StartTimingReads(); void StartTimingReads(MinDataRate minRate);
void PauseTimingReads(); void PauseTimingReads();
void ResumeTimingReads(); void ResumeTimingReads();
void StopTimingReads(); void StopTimingReads();
void BytesRead(long count); void BytesRead(long count);
void StartTimingWrite(long size); void StartTimingWrite(MinDataRate minRate, long size);
void StopTimingWrite(); void StopTimingWrite();
} }
} }

View File

@ -3,10 +3,8 @@
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
{ {
public enum TimeoutAction public interface ITimeoutHandler
{ {
StopProcessingNextRequest, void OnTimeout(TimeoutReason reason);
SendTimeoutResponse,
AbortConnection,
} }
} }

View File

@ -0,0 +1,15 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
{
public enum TimeoutReason
{
KeepAlive,
RequestHeaders,
ReadDataRate,
WriteDataRate,
RequestBodyDrain,
TimeoutFeature,
}
}

View File

@ -0,0 +1,247 @@
// 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 System.Diagnostics;
using System.Threading;
using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
{
public class TimeoutControl : ITimeoutControl, IConnectionTimeoutFeature
{
private readonly ITimeoutHandler _timeoutHandler;
private long _lastTimestamp;
private long _timeoutTimestamp = long.MaxValue;
private TimeoutReason _timeoutReason;
private readonly object _readTimingLock = new object();
private MinDataRate _minReadRate;
private bool _readTimingEnabled;
private bool _readTimingPauseRequested;
private long _readTimingElapsedTicks;
private long _readTimingBytesRead;
private readonly object _writeTimingLock = new object();
private int _writeTimingWrites;
private long _writeTimingTimeoutTimestamp;
public TimeoutControl(ITimeoutHandler timeoutHandler)
{
_timeoutHandler = timeoutHandler;
}
internal IDebugger Debugger { get; set; } = DebuggerWrapper.Singleton;
public void Initialize(DateTimeOffset now)
{
_lastTimestamp = now.Ticks;
}
public void Tick(DateTimeOffset now)
{
var timestamp = now.Ticks;
CheckForTimeout(timestamp);
CheckForReadDataRateTimeout(timestamp);
CheckForWriteDataRateTimeout(timestamp);
Interlocked.Exchange(ref _lastTimestamp, timestamp);
}
private void CheckForTimeout(long timestamp)
{
if (!Debugger.IsAttached)
{
if (timestamp > Interlocked.Read(ref _timeoutTimestamp))
{
CancelTimeout();
_timeoutHandler.OnTimeout(_timeoutReason);
}
}
}
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 (Interlocked.Read(ref _timeoutTimestamp) != long.MaxValue)
{
return;
}
lock (_readTimingLock)
{
if (!_readTimingEnabled)
{
return;
}
_readTimingElapsedTicks += timestamp - _lastTimestamp;
if (_minReadRate.BytesPerSecond > 0 && _readTimingElapsedTicks > _minReadRate.GracePeriod.Ticks)
{
var elapsedSeconds = (double)_readTimingElapsedTicks / TimeSpan.TicksPerSecond;
var rate = Interlocked.Read(ref _readTimingBytesRead) / elapsedSeconds;
if (rate < _minReadRate.BytesPerSecond && !Debugger.IsAttached)
{
_timeoutHandler.OnTimeout(TimeoutReason.ReadDataRate);
}
}
// 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;
}
}
}
private void CheckForWriteDataRateTimeout(long timestamp)
{
lock (_writeTimingLock)
{
if (_writeTimingWrites > 0 && timestamp > _writeTimingTimeoutTimestamp && !Debugger.IsAttached)
{
_timeoutHandler.OnTimeout(TimeoutReason.WriteDataRate);
}
}
}
public void SetTimeout(long ticks, TimeoutReason timeoutReason)
{
Debug.Assert(_timeoutTimestamp == long.MaxValue, "Concurrent timeouts are not supported");
AssignTimeout(ticks, timeoutReason);
}
public void ResetTimeout(long ticks, TimeoutReason timeoutReason)
{
AssignTimeout(ticks, timeoutReason);
}
public void CancelTimeout()
{
Interlocked.Exchange(ref _timeoutTimestamp, long.MaxValue);
}
private void AssignTimeout(long ticks, TimeoutReason timeoutReason)
{
_timeoutReason = timeoutReason;
// Add Heartbeat.Interval since this can be called right before the next heartbeat.
Interlocked.Exchange(ref _timeoutTimestamp, _lastTimestamp + ticks + Heartbeat.Interval.Ticks);
}
public void StartTimingReads(MinDataRate minRate)
{
lock (_readTimingLock)
{
_minReadRate = minRate;
_readTimingElapsedTicks = 0;
_readTimingBytesRead = 0;
_readTimingEnabled = true;
}
}
public void StopTimingReads()
{
lock (_readTimingLock)
{
_readTimingEnabled = false;
}
}
public void PauseTimingReads()
{
lock (_readTimingLock)
{
_readTimingPauseRequested = true;
}
}
public void ResumeTimingReads()
{
lock (_readTimingLock)
{
_readTimingEnabled = true;
// In case pause and resume were both called between ticks
_readTimingPauseRequested = false;
}
}
public void BytesRead(long count)
{
Interlocked.Add(ref _readTimingBytesRead, count);
}
public void StartTimingWrite(MinDataRate minRate, long size)
{
lock (_writeTimingLock)
{
// Add Heartbeat.Interval since this can be called right before the next heartbeat.
var currentTimeUpperBound = _lastTimestamp + Heartbeat.Interval.Ticks;
var ticksToCompleteWriteAtMinRate = TimeSpan.FromSeconds(size / minRate.BytesPerSecond).Ticks;
// If ticksToCompleteWriteAtMinRate is less than the configured grace period,
// allow that write to take up to the grace period to complete. Only add the grace period
// to the current time and not to any accumulated timeout.
var singleWriteTimeoutTimestamp = currentTimeUpperBound + Math.Max(
minRate.GracePeriod.Ticks,
ticksToCompleteWriteAtMinRate);
// Don't penalize a connection for completing previous writes more quickly than required.
// We don't want to kill a connection when flushing the chunk terminator just because the previous
// chunk was large if the previous chunk was flushed quickly.
// Don't add any grace period to this accumulated timeout because the grace period could
// get accumulated repeatedly making the timeout for a bunch of consecutive small writes
// far too conservative.
var accumulatedWriteTimeoutTimestamp = _writeTimingTimeoutTimestamp + ticksToCompleteWriteAtMinRate;
_writeTimingTimeoutTimestamp = Math.Max(singleWriteTimeoutTimestamp, accumulatedWriteTimeoutTimestamp);
_writeTimingWrites++;
}
}
public void StopTimingWrite()
{
lock (_writeTimingLock)
{
_writeTimingWrites--;
}
}
void IConnectionTimeoutFeature.SetTimeout(TimeSpan timeSpan)
{
if (timeSpan < TimeSpan.Zero)
{
throw new ArgumentException(CoreStrings.PositiveFiniteTimeSpanRequired, nameof(timeSpan));
}
if (_timeoutTimestamp != long.MaxValue)
{
throw new InvalidOperationException(CoreStrings.ConcurrentTimeoutsNotSupported);
}
SetTimeout(timeSpan.Ticks, TimeoutReason.TimeoutFeature);
}
void IConnectionTimeoutFeature.ResetTimeout(TimeSpan timeSpan)
{
if (timeSpan < TimeSpan.Zero)
{
throw new ArgumentException(CoreStrings.PositiveFiniteTimeSpanRequired, nameof(timeSpan));
}
ResetTimeout(timeSpan.Ticks, TimeoutReason.TimeoutFeature);
}
}
}

View File

@ -0,0 +1,24 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
{
public static class TimeoutControlExtensions
{
public static void StartDrainTimeout(this ITimeoutControl timeoutControl, MinDataRate minDataRate, long? maxResponseBufferSize)
{
// If maxResponseBufferSize has no value, there's no backpressure and we can't reasonably timeout draining.
if (minDataRate == null || maxResponseBufferSize == null)
{
return;
}
// With full backpressure and a connection adapter there could be 2 two pipes buffering.
// We already validate that the buffer size is positive.
// There's no reason to stop timing the write after the connection is closed.
var oneBufferSize = maxResponseBufferSize.Value;
var maxBufferedBytes = oneBufferSize < long.MaxValue / 2 ? oneBufferSize * 2 : long.MaxValue;
timeoutControl.StartTimingWrite(minDataRate, maxBufferedBytes);
}
}
}

View File

@ -12,9 +12,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
{ {
/// <summary> /// <summary>
/// This wraps PipeWriter.FlushAsync() in a way that allows multiple awaiters making it safe to call from publicly /// This wraps PipeWriter.FlushAsync() in a way that allows multiple awaiters making it safe to call from publicly
/// exposed Stream implementations. /// exposed Stream implementations while also tracking response data rate.
/// </summary> /// </summary>
public class StreamSafePipeFlusher public class TimingPipeFlusher
{ {
private readonly PipeWriter _writer; private readonly PipeWriter _writer;
private readonly ITimeoutControl _timeoutControl; private readonly ITimeoutControl _timeoutControl;
@ -22,7 +22,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
private Task _lastFlushTask = Task.CompletedTask; private Task _lastFlushTask = Task.CompletedTask;
public StreamSafePipeFlusher( public TimingPipeFlusher(
PipeWriter writer, PipeWriter writer,
ITimeoutControl timeoutControl) ITimeoutControl timeoutControl)
{ {
@ -30,7 +30,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
_timeoutControl = timeoutControl; _timeoutControl = timeoutControl;
} }
public Task FlushAsync(long count = 0, IHttpOutputProducer outputProducer = null, CancellationToken cancellationToken = default) public Task FlushAsync()
{
return FlushAsync(outputAborter: null, cancellationToken: default);
}
public Task FlushAsync(IHttpOutputAborter outputAborter, CancellationToken cancellationToken)
{
return FlushAsync(minRate: null, count: 0, outputAborter: outputAborter, cancellationToken: cancellationToken);
}
public Task FlushAsync(MinDataRate minRate, long count, IHttpOutputAborter outputAborter, CancellationToken cancellationToken)
{ {
var flushValueTask = _writer.FlushAsync(cancellationToken); var flushValueTask = _writer.FlushAsync(cancellationToken);
@ -51,13 +61,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
_lastFlushTask = flushValueTask.AsTask(); _lastFlushTask = flushValueTask.AsTask();
} }
return TimeFlushAsync(count, outputProducer, cancellationToken); return TimeFlushAsync(minRate, count, outputAborter, cancellationToken);
} }
} }
private async Task TimeFlushAsync(long count, IHttpOutputProducer outputProducer, CancellationToken cancellationToken) private async Task TimeFlushAsync(MinDataRate minRate, long count, IHttpOutputAborter outputAborter, CancellationToken cancellationToken)
{ {
_timeoutControl.StartTimingWrite(count); if (minRate != null)
{
_timeoutControl.StartTimingWrite(minRate, count);
}
try try
{ {
@ -65,14 +78,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
} }
catch (OperationCanceledException ex) catch (OperationCanceledException ex)
{ {
outputProducer.Abort(new ConnectionAbortedException(CoreStrings.ConnectionOrStreamAbortedByCancellationToken, ex)); outputAborter.Abort(new ConnectionAbortedException(CoreStrings.ConnectionOrStreamAbortedByCancellationToken, ex));
} }
catch catch
{ {
// A canceled token is the only reason flush should ever throw. // A canceled token is the only reason flush should ever throw.
} }
_timeoutControl.StopTimingWrite(); if (minRate != null)
{
_timeoutControl.StopTimingWrite();
}
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
} }

View File

@ -2184,6 +2184,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
internal static string FormatHPackErrorIntegerTooBig() internal static string FormatHPackErrorIntegerTooBig()
=> GetString("HPackErrorIntegerTooBig"); => GetString("HPackErrorIntegerTooBig");
/// <summary>
/// The client closed the connection.
/// </summary>
internal static string ConnectionAbortedByClient
{
get => GetString("ConnectionAbortedByClient");
}
/// <summary>
/// The client closed the connection.
/// </summary>
internal static string FormatConnectionAbortedByClient()
=> GetString("ConnectionAbortedByClient");
private static string GetString(string name, params string[] formatterNames) private static string GetString(string name, params string[] formatterNames)
{ {
var value = _resourceManager.GetString(name); var value = _resourceManager.GetString(name);

View File

@ -1,13 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Text;
namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal
{
public interface IBytesWrittenFeature
{
long TotalBytesWritten { get; }
}
}

View File

@ -21,8 +21,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal
ITransportSchedulerFeature, ITransportSchedulerFeature,
IConnectionLifetimeFeature, IConnectionLifetimeFeature,
IConnectionHeartbeatFeature, IConnectionHeartbeatFeature,
IConnectionLifetimeNotificationFeature, IConnectionLifetimeNotificationFeature
IBytesWrittenFeature
{ {
// NOTE: When feature interfaces are added to or removed from this TransportConnection class implementation, // NOTE: When feature interfaces are added to or removed from this TransportConnection class implementation,
// then the list of `features` in the generated code project MUST also be updated. // then the list of `features` in the generated code project MUST also be updated.
@ -101,7 +100,5 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal
{ {
OnHeartbeat(action, state); OnHeartbeat(action, state);
} }
long IBytesWrittenFeature.TotalBytesWritten => TotalBytesWritten;
} }
} }

View File

@ -22,7 +22,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal
private static readonly Type IConnectionLifetimeFeatureType = typeof(IConnectionLifetimeFeature); private static readonly Type IConnectionLifetimeFeatureType = typeof(IConnectionLifetimeFeature);
private static readonly Type IConnectionHeartbeatFeatureType = typeof(IConnectionHeartbeatFeature); private static readonly Type IConnectionHeartbeatFeatureType = typeof(IConnectionHeartbeatFeature);
private static readonly Type IConnectionLifetimeNotificationFeatureType = typeof(IConnectionLifetimeNotificationFeature); private static readonly Type IConnectionLifetimeNotificationFeatureType = typeof(IConnectionLifetimeNotificationFeature);
private static readonly Type IBytesWrittenFeatureType = typeof(IBytesWrittenFeature);
private object _currentIHttpConnectionFeature; private object _currentIHttpConnectionFeature;
private object _currentIConnectionIdFeature; private object _currentIConnectionIdFeature;
@ -34,7 +33,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal
private object _currentIConnectionLifetimeFeature; private object _currentIConnectionLifetimeFeature;
private object _currentIConnectionHeartbeatFeature; private object _currentIConnectionHeartbeatFeature;
private object _currentIConnectionLifetimeNotificationFeature; private object _currentIConnectionLifetimeNotificationFeature;
private object _currentIBytesWrittenFeature;
private int _featureRevision; private int _featureRevision;
@ -52,7 +50,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal
_currentIConnectionLifetimeFeature = this; _currentIConnectionLifetimeFeature = this;
_currentIConnectionHeartbeatFeature = this; _currentIConnectionHeartbeatFeature = this;
_currentIConnectionLifetimeNotificationFeature = this; _currentIConnectionLifetimeNotificationFeature = this;
_currentIBytesWrittenFeature = this;
} }
@ -148,10 +145,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal
{ {
feature = _currentIConnectionLifetimeNotificationFeature; feature = _currentIConnectionLifetimeNotificationFeature;
} }
else if (key == IBytesWrittenFeatureType)
{
feature = _currentIBytesWrittenFeature;
}
else if (MaybeExtra != null) else if (MaybeExtra != null)
{ {
feature = ExtraFeatureGet(key); feature = ExtraFeatureGet(key);
@ -204,10 +197,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal
{ {
_currentIConnectionLifetimeNotificationFeature = value; _currentIConnectionLifetimeNotificationFeature = value;
} }
else if (key == IBytesWrittenFeatureType)
{
_currentIBytesWrittenFeature = value;
}
else else
{ {
ExtraFeatureSet(key, value); ExtraFeatureSet(key, value);
@ -258,10 +247,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal
{ {
feature = (TFeature)_currentIConnectionLifetimeNotificationFeature; feature = (TFeature)_currentIConnectionLifetimeNotificationFeature;
} }
else if (typeof(TFeature) == typeof(IBytesWrittenFeature))
{
feature = (TFeature)_currentIBytesWrittenFeature;
}
else if (MaybeExtra != null) else if (MaybeExtra != null)
{ {
feature = (TFeature)(ExtraFeatureGet(typeof(TFeature))); feature = (TFeature)(ExtraFeatureGet(typeof(TFeature)));
@ -313,10 +298,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal
{ {
_currentIConnectionLifetimeNotificationFeature = feature; _currentIConnectionLifetimeNotificationFeature = feature;
} }
else if (typeof(TFeature) == typeof(IBytesWrittenFeature))
{
_currentIBytesWrittenFeature = feature;
}
else else
{ {
ExtraFeatureSet(typeof(TFeature), feature); ExtraFeatureSet(typeof(TFeature), feature);
@ -365,10 +346,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal
{ {
yield return new KeyValuePair<Type, object>(IConnectionLifetimeNotificationFeatureType, _currentIConnectionLifetimeNotificationFeature); yield return new KeyValuePair<Type, object>(IConnectionLifetimeNotificationFeatureType, _currentIConnectionLifetimeNotificationFeature);
} }
if (_currentIBytesWrittenFeature != null)
{
yield return new KeyValuePair<Type, object>(IBytesWrittenFeatureType, _currentIBytesWrittenFeature);
}
if (MaybeExtra != null) if (MaybeExtra != null)
{ {

View File

@ -38,7 +38,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal
public virtual MemoryPool<byte> MemoryPool { get; } public virtual MemoryPool<byte> MemoryPool { get; }
public virtual PipeScheduler InputWriterScheduler { get; } public virtual PipeScheduler InputWriterScheduler { get; }
public virtual PipeScheduler OutputReaderScheduler { get; } public virtual PipeScheduler OutputReaderScheduler { get; }
public virtual long TotalBytesWritten { get; }
public override IDuplexPipe Transport { get; set; } public override IDuplexPipe Transport { get; set; }
public IDuplexPipe Application { get; set; } public IDuplexPipe Application { get; set; }

View File

@ -54,8 +54,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal
public override PipeScheduler InputWriterScheduler => Thread; public override PipeScheduler InputWriterScheduler => Thread;
public override PipeScheduler OutputReaderScheduler => Thread; public override PipeScheduler OutputReaderScheduler => Thread;
public override long TotalBytesWritten => OutputConsumer?.TotalBytesWritten ?? 0;
public async Task Start() public async Task Start()
{ {
try try
@ -98,7 +96,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal
finally finally
{ {
// Now, complete the input so that no more reads can happen // Now, complete the input so that no more reads can happen
Input.Complete(inputError ?? _abortReason ?? new ConnectionAbortedException()); Input.Complete(inputError ?? _abortReason ?? new ConnectionAbortedException("The libuv transport's send loop completed gracefully."));
Output.Complete(outputError); Output.Complete(outputError);
// Make sure it isn't possible for a paused read to resume reading after calling uv_close // Make sure it isn't possible for a paused read to resume reading after calling uv_close

View File

@ -17,8 +17,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal
private readonly ILibuvTrace _log; private readonly ILibuvTrace _log;
private readonly PipeReader _pipe; private readonly PipeReader _pipe;
private long _totalBytesWritten;
public LibuvOutputConsumer( public LibuvOutputConsumer(
PipeReader pipe, PipeReader pipe,
LibuvThread thread, LibuvThread thread,
@ -33,8 +31,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal
_log = log; _log = log;
} }
public long TotalBytesWritten => Interlocked.Read(ref _totalBytesWritten);
public async Task WriteOutputAsync() public async Task WriteOutputAsync()
{ {
var pool = _thread.WriteReqPool; var pool = _thread.WriteReqPool;
@ -66,10 +62,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal
var writeResult = await writeReq.WriteAsync(_socket, buffer); var writeResult = await writeReq.WriteAsync(_socket, buffer);
// This is not interlocked because there could be a concurrent writer.
// Instead it's to prevent read tearing on 32-bit systems.
Interlocked.Add(ref _totalBytesWritten, buffer.Length);
LogWriteInfo(writeResult.Status, writeResult.Error); LogWriteInfo(writeResult.Status, writeResult.Error);
if (writeResult.Error != null) if (writeResult.Error != null)

View File

@ -32,7 +32,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal
private readonly object _shutdownLock = new object(); private readonly object _shutdownLock = new object();
private volatile bool _socketDisposed; private volatile bool _socketDisposed;
private volatile Exception _shutdownReason; private volatile Exception _shutdownReason;
private long _totalBytesWritten;
internal SocketConnection(Socket socket, MemoryPool<byte> memoryPool, PipeScheduler scheduler, ISocketsTrace trace) internal SocketConnection(Socket socket, MemoryPool<byte> memoryPool, PipeScheduler scheduler, ISocketsTrace trace)
{ {
@ -68,7 +67,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal
public override MemoryPool<byte> MemoryPool { get; } public override MemoryPool<byte> MemoryPool { get; }
public override PipeScheduler InputWriterScheduler => _scheduler; public override PipeScheduler InputWriterScheduler => _scheduler;
public override PipeScheduler OutputReaderScheduler => _scheduler; public override PipeScheduler OutputReaderScheduler => _scheduler;
public override long TotalBytesWritten => Interlocked.Read(ref _totalBytesWritten);
public async Task StartAsync() public async Task StartAsync()
{ {
@ -264,10 +262,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal
await _sender.SendAsync(buffer); await _sender.SendAsync(buffer);
} }
// This is not interlocked because there could be a concurrent writer.
// Instead it's to prevent read tearing on 32-bit systems.
Interlocked.Add(ref _totalBytesWritten, buffer.Length);
Output.AdvanceTo(end); Output.AdvanceTo(end);
if (isCompleted) if (isCompleted)
@ -294,7 +288,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal
// shutdownReason should only be null if the output was completed gracefully, so no one should ever // shutdownReason should only be null if the output was completed gracefully, so no one should ever
// ever observe the nondescript ConnectionAbortedException except for connection middleware attempting // ever observe the nondescript ConnectionAbortedException except for connection middleware attempting
// to half close the connection which is currently unsupported. // to half close the connection which is currently unsupported.
_shutdownReason = shutdownReason ?? new ConnectionAbortedException(); _shutdownReason = shutdownReason ?? new ConnectionAbortedException("The Socket transport's send loop completed gracefully.");
_trace.ConnectionWriteFin(ConnectionId); _trace.ConnectionWriteFin(ConnectionId);

View File

@ -35,7 +35,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
private readonly IDuplexPipe _application; private readonly IDuplexPipe _application;
private readonly TestHttp1Connection _http1Connection; private readonly TestHttp1Connection _http1Connection;
private readonly ServiceContext _serviceContext; private readonly ServiceContext _serviceContext;
private readonly Http1ConnectionContext _http1ConnectionContext; private readonly HttpConnectionContext _http1ConnectionContext;
private readonly MemoryPool<byte> _pipelineFactory; private readonly MemoryPool<byte> _pipelineFactory;
private SequencePosition _consumed; private SequencePosition _consumed;
private SequencePosition _examined; private SequencePosition _examined;
@ -52,11 +52,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
var connectionFeatures = new FeatureCollection(); var connectionFeatures = new FeatureCollection();
connectionFeatures.Set(Mock.Of<IConnectionLifetimeFeature>()); connectionFeatures.Set(Mock.Of<IConnectionLifetimeFeature>());
connectionFeatures.Set(Mock.Of<IBytesWrittenFeature>());
_serviceContext = new TestServiceContext(); _serviceContext = new TestServiceContext();
_timeoutControl = new Mock<ITimeoutControl>(); _timeoutControl = new Mock<ITimeoutControl>();
_http1ConnectionContext = new Http1ConnectionContext _http1ConnectionContext = new HttpConnectionContext
{ {
ServiceContext = _serviceContext, ServiceContext = _serviceContext,
ConnectionContext = Mock.Of<ConnectionContext>(), ConnectionContext = Mock.Of<ConnectionContext>(),
@ -425,7 +424,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
_transport.Input.AdvanceTo(_consumed, _examined); _transport.Input.AdvanceTo(_consumed, _examined);
var expectedRequestHeadersTimeout = _serviceContext.ServerOptions.Limits.RequestHeadersTimeout.Ticks; var expectedRequestHeadersTimeout = _serviceContext.ServerOptions.Limits.RequestHeadersTimeout.Ticks;
_timeoutControl.Verify(cc => cc.ResetTimeout(expectedRequestHeadersTimeout, TimeoutAction.SendTimeoutResponse)); _timeoutControl.Verify(cc => cc.ResetTimeout(expectedRequestHeadersTimeout, TimeoutReason.RequestHeaders));
} }
[Fact] [Fact]
@ -542,7 +541,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
var requestProcessingTask = _http1Connection.ProcessRequestsAsync<object>(null); var requestProcessingTask = _http1Connection.ProcessRequestsAsync<object>(null);
var expectedKeepAliveTimeout = _serviceContext.ServerOptions.Limits.KeepAliveTimeout.Ticks; var expectedKeepAliveTimeout = _serviceContext.ServerOptions.Limits.KeepAliveTimeout.Ticks;
_timeoutControl.Verify(cc => cc.SetTimeout(expectedKeepAliveTimeout, TimeoutAction.StopProcessingNextRequest)); _timeoutControl.Verify(cc => cc.SetTimeout(expectedKeepAliveTimeout, TimeoutReason.KeepAlive));
_http1Connection.StopProcessingNextRequest(); _http1Connection.StopProcessingNextRequest();
_application.Output.Complete(); _application.Output.Complete();

View File

@ -1,592 +1,49 @@
// Copyright (c) .NET Foundation. All rights reserved. // Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Buffers;
using System.Collections.Generic;
using System.IO.Pipelines; using System.IO.Pipelines;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Connections.Features;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core.Adapter.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal;
using Microsoft.AspNetCore.Testing; using Microsoft.AspNetCore.Testing;
using Moq; using Moq;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{ {
public class HttpConnectionTests : IDisposable public class HttpConnectionTests
{ {
private readonly MemoryPool<byte> _memoryPool; [Fact]
private readonly HttpConnectionContext _httpConnectionContext; public async Task WriteDataRateTimeoutAbortsConnection()
private readonly HttpConnection _httpConnection;
public HttpConnectionTests()
{ {
_memoryPool = KestrelMemoryPool.Create(); var mockConnectionContext = new Mock<ConnectionContext>();
var options = new PipeOptions(_memoryPool, readerScheduler: PipeScheduler.Inline, writerScheduler: PipeScheduler.Inline, useSynchronizationContext: false);
var pair = DuplexPipe.CreateConnectionPair(options, options);
var connectionFeatures = new FeatureCollection(); var httpConnectionContext = new HttpConnectionContext
connectionFeatures.Set(Mock.Of<IConnectionLifetimeFeature>());
connectionFeatures.Set(Mock.Of<IBytesWrittenFeature>());
_httpConnectionContext = new HttpConnectionContext
{ {
ConnectionId = "0123456789", ConnectionContext = mockConnectionContext.Object,
ConnectionContext = Mock.Of<ConnectionContext>(), Transport = new DuplexPipe(Mock.Of<PipeReader>(), Mock.Of<PipeWriter>()),
ConnectionAdapters = new List<IConnectionAdapter>(), ServiceContext = new TestServiceContext()
ConnectionFeatures = connectionFeatures,
MemoryPool = _memoryPool,
Transport = pair.Transport,
ServiceContext = new TestServiceContext
{
SystemClock = new SystemClock()
}
}; };
_httpConnection = new HttpConnection(_httpConnectionContext); var httpConnection = new HttpConnection(httpConnectionContext);
}
public void Dispose()
{
_memoryPool.Dispose();
}
[Fact]
public void DoesNotTimeOutWhenDebuggerIsAttached()
{
var mockDebugger = new Mock<IDebugger>();
mockDebugger.SetupGet(g => g.IsAttached).Returns(true);
_httpConnection.Debugger = mockDebugger.Object;
_httpConnection.Initialize(_httpConnectionContext.Transport);
var now = DateTimeOffset.Now;
_httpConnection.Tick(now);
_httpConnection.SetTimeout(1, TimeoutAction.SendTimeoutResponse);
_httpConnection.Tick(now.AddTicks(2).Add(Heartbeat.Interval));
Assert.False(_httpConnection.RequestTimedOut);
}
[Fact]
public void DoesNotTimeOutWhenRequestBodyDoesNotSatisfyMinimumDataRateButDebuggerIsAttached()
{
var mockDebugger = new Mock<IDebugger>();
mockDebugger.SetupGet(g => g.IsAttached).Returns(true);
_httpConnection.Debugger = mockDebugger.Object;
var bytesPerSecond = 100;
var mockLogger = new Mock<IKestrelTrace>();
mockLogger.Setup(l => l.RequestBodyMinimumDataRateNotSatisfied(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<double>())).Throws(new InvalidOperationException("Should not log"));
TickBodyWithMinimumDataRate(mockLogger.Object, bytesPerSecond);
Assert.False(_httpConnection.RequestTimedOut);
}
[Fact]
public void TimesOutWhenRequestBodyDoesNotSatisfyMinimumDataRate()
{
var bytesPerSecond = 100;
var mockLogger = new Mock<IKestrelTrace>();
TickBodyWithMinimumDataRate(mockLogger.Object, bytesPerSecond);
// Timed out
Assert.True(_httpConnection.RequestTimedOut);
mockLogger.Verify(logger =>
logger.RequestBodyMinimumDataRateNotSatisfied(It.IsAny<string>(), It.IsAny<string>(), bytesPerSecond), Times.Once);
}
private void TickBodyWithMinimumDataRate(IKestrelTrace logger, int bytesPerSecond)
{
var gracePeriod = TimeSpan.FromSeconds(5);
_httpConnectionContext.ServiceContext.ServerOptions.Limits.MinRequestBodyDataRate =
new MinDataRate(bytesPerSecond: bytesPerSecond, gracePeriod: gracePeriod);
_httpConnectionContext.ServiceContext.Log = logger;
_httpConnection.Initialize(_httpConnectionContext.Transport);
_httpConnection.Http1Connection.Reset();
// Initialize timestamp
var now = DateTimeOffset.UtcNow;
_httpConnection.Tick(now);
_httpConnection.StartTimingReads();
// Tick after grace period w/ low data rate
now += gracePeriod + TimeSpan.FromSeconds(1);
_httpConnection.BytesRead(1);
_httpConnection.Tick(now);
}
[Fact]
public void RequestBodyMinimumDataRateNotEnforcedDuringGracePeriod()
{
var bytesPerSecond = 100;
var gracePeriod = TimeSpan.FromSeconds(2);
_httpConnectionContext.ServiceContext.ServerOptions.Limits.MinRequestBodyDataRate =
new MinDataRate(bytesPerSecond: bytesPerSecond, gracePeriod: gracePeriod);
var mockLogger = new Mock<IKestrelTrace>();
_httpConnectionContext.ServiceContext.Log = mockLogger.Object;
_httpConnection.Initialize(_httpConnectionContext.Transport);
_httpConnection.Http1Connection.Reset();
// Initialize timestamp
var now = DateTimeOffset.UtcNow;
_httpConnection.Tick(now);
_httpConnection.StartTimingReads();
// Tick during grace period w/ low data rate
now += TimeSpan.FromSeconds(1);
_httpConnection.BytesRead(10);
_httpConnection.Tick(now);
// Not timed out
Assert.False(_httpConnection.RequestTimedOut);
mockLogger.Verify(logger =>
logger.RequestBodyMinimumDataRateNotSatisfied(It.IsAny<string>(), It.IsAny<string>(), bytesPerSecond), Times.Never);
// Tick after grace period w/ low data rate
now += TimeSpan.FromSeconds(2);
_httpConnection.BytesRead(10);
_httpConnection.Tick(now);
// Timed out
Assert.True(_httpConnection.RequestTimedOut);
mockLogger.Verify(logger =>
logger.RequestBodyMinimumDataRateNotSatisfied(It.IsAny<string>(), It.IsAny<string>(), bytesPerSecond), Times.Once);
}
[Fact]
public void RequestBodyDataRateIsAveragedOverTimeSpentReadingRequestBody()
{
var bytesPerSecond = 100;
var gracePeriod = TimeSpan.FromSeconds(2);
_httpConnectionContext.ServiceContext.ServerOptions.Limits.MinRequestBodyDataRate =
new MinDataRate(bytesPerSecond: bytesPerSecond, gracePeriod: gracePeriod);
var mockLogger = new Mock<IKestrelTrace>();
_httpConnectionContext.ServiceContext.Log = mockLogger.Object;
_httpConnection.Initialize(_httpConnectionContext.Transport);
_httpConnection.Http1Connection.Reset();
// Initialize timestamp
var now = DateTimeOffset.UtcNow;
_httpConnection.Tick(now);
_httpConnection.StartTimingReads();
// Set base data rate to 200 bytes/second
now += gracePeriod;
_httpConnection.BytesRead(400);
_httpConnection.Tick(now);
// Data rate: 200 bytes/second
now += TimeSpan.FromSeconds(1);
_httpConnection.BytesRead(200);
_httpConnection.Tick(now);
// Not timed out
Assert.False(_httpConnection.RequestTimedOut);
mockLogger.Verify(logger =>
logger.RequestBodyMinimumDataRateNotSatisfied(It.IsAny<string>(), It.IsAny<string>(), bytesPerSecond), Times.Never);
// Data rate: 150 bytes/second
now += TimeSpan.FromSeconds(1);
_httpConnection.BytesRead(0);
_httpConnection.Tick(now);
// Not timed out
Assert.False(_httpConnection.RequestTimedOut);
mockLogger.Verify(logger =>
logger.RequestBodyMinimumDataRateNotSatisfied(It.IsAny<string>(), It.IsAny<string>(), bytesPerSecond), Times.Never);
// Data rate: 120 bytes/second
now += TimeSpan.FromSeconds(1);
_httpConnection.BytesRead(0);
_httpConnection.Tick(now);
// Not timed out
Assert.False(_httpConnection.RequestTimedOut);
mockLogger.Verify(logger =>
logger.RequestBodyMinimumDataRateNotSatisfied(It.IsAny<string>(), It.IsAny<string>(), bytesPerSecond), Times.Never);
// Data rate: 100 bytes/second
now += TimeSpan.FromSeconds(1);
_httpConnection.BytesRead(0);
_httpConnection.Tick(now);
// Not timed out
Assert.False(_httpConnection.RequestTimedOut);
mockLogger.Verify(logger =>
logger.RequestBodyMinimumDataRateNotSatisfied(It.IsAny<string>(), It.IsAny<string>(), bytesPerSecond), Times.Never);
// Data rate: ~85 bytes/second
now += TimeSpan.FromSeconds(1);
_httpConnection.BytesRead(0);
_httpConnection.Tick(now);
// Timed out
Assert.True(_httpConnection.RequestTimedOut);
mockLogger.Verify(logger =>
logger.RequestBodyMinimumDataRateNotSatisfied(It.IsAny<string>(), It.IsAny<string>(), bytesPerSecond), Times.Once);
}
[Fact]
public void RequestBodyDataRateNotComputedOnPausedTime()
{
var systemClock = new MockSystemClock();
_httpConnectionContext.ServiceContext.ServerOptions.Limits.MinRequestBodyDataRate =
new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(2));
_httpConnectionContext.ServiceContext.SystemClock = systemClock;
var mockLogger = new Mock<IKestrelTrace>();
_httpConnectionContext.ServiceContext.Log = mockLogger.Object;
_httpConnection.Initialize(_httpConnectionContext.Transport);
_httpConnection.Http1Connection.Reset();
// Initialize timestamp
_httpConnection.Tick(systemClock.UtcNow);
_httpConnection.StartTimingReads();
// Tick at 3s, expected counted time is 3s, expected data rate is 200 bytes/second
systemClock.UtcNow += TimeSpan.FromSeconds(3);
_httpConnection.BytesRead(600);
_httpConnection.Tick(systemClock.UtcNow);
// Pause at 3.5s
systemClock.UtcNow += TimeSpan.FromSeconds(0.5);
_httpConnection.PauseTimingReads();
// Tick at 4s, expected counted time is 4s (first tick after pause goes through), expected data rate is 150 bytes/second
systemClock.UtcNow += TimeSpan.FromSeconds(0.5);
_httpConnection.Tick(systemClock.UtcNow);
// Tick at 6s, expected counted time is 4s, expected data rate is 150 bytes/second
systemClock.UtcNow += TimeSpan.FromSeconds(2);
_httpConnection.Tick(systemClock.UtcNow);
// Not timed out
Assert.False(_httpConnection.RequestTimedOut);
mockLogger.Verify(
logger => logger.RequestBodyMinimumDataRateNotSatisfied(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<double>()),
Times.Never);
// Resume at 6.5s
systemClock.UtcNow += TimeSpan.FromSeconds(0.5);
_httpConnection.ResumeTimingReads();
// Tick at 9s, expected counted time is 6s, expected data rate is 100 bytes/second
systemClock.UtcNow += TimeSpan.FromSeconds(1.5);
_httpConnection.Tick(systemClock.UtcNow);
// Not timed out
Assert.False(_httpConnection.RequestTimedOut);
mockLogger.Verify(
logger => logger.RequestBodyMinimumDataRateNotSatisfied(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<double>()),
Times.Never);
// Tick at 10s, expected counted time is 7s, expected data rate drops below 100 bytes/second
systemClock.UtcNow += TimeSpan.FromSeconds(1);
_httpConnection.Tick(systemClock.UtcNow);
// Timed out
Assert.True(_httpConnection.RequestTimedOut);
mockLogger.Verify(
logger => logger.RequestBodyMinimumDataRateNotSatisfied(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<double>()),
Times.Once);
}
[Fact]
public void ReadTimingNotPausedWhenResumeCalledBeforeNextTick()
{
var systemClock = new MockSystemClock();
_httpConnectionContext.ServiceContext.ServerOptions.Limits.MinRequestBodyDataRate =
new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(2));
_httpConnectionContext.ServiceContext.SystemClock = systemClock;
var mockLogger = new Mock<IKestrelTrace>();
_httpConnectionContext.ServiceContext.Log = mockLogger.Object;
_httpConnection.Initialize(_httpConnectionContext.Transport);
_httpConnection.Http1Connection.Reset();
// Initialize timestamp
_httpConnection.Tick(systemClock.UtcNow);
_httpConnection.StartTimingReads();
// Tick at 2s, expected counted time is 2s, expected data rate is 100 bytes/second
systemClock.UtcNow += TimeSpan.FromSeconds(2);
_httpConnection.BytesRead(200);
_httpConnection.Tick(systemClock.UtcNow);
// Not timed out
Assert.False(_httpConnection.RequestTimedOut);
mockLogger.Verify(
logger => logger.RequestBodyMinimumDataRateNotSatisfied(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<double>()),
Times.Never);
// Pause at 2.25s
systemClock.UtcNow += TimeSpan.FromSeconds(0.25);
_httpConnection.PauseTimingReads();
// Resume at 2.5s
systemClock.UtcNow += TimeSpan.FromSeconds(0.25);
_httpConnection.ResumeTimingReads();
// Tick at 3s, expected counted time is 3s, expected data rate is 100 bytes/second
systemClock.UtcNow += TimeSpan.FromSeconds(0.5);
_httpConnection.BytesRead(100);
_httpConnection.Tick(systemClock.UtcNow);
// Not timed out
Assert.False(_httpConnection.RequestTimedOut);
mockLogger.Verify(
logger => logger.RequestBodyMinimumDataRateNotSatisfied(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<double>()),
Times.Never);
// Tick at 4s, expected counted time is 4s, expected data rate drops below 100 bytes/second
systemClock.UtcNow += TimeSpan.FromSeconds(1);
_httpConnection.Tick(systemClock.UtcNow);
// Timed out
Assert.True(_httpConnection.RequestTimedOut);
mockLogger.Verify(
logger => logger.RequestBodyMinimumDataRateNotSatisfied(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<double>()),
Times.Once);
}
[Fact]
public void ReadTimingNotEnforcedWhenTimeoutIsSet()
{
var systemClock = new MockSystemClock();
var timeout = TimeSpan.FromSeconds(5);
_httpConnectionContext.ServiceContext.ServerOptions.Limits.MinRequestBodyDataRate =
new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(2));
_httpConnectionContext.ServiceContext.SystemClock = systemClock;
var mockLogger = new Mock<IKestrelTrace>();
_httpConnectionContext.ServiceContext.Log = mockLogger.Object;
_httpConnection.Initialize(_httpConnectionContext.Transport);
_httpConnection.Http1Connection.Reset();
var startTime = systemClock.UtcNow;
// Initialize timestamp
_httpConnection.Tick(startTime);
_httpConnection.StartTimingReads();
_httpConnection.SetTimeout(timeout.Ticks, TimeoutAction.StopProcessingNextRequest);
// Tick beyond grace period with low data rate
systemClock.UtcNow += TimeSpan.FromSeconds(3);
_httpConnection.BytesRead(1);
_httpConnection.Tick(systemClock.UtcNow);
// Not timed out
Assert.False(_httpConnection.RequestTimedOut);
// Tick just past timeout period, adjusted by Heartbeat.Interval
systemClock.UtcNow = startTime + timeout + Heartbeat.Interval + TimeSpan.FromTicks(1);
_httpConnection.Tick(systemClock.UtcNow);
// Timed out
Assert.True(_httpConnection.RequestTimedOut);
}
[Fact]
public async Task WriteTimingAbortsConnectionWhenWriteDoesNotCompleteWithMinimumDataRate()
{
var systemClock = new MockSystemClock();
var aborted = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously); var aborted = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
_httpConnectionContext.ServiceContext.ServerOptions.Limits.MinResponseDataRate = httpConnection.Initialize(httpConnectionContext.Transport);
new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(2)); httpConnection.Http1Connection.Reset();
_httpConnectionContext.ServiceContext.SystemClock = systemClock; httpConnection.Http1Connection.RequestAborted.Register(() =>
var mockLogger = new Mock<IKestrelTrace>();
_httpConnectionContext.ServiceContext.Log = mockLogger.Object;
_httpConnection.Initialize(_httpConnectionContext.Transport);
_httpConnection.Http1Connection.Reset();
_httpConnection.Http1Connection.RequestAborted.Register(() =>
{ {
aborted.SetResult(null); aborted.SetResult(null);
}); });
// Initialize timestamp httpConnection.OnTimeout(TimeoutReason.WriteDataRate);
_httpConnection.Tick(systemClock.UtcNow);
// Should complete within 4 seconds, but the timeout is adjusted by adding Heartbeat.Interval mockConnectionContext
_httpConnection.StartTimingWrite(400); .Verify(c => c.Abort(It.Is<ConnectionAbortedException>(ex => ex.Message == CoreStrings.ConnectionTimedBecauseResponseMininumDataRateNotSatisfied)),
Times.Once);
// Tick just past 4s plus Heartbeat.Interval
systemClock.UtcNow += TimeSpan.FromSeconds(4) + Heartbeat.Interval + TimeSpan.FromTicks(1);
_httpConnection.Tick(systemClock.UtcNow);
Assert.True(_httpConnection.RequestTimedOut);
await aborted.Task.DefaultTimeout(); await aborted.Task.DefaultTimeout();
} }
[Fact]
public async Task WriteTimingAbortsConnectionWhenSmallWriteDoesNotCompleteWithinGracePeriod()
{
var systemClock = new MockSystemClock();
var minResponseDataRate = new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(5));
var aborted = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
_httpConnectionContext.ServiceContext.ServerOptions.Limits.MinResponseDataRate = minResponseDataRate;
_httpConnectionContext.ServiceContext.SystemClock = systemClock;
var mockLogger = new Mock<IKestrelTrace>();
_httpConnectionContext.ServiceContext.Log = mockLogger.Object;
_httpConnection.Initialize(_httpConnectionContext.Transport);
_httpConnection.Http1Connection.Reset();
_httpConnection.Http1Connection.RequestAborted.Register(() =>
{
aborted.SetResult(null);
});
// Initialize timestamp
var startTime = systemClock.UtcNow;
_httpConnection.Tick(startTime);
// Should complete within 1 second, but the timeout is adjusted by adding Heartbeat.Interval
_httpConnection.StartTimingWrite(100);
// Tick just past 1s plus Heartbeat.Interval
systemClock.UtcNow += TimeSpan.FromSeconds(1) + Heartbeat.Interval + TimeSpan.FromTicks(1);
_httpConnection.Tick(systemClock.UtcNow);
// Still within grace period, not timed out
Assert.False(_httpConnection.RequestTimedOut);
// Tick just past grace period (adjusted by Heartbeat.Interval)
systemClock.UtcNow = startTime + minResponseDataRate.GracePeriod + Heartbeat.Interval + TimeSpan.FromTicks(1);
_httpConnection.Tick(systemClock.UtcNow);
Assert.True(_httpConnection.RequestTimedOut);
await aborted.Task.DefaultTimeout();
}
[Fact]
public async Task WriteTimingTimeoutPushedOnConcurrentWrite()
{
var systemClock = new MockSystemClock();
var aborted = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
_httpConnectionContext.ServiceContext.ServerOptions.Limits.MinResponseDataRate =
new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(2));
_httpConnectionContext.ServiceContext.SystemClock = systemClock;
var mockLogger = new Mock<IKestrelTrace>();
_httpConnectionContext.ServiceContext.Log = mockLogger.Object;
_httpConnection.Initialize(_httpConnectionContext.Transport);
_httpConnection.Http1Connection.Reset();
_httpConnection.Http1Connection.RequestAborted.Register(() =>
{
aborted.SetResult(null);
});
// Initialize timestamp
_httpConnection.Tick(systemClock.UtcNow);
// Should complete within 5 seconds, but the timeout is adjusted by adding Heartbeat.Interval
_httpConnection.StartTimingWrite(500);
// Start a concurrent write after 3 seconds, which should complete within 3 seconds (adjusted by Heartbeat.Interval)
_httpConnection.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);
_httpConnection.Tick(systemClock.UtcNow);
// Not timed out because the timeout was pushed by the second write
Assert.False(_httpConnection.RequestTimedOut);
// Complete the first write, this should have no effect on the timeout
_httpConnection.StopTimingWrite();
// Tick just past +3s, when the second write should have completed
systemClock.UtcNow += TimeSpan.FromSeconds(3) + TimeSpan.FromTicks(1);
_httpConnection.Tick(systemClock.UtcNow);
Assert.True(_httpConnection.RequestTimedOut);
await aborted.Task.DefaultTimeout();
}
[Fact]
public async Task WriteTimingAbortsConnectionWhenRepeatedSmallWritesDoNotCompleteWithMinimumDataRate()
{
var systemClock = new MockSystemClock();
var minResponseDataRate = new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(5));
var numWrites = 5;
var writeSize = 100;
var aborted = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
_httpConnectionContext.ServiceContext.ServerOptions.Limits.MinResponseDataRate = minResponseDataRate;
_httpConnectionContext.ServiceContext.SystemClock = systemClock;
var mockLogger = new Mock<IKestrelTrace>();
_httpConnectionContext.ServiceContext.Log = mockLogger.Object;
_httpConnection.Initialize(_httpConnectionContext.Transport);
_httpConnection.Http1Connection.Reset();
_httpConnection.Http1Connection.RequestAborted.Register(() =>
{
aborted.SetResult(null);
});
// Initialize timestamp
var startTime = systemClock.UtcNow;
_httpConnection.Tick(startTime);
// 5 consecutive 100 byte writes.
for (var i = 0; i < numWrites - 1; i++)
{
_httpConnection.StartTimingWrite(writeSize);
_httpConnection.StopTimingWrite();
}
// Stall the last write.
_httpConnection.StartTimingWrite(writeSize);
// Move the clock forward Heartbeat.Interval + MinDataRate.GracePeriod + 4 seconds.
// The grace period should only be added for the first write. The subsequent 4 100 byte writes should add 1 second each to the timeout given the 100 byte/s min rate.
systemClock.UtcNow += Heartbeat.Interval + minResponseDataRate.GracePeriod + TimeSpan.FromSeconds((numWrites - 1) * writeSize / minResponseDataRate.BytesPerSecond);
_httpConnection.Tick(systemClock.UtcNow);
Assert.False(_httpConnection.RequestTimedOut);
// On more tick forward triggers the timeout.
systemClock.UtcNow += TimeSpan.FromTicks(1);
_httpConnection.Tick(systemClock.UtcNow);
Assert.True(_httpConnection.RequestTimedOut);
await aborted.Task.TimeoutAfter(TimeSpan.FromSeconds(10));
}
} }
} }

View File

@ -23,9 +23,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
private readonly IDuplexPipe _application; private readonly IDuplexPipe _application;
private readonly TestHttp1Connection _http1Connection; private readonly TestHttp1Connection _http1Connection;
private readonly ServiceContext _serviceContext; private readonly ServiceContext _serviceContext;
private readonly Http1ConnectionContext _http1ConnectionContext; private readonly HttpConnectionContext _http1ConnectionContext;
private readonly MemoryPool<byte> _memoryPool; private readonly MemoryPool<byte> _memoryPool;
private Mock<ITimeoutControl> _timeoutControl;
private readonly IFeatureCollection _collection; private readonly IFeatureCollection _collection;
@ -39,13 +38,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
_application = pair.Application; _application = pair.Application;
_serviceContext = new TestServiceContext(); _serviceContext = new TestServiceContext();
_timeoutControl = new Mock<ITimeoutControl>(); _http1ConnectionContext = new HttpConnectionContext
_http1ConnectionContext = new Http1ConnectionContext
{ {
ServiceContext = _serviceContext, ServiceContext = _serviceContext,
ConnectionFeatures = new FeatureCollection(), ConnectionFeatures = new FeatureCollection(),
MemoryPool = _memoryPool, MemoryPool = _memoryPool,
TimeoutControl = _timeoutControl.Object, TimeoutControl = Mock.Of<ITimeoutControl>(),
Transport = pair.Transport Transport = pair.Transport
}; };

View File

@ -2,12 +2,12 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Buffers;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO.Pipelines; using System.IO.Pipelines;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal;
using Microsoft.AspNetCore.Testing; using Microsoft.AspNetCore.Testing;
@ -25,7 +25,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{ {
var options = new PipeOptions(memoryPool, readerScheduler: PipeScheduler.Inline, writerScheduler: PipeScheduler.Inline, useSynchronizationContext: false); var options = new PipeOptions(memoryPool, readerScheduler: PipeScheduler.Inline, writerScheduler: PipeScheduler.Inline, useSynchronizationContext: false);
var pair = DuplexPipe.CreateConnectionPair(options, options); var pair = DuplexPipe.CreateConnectionPair(options, options);
var http1ConnectionContext = new Http1ConnectionContext var http1ConnectionContext = new HttpConnectionContext
{ {
ServiceContext = new TestServiceContext(), ServiceContext = new TestServiceContext(),
ConnectionFeatures = new FeatureCollection(), ConnectionFeatures = new FeatureCollection(),

View File

@ -10,6 +10,7 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal;
@ -422,7 +423,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
var options = new PipeOptions(pool: memoryPool, readerScheduler: PipeScheduler.Inline, writerScheduler: PipeScheduler.Inline, useSynchronizationContext: false); var options = new PipeOptions(pool: memoryPool, readerScheduler: PipeScheduler.Inline, writerScheduler: PipeScheduler.Inline, useSynchronizationContext: false);
var pair = DuplexPipe.CreateConnectionPair(options, options); var pair = DuplexPipe.CreateConnectionPair(options, options);
var transport = pair.Transport; var transport = pair.Transport;
var http1ConnectionContext = new Http1ConnectionContext var http1ConnectionContext = new HttpConnectionContext
{ {
ServiceContext = new TestServiceContext(), ServiceContext = new TestServiceContext(),
ConnectionFeatures = new FeatureCollection(), ConnectionFeatures = new FeatureCollection(),
@ -747,9 +748,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
.Callback(() => produceContinueCalled = true); .Callback(() => produceContinueCalled = true);
input.Http1Connection.HttpResponseControl = mockHttpResponseControl.Object; input.Http1Connection.HttpResponseControl = mockHttpResponseControl.Object;
var minReadRate = input.Http1Connection.MinRequestBodyDataRate;
var mockTimeoutControl = new Mock<ITimeoutControl>(); var mockTimeoutControl = new Mock<ITimeoutControl>();
mockTimeoutControl mockTimeoutControl
.Setup(timeoutControl => timeoutControl.StartTimingReads()) .Setup(timeoutControl => timeoutControl.StartTimingReads(minReadRate))
.Callback(() => startTimingReadsCalledAfterProduceContinue = produceContinueCalled); .Callback(() => startTimingReadsCalledAfterProduceContinue = produceContinueCalled);
input.Http1ConnectionContext.TimeoutControl = mockTimeoutControl.Object; input.Http1ConnectionContext.TimeoutControl = mockTimeoutControl.Object;
@ -773,6 +775,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{ {
using (var input = new TestInput()) using (var input = new TestInput())
{ {
var minReadRate = input.Http1Connection.MinRequestBodyDataRate;
var mockTimeoutControl = new Mock<ITimeoutControl>(); var mockTimeoutControl = new Mock<ITimeoutControl>();
input.Http1ConnectionContext.TimeoutControl = mockTimeoutControl.Object; input.Http1ConnectionContext.TimeoutControl = mockTimeoutControl.Object;
@ -786,7 +789,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
Assert.Equal(0, await body.ReadAsync(new ArraySegment<byte>(new byte[1]))); Assert.Equal(0, await body.ReadAsync(new ArraySegment<byte>(new byte[1])));
mockTimeoutControl.Verify(timeoutControl => timeoutControl.StartTimingReads(), Times.Never); mockTimeoutControl.Verify(timeoutControl => timeoutControl.StartTimingReads(minReadRate), Times.Never);
mockTimeoutControl.Verify(timeoutControl => timeoutControl.StopTimingReads(), Times.Never); mockTimeoutControl.Verify(timeoutControl => timeoutControl.StopTimingReads(), Times.Never);
// Due to the limits set on HttpProtocol.RequestBodyPipe, backpressure should be triggered on every // Due to the limits set on HttpProtocol.RequestBodyPipe, backpressure should be triggered on every

View File

@ -4,10 +4,9 @@
using System; using System;
using System.Buffers; using System.Buffers;
using System.IO.Pipelines; using System.IO.Pipelines;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal;
@ -96,7 +95,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
connectionContext, connectionContext,
serviceContext.Log, serviceContext.Log,
Mock.Of<ITimeoutControl>(), Mock.Of<ITimeoutControl>(),
Mock.Of<IBytesWrittenFeature>()); Mock.Of<IHttpMinResponseDataRateFeature>());
return socketOutput; return socketOutput;
} }

View File

@ -9,6 +9,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Connections.Features;
using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal;
@ -31,9 +32,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
var connectionFeatures = new FeatureCollection(); var connectionFeatures = new FeatureCollection();
connectionFeatures.Set(Mock.Of<IConnectionLifetimeFeature>()); connectionFeatures.Set(Mock.Of<IConnectionLifetimeFeature>());
connectionFeatures.Set(Mock.Of<IBytesWrittenFeature>());
Http1ConnectionContext = new Http1ConnectionContext Http1ConnectionContext = new HttpConnectionContext
{ {
ServiceContext = new TestServiceContext(), ServiceContext = new TestServiceContext(),
ConnectionContext = Mock.Of<ConnectionContext>(), ConnectionContext = Mock.Of<ConnectionContext>(),
@ -45,13 +45,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
Http1Connection = new Http1Connection(Http1ConnectionContext); Http1Connection = new Http1Connection(Http1ConnectionContext);
Http1Connection.HttpResponseControl = Mock.Of<IHttpResponseControl>(); Http1Connection.HttpResponseControl = Mock.Of<IHttpResponseControl>();
Http1Connection.Reset();
} }
public IDuplexPipe Transport { get; } public IDuplexPipe Transport { get; }
public IDuplexPipe Application { get; } public IDuplexPipe Application { get; }
public Http1ConnectionContext Http1ConnectionContext { get; } public HttpConnectionContext Http1ConnectionContext { get; }
public Http1Connection Http1Connection { get; set; } public Http1Connection Http1Connection { get; set; }

View File

@ -0,0 +1,405 @@
// 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.Internal.Infrastructure;
using Microsoft.AspNetCore.Testing;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
public class TimeoutControlTests
{
private readonly Mock<ITimeoutHandler> _mockTimeoutHandler;
private readonly TimeoutControl _timeoutControl;
public TimeoutControlTests()
{
_mockTimeoutHandler = new Mock<ITimeoutHandler>();
_timeoutControl = new TimeoutControl(_mockTimeoutHandler.Object);
}
[Fact]
public void DoesNotTimeOutWhenDebuggerIsAttached()
{
var mockDebugger = new Mock<IDebugger>();
mockDebugger.SetupGet(g => g.IsAttached).Returns(true);
_timeoutControl.Debugger = mockDebugger.Object;
var now = DateTimeOffset.Now;
_timeoutControl.Tick(now);
_timeoutControl.SetTimeout(1, TimeoutReason.RequestHeaders);
_timeoutControl.Tick(now.AddTicks(2).Add(Heartbeat.Interval));
_mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny<TimeoutReason>()), Times.Never);
}
[Fact]
public void DoesNotTimeOutWhenRequestBodyDoesNotSatisfyMinimumDataRateButDebuggerIsAttached()
{
var mockDebugger = new Mock<IDebugger>();
mockDebugger.SetupGet(g => g.IsAttached).Returns(true);
_timeoutControl.Debugger = mockDebugger.Object;
TickBodyWithMinimumDataRate(bytesPerSecond: 100);
_mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny<TimeoutReason>()), Times.Never);
}
[Fact]
public void TimesOutWhenRequestBodyDoesNotSatisfyMinimumDataRate()
{
TickBodyWithMinimumDataRate(bytesPerSecond: 100);
// Timed out
_mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny<TimeoutReason>()), Times.Once);
}
[Fact]
public void RequestBodyMinimumDataRateNotEnforcedDuringGracePeriod()
{
var minRate = new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(2));
// Initialize timestamp
var now = DateTimeOffset.UtcNow;
_timeoutControl.Tick(now);
_timeoutControl.StartTimingReads(minRate);
// Tick during grace period w/ low data rate
now += TimeSpan.FromSeconds(1);
_timeoutControl.BytesRead(10);
_timeoutControl.Tick(now);
// Not timed out
_mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny<TimeoutReason>()), Times.Never);
// Tick after grace period w/ low data rate
now += TimeSpan.FromSeconds(2);
_timeoutControl.BytesRead(10);
_timeoutControl.Tick(now);
// Timed out
_mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.ReadDataRate), Times.Once);
}
[Fact]
public void RequestBodyDataRateIsAveragedOverTimeSpentReadingRequestBody()
{
var gracePeriod = TimeSpan.FromSeconds(2);
var minRate = new MinDataRate(bytesPerSecond: 100, gracePeriod: gracePeriod);
// Initialize timestamp
var now = DateTimeOffset.UtcNow;
_timeoutControl.Tick(now);
_timeoutControl.StartTimingReads(minRate);
// Set base data rate to 200 bytes/second
now += gracePeriod;
_timeoutControl.BytesRead(400);
_timeoutControl.Tick(now);
// Data rate: 200 bytes/second
now += TimeSpan.FromSeconds(1);
_timeoutControl.BytesRead(200);
_timeoutControl.Tick(now);
// Not timed out
_mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny<TimeoutReason>()), Times.Never);
// Data rate: 150 bytes/second
now += TimeSpan.FromSeconds(1);
_timeoutControl.BytesRead(0);
_timeoutControl.Tick(now);
// Not timed out
_mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny<TimeoutReason>()), Times.Never);
// Data rate: 120 bytes/second
now += TimeSpan.FromSeconds(1);
_timeoutControl.BytesRead(0);
_timeoutControl.Tick(now);
// Not timed out
_mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny<TimeoutReason>()), Times.Never);
// Data rate: 100 bytes/second
now += TimeSpan.FromSeconds(1);
_timeoutControl.BytesRead(0);
_timeoutControl.Tick(now);
// Not timed out
_mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny<TimeoutReason>()), Times.Never);
// Data rate: ~85 bytes/second
now += TimeSpan.FromSeconds(1);
_timeoutControl.BytesRead(0);
_timeoutControl.Tick(now);
// Timed out
_mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.ReadDataRate), Times.Once);
}
[Fact]
public void RequestBodyDataRateNotComputedOnPausedTime()
{
var systemClock = new MockSystemClock();
var minRate = new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(2));
// Initialize timestamp
_timeoutControl.Tick(systemClock.UtcNow);
_timeoutControl.StartTimingReads(minRate);
// Tick at 3s, expected counted time is 3s, expected data rate is 200 bytes/second
systemClock.UtcNow += TimeSpan.FromSeconds(3);
_timeoutControl.BytesRead(600);
_timeoutControl.Tick(systemClock.UtcNow);
// Pause at 3.5s
systemClock.UtcNow += TimeSpan.FromSeconds(0.5);
_timeoutControl.PauseTimingReads();
// Tick at 4s, expected counted time is 4s (first tick after pause goes through), expected data rate is 150 bytes/second
systemClock.UtcNow += TimeSpan.FromSeconds(0.5);
_timeoutControl.Tick(systemClock.UtcNow);
// Tick at 6s, expected counted time is 4s, expected data rate is 150 bytes/second
systemClock.UtcNow += TimeSpan.FromSeconds(2);
_timeoutControl.Tick(systemClock.UtcNow);
// Not timed out
_mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny<TimeoutReason>()), Times.Never);
// Resume at 6.5s
systemClock.UtcNow += TimeSpan.FromSeconds(0.5);
_timeoutControl.ResumeTimingReads();
// Tick at 9s, expected counted time is 6s, expected data rate is 100 bytes/second
systemClock.UtcNow += TimeSpan.FromSeconds(1.5);
_timeoutControl.Tick(systemClock.UtcNow);
// Not timed out
_mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny<TimeoutReason>()), Times.Never);
// Tick at 10s, expected counted time is 7s, expected data rate drops below 100 bytes/second
systemClock.UtcNow += TimeSpan.FromSeconds(1);
_timeoutControl.Tick(systemClock.UtcNow);
// Timed out
_mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.ReadDataRate), Times.Once);
}
[Fact]
public void ReadTimingNotPausedWhenResumeCalledBeforeNextTick()
{
var systemClock = new MockSystemClock();
var minRate = new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(2));
// Initialize timestamp
_timeoutControl.Tick(systemClock.UtcNow);
_timeoutControl.StartTimingReads(minRate);
// Tick at 2s, expected counted time is 2s, expected data rate is 100 bytes/second
systemClock.UtcNow += TimeSpan.FromSeconds(2);
_timeoutControl.BytesRead(200);
_timeoutControl.Tick(systemClock.UtcNow);
// Not timed out
_mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny<TimeoutReason>()), Times.Never);
// Pause at 2.25s
systemClock.UtcNow += TimeSpan.FromSeconds(0.25);
_timeoutControl.PauseTimingReads();
// Resume at 2.5s
systemClock.UtcNow += TimeSpan.FromSeconds(0.25);
_timeoutControl.ResumeTimingReads();
// Tick at 3s, expected counted time is 3s, expected data rate is 100 bytes/second
systemClock.UtcNow += TimeSpan.FromSeconds(0.5);
_timeoutControl.BytesRead(100);
_timeoutControl.Tick(systemClock.UtcNow);
// Not timed out
_mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny<TimeoutReason>()), Times.Never);
// Tick at 4s, expected counted time is 4s, expected data rate drops below 100 bytes/second
systemClock.UtcNow += TimeSpan.FromSeconds(1);
_timeoutControl.Tick(systemClock.UtcNow);
// Timed out
_mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.ReadDataRate), Times.Once);
}
[Fact]
public void ReadTimingNotEnforcedWhenTimeoutIsSet()
{
var systemClock = new MockSystemClock();
var timeout = TimeSpan.FromSeconds(5);
var minRate = new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(2));
var startTime = systemClock.UtcNow;
// Initialize timestamp
_timeoutControl.Tick(startTime);
_timeoutControl.StartTimingReads(minRate);
_timeoutControl.SetTimeout(timeout.Ticks, TimeoutReason.RequestBodyDrain);
// Tick beyond grace period with low data rate
systemClock.UtcNow += TimeSpan.FromSeconds(3);
_timeoutControl.BytesRead(1);
_timeoutControl.Tick(systemClock.UtcNow);
// Not timed out
_mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny<TimeoutReason>()), Times.Never);
// Tick just past timeout period, adjusted by Heartbeat.Interval
systemClock.UtcNow = startTime + timeout + Heartbeat.Interval + TimeSpan.FromTicks(1);
_timeoutControl.Tick(systemClock.UtcNow);
// Timed out
_mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.RequestBodyDrain), Times.Once);
}
[Fact]
public void WriteTimingAbortsConnectionWhenWriteDoesNotCompleteWithMinimumDataRate()
{
var systemClock = new MockSystemClock();
var minRate = new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(2));
// Initialize timestamp
_timeoutControl.Tick(systemClock.UtcNow);
// Should complete within 4 seconds, but the timeout is adjusted by adding Heartbeat.Interval
_timeoutControl.StartTimingWrite(minRate, 400);
// Tick just past 4s plus Heartbeat.Interval
systemClock.UtcNow += TimeSpan.FromSeconds(4) + Heartbeat.Interval + TimeSpan.FromTicks(1);
_timeoutControl.Tick(systemClock.UtcNow);
_mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.WriteDataRate), Times.Once);
}
[Fact]
public void WriteTimingAbortsConnectionWhenSmallWriteDoesNotCompleteWithinGracePeriod()
{
var systemClock = new MockSystemClock();
var minRate = new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(5));
// Initialize timestamp
var startTime = systemClock.UtcNow;
_timeoutControl.Tick(startTime);
// Should complete within 1 second, but the timeout is adjusted by adding Heartbeat.Interval
_timeoutControl.StartTimingWrite(minRate, 100);
// Tick just past 1s plus Heartbeat.Interval
systemClock.UtcNow += TimeSpan.FromSeconds(1) + Heartbeat.Interval + TimeSpan.FromTicks(1);
_timeoutControl.Tick(systemClock.UtcNow);
// Still within grace period, not timed out
_mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny<TimeoutReason>()), Times.Never);
// Tick just past grace period (adjusted by Heartbeat.Interval)
systemClock.UtcNow = startTime + minRate.GracePeriod + Heartbeat.Interval + TimeSpan.FromTicks(1);
_timeoutControl.Tick(systemClock.UtcNow);
_mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.WriteDataRate), Times.Once);
}
[Fact]
public void WriteTimingTimeoutPushedOnConcurrentWrite()
{
var systemClock = new MockSystemClock();
var minRate = new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(2));
// Initialize timestamp
_timeoutControl.Tick(systemClock.UtcNow);
// Should complete within 5 seconds, but the timeout is adjusted by adding Heartbeat.Interval
_timeoutControl.StartTimingWrite(minRate, 500);
// Start a concurrent write after 3 seconds, which should complete within 3 seconds (adjusted by Heartbeat.Interval)
_timeoutControl.StartTimingWrite(minRate, 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);
_timeoutControl.Tick(systemClock.UtcNow);
// Not timed out because the timeout was pushed by the second write
_mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny<TimeoutReason>()), Times.Never);
// Complete the first write, this should have no effect on the timeout
_timeoutControl.StopTimingWrite();
// Tick just past +3s, when the second write should have completed
systemClock.UtcNow += TimeSpan.FromSeconds(3) + TimeSpan.FromTicks(1);
_timeoutControl.Tick(systemClock.UtcNow);
_mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.WriteDataRate), Times.Once);
}
[Fact]
public void WriteTimingAbortsConnectionWhenRepeatedSmallWritesDoNotCompleteWithMinimumDataRate()
{
var systemClock = new MockSystemClock();
var minRate = new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(5));
var numWrites = 5;
var writeSize = 100;
// Initialize timestamp
var startTime = systemClock.UtcNow;
_timeoutControl.Tick(startTime);
// 5 consecutive 100 byte writes.
for (var i = 0; i < numWrites - 1; i++)
{
_timeoutControl.StartTimingWrite(minRate, writeSize);
_timeoutControl.StopTimingWrite();
}
// Stall the last write.
_timeoutControl.StartTimingWrite(minRate, writeSize);
// Move the clock forward Heartbeat.Interval + MinDataRate.GracePeriod + 4 seconds.
// The grace period should only be added for the first write. The subsequent 4 100 byte writes should add 1 second each to the timeout given the 100 byte/s min rate.
systemClock.UtcNow += Heartbeat.Interval + minRate.GracePeriod + TimeSpan.FromSeconds((numWrites - 1) * writeSize / minRate.BytesPerSecond);
_timeoutControl.Tick(systemClock.UtcNow);
_mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny<TimeoutReason>()), Times.Never);
// On more tick forward triggers the timeout.
systemClock.UtcNow += TimeSpan.FromTicks(1);
_timeoutControl.Tick(systemClock.UtcNow);
_mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.WriteDataRate), Times.Once);
}
private void TickBodyWithMinimumDataRate(int bytesPerSecond)
{
var gracePeriod = TimeSpan.FromSeconds(5);
var minRate = new MinDataRate(bytesPerSecond, gracePeriod);
// Initialize timestamp
var now = DateTimeOffset.UtcNow;
_timeoutControl.Tick(now);
_timeoutControl.StartTimingReads(minRate);
// Tick after grace period w/ low data rate
now += gracePeriod + TimeSpan.FromSeconds(1);
_timeoutControl.BytesRead(1);
_timeoutControl.Tick(now);
}
}
}

View File

@ -16,7 +16,7 @@ using Xunit;
namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
{ {
public class ConnectionAdapterTests : LoggedTest public class ConnectionAdapterTests : TestApplicationErrorLoggerLoggedTest
{ {
[Fact] [Fact]
public async Task CanReadAndWriteWithRewritingConnectionAdapter() public async Task CanReadAndWriteWithRewritingConnectionAdapter()
@ -164,6 +164,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
await connection.WaitForConnectionClose(); await connection.WaitForConnectionClose();
} }
} }
Assert.Contains(TestApplicationErrorLogger.Messages, m => m.Message.Contains($"Uncaught exception from the {nameof(IConnectionAdapter.OnConnectionAsync)} method of an {nameof(IConnectionAdapter)}."));
} }
[Fact] [Fact]
@ -220,7 +222,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
public async Task<IAdaptedConnection> OnConnectionAsync(ConnectionAdapterContext context) public async Task<IAdaptedConnection> OnConnectionAsync(ConnectionAdapterContext context)
{ {
await Task.Delay(100); await Task.Yield();
return new AdaptedConnection(new RewritingStream(context.ConnectionStream)); return new AdaptedConnection(new RewritingStream(context.ConnectionStream));
} }
} }

View File

@ -13,9 +13,11 @@ using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Server.Kestrel.Core.Features; using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.AspNetCore.Testing; using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers; using Microsoft.Net.Http.Headers;
using Moq;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
@ -2874,6 +2876,34 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
expectedErrorMessage: CoreStrings.FormatHttp2ErrorHeadersInterleaved(Http2FrameType.GOAWAY, streamId: 0, headersStreamId: 1)); expectedErrorMessage: CoreStrings.FormatHttp2ErrorHeadersInterleaved(Http2FrameType.GOAWAY, streamId: 0, headersStreamId: 1));
} }
[Fact]
public async Task GOAWAY_Received_ConnectionClosedWhenResponseNotDrainedAtMinimumDataRate()
{
var mockSystemClock = new MockSystemClock();
var limits = _connectionContext.ServiceContext.ServerOptions.Limits;
var timeoutControl = _connectionContext.TimeoutControl;
_timeoutControl.Initialize(mockSystemClock.UtcNow);
await InitializeConnectionAsync(_noopApplication);
await SendGoAwayAsync();
await WaitForConnectionStopAsync(expectedLastStreamId: 0, ignoreNonGoAwayFrames: false);
mockSystemClock.UtcNow +=
Heartbeat.Interval +
TimeSpan.FromSeconds(limits.MaxResponseBufferSize.Value * 2 / limits.MinResponseDataRate.BytesPerSecond);
_timeoutControl.Tick(mockSystemClock.UtcNow);
_mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny<TimeoutReason>()), Times.Never);
mockSystemClock.UtcNow += TimeSpan.FromTicks(1);
_timeoutControl.Tick(mockSystemClock.UtcNow);
_mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.WriteDataRate), Times.Once);
}
[Fact] [Fact]
public async Task WINDOW_UPDATE_Received_StreamIdEven_ConnectionError() public async Task WINDOW_UPDATE_Received_StreamIdEven_ConnectionError()
{ {
@ -3482,20 +3512,21 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
_pair.Application.Output.Complete(new ConnectionResetException(string.Empty)); _pair.Application.Output.Complete(new ConnectionResetException(string.Empty));
var result = await _pair.Application.Input.ReadAsync(); await WaitForConnectionStopAsync(expectedLastStreamId: 0, ignoreNonGoAwayFrames: false);
Assert.True(result.IsCompleted);
Assert.DoesNotContain(TestApplicationErrorLogger.Messages, m => m.Exception is ConnectionResetException); Assert.DoesNotContain(TestApplicationErrorLogger.Messages, m => m.Exception is ConnectionResetException);
} }
[Fact] [Fact]
public async Task OnInputOrOutputCompletedSendsFinalGOAWAY() public async Task OnInputOrOutputCompletedCompletesOutput()
{ {
await InitializeConnectionAsync(_noopApplication); await InitializeConnectionAsync(_noopApplication);
_connection.OnInputOrOutputCompleted(); _connection.OnInputOrOutputCompleted();
await _closedStateReached.Task.DefaultTimeout(); await _closedStateReached.Task.DefaultTimeout();
VerifyGoAway(await ReceiveFrameAsync(), 0, Http2ErrorCode.NO_ERROR); var result = await _pair.Application.Input.ReadAsync().AsTask().DefaultTimeout();
Assert.True(result.IsCompleted);
Assert.True(result.Buffer.IsEmpty);
} }
[Fact] [Fact]
@ -3655,7 +3686,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StartStreamAsync(3, _browserRequestHeaders, endStream: false); await StartStreamAsync(3, _browserRequestHeaders, endStream: false);
await WaitForConnectionStopAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); var result = await _pair.Application.Input.ReadAsync().AsTask().DefaultTimeout();
Assert.True(result.IsCompleted);
Assert.True(result.Buffer.IsEmpty);
} }
[Fact] [Fact]

View File

@ -16,6 +16,7 @@ using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core.Features; using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack;
@ -55,6 +56,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
protected readonly HPackDecoder _hpackDecoder; protected readonly HPackDecoder _hpackDecoder;
private readonly byte[] _headerEncodingBuffer = new byte[Http2PeerSettings.MinAllowedMaxFrameSize]; private readonly byte[] _headerEncodingBuffer = new byte[Http2PeerSettings.MinAllowedMaxFrameSize];
protected readonly Mock<ITimeoutHandler> _mockTimeoutHandler = new Mock<ITimeoutHandler>();
protected readonly TimeoutControl _timeoutControl;
protected readonly ConcurrentDictionary<int, TaskCompletionSource<object>> _runningStreams = new ConcurrentDictionary<int, TaskCompletionSource<object>>(); protected readonly ConcurrentDictionary<int, TaskCompletionSource<object>> _runningStreams = new ConcurrentDictionary<int, TaskCompletionSource<object>>();
protected readonly Dictionary<string, string> _receivedHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); protected readonly Dictionary<string, string> _receivedHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
protected readonly Dictionary<string, string> _decodedHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); protected readonly Dictionary<string, string> _decodedHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
@ -77,7 +81,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
protected readonly RequestDelegate _echoHost; protected readonly RequestDelegate _echoHost;
protected readonly RequestDelegate _echoPath; protected readonly RequestDelegate _echoPath;
protected Http2ConnectionContext _connectionContext; protected HttpConnectionContext _connectionContext;
protected Http2Connection _connection; protected Http2Connection _connection;
protected Task _connectionTask; protected Task _connectionTask;
@ -101,6 +105,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
_pair = DuplexPipe.CreateConnectionPair(inputPipeOptions, outputPipeOptions); _pair = DuplexPipe.CreateConnectionPair(inputPipeOptions, outputPipeOptions);
_hpackDecoder = new HPackDecoder((int)_clientSettings.HeaderTableSize, MaxRequestHeaderFieldSize); _hpackDecoder = new HPackDecoder((int)_clientSettings.HeaderTableSize, MaxRequestHeaderFieldSize);
_timeoutControl = new TimeoutControl(_mockTimeoutHandler.Object);
_noopApplication = context => Task.CompletedTask; _noopApplication = context => Task.CompletedTask;
@ -282,13 +287,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
.Setup(m => m.Http2ConnectionClosed(It.IsAny<string>(), It.IsAny<int>())) .Setup(m => m.Http2ConnectionClosed(It.IsAny<string>(), It.IsAny<int>()))
.Callback(() => _closedStateReached.SetResult(null)); .Callback(() => _closedStateReached.SetResult(null));
_connectionContext = new Http2ConnectionContext _connectionContext = new HttpConnectionContext
{ {
ConnectionContext = Mock.Of<ConnectionContext>(), ConnectionContext = Mock.Of<ConnectionContext>(),
ConnectionFeatures = new FeatureCollection(), ConnectionFeatures = new FeatureCollection(),
ServiceContext = new TestServiceContext(LoggerFactory, mockKestrelTrace.Object), ServiceContext = new TestServiceContext(LoggerFactory, mockKestrelTrace.Object),
MemoryPool = _memoryPool, MemoryPool = _memoryPool,
Transport = _pair.Transport Transport = _pair.Transport,
TimeoutControl = _timeoutControl
}; };
_connection = new Http2Connection(_connectionContext); _connection = new Http2Connection(_connectionContext);

View File

@ -100,20 +100,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
using (var server = CreateServer(testContext)) using (var server = CreateServer(testContext))
using (var connection = server.CreateConnection()) using (var connection = server.CreateConnection())
{ {
// When the in-memory connection is aborted, the input PipeWriter is completed behind the scenes foreach (var ch in "POST / HTTP/1.1\r\nHost:\r\n\r\n")
// so eventually connection.Send() throws an InvalidOperationException.
await Assert.ThrowsAsync<InvalidOperationException>(async () =>
{ {
foreach (var ch in "POST / HTTP/1.1\r\nHost:\r\n\r\n") await connection.Send(ch.ToString());
{
await connection.Send(ch.ToString());
testContext.MockSystemClock.UtcNow += ShortDelay; testContext.MockSystemClock.UtcNow += ShortDelay;
heartbeatManager.OnHeartbeat(testContext.SystemClock.UtcNow); heartbeatManager.OnHeartbeat(testContext.SystemClock.UtcNow);
} }
});
await ReceiveTimeoutResponse(connection, testContext); await ReceiveTimeoutResponse(connection, testContext);
await connection.WaitForConnectionClose();
} }
} }

View File

@ -0,0 +1,80 @@
// 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 System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.Logging;
using Xunit;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
public class ResponseDrainingTests : TestApplicationErrorLoggerLoggedTest
{
public static TheoryData<ListenOptions> ConnectionAdapterData => new TheoryData<ListenOptions>
{
new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)),
new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0))
{
ConnectionAdapters = { new PassThroughConnectionAdapter() }
}
};
[Theory]
[MemberData(nameof(ConnectionAdapterData))]
public async Task ConnectionClosedWhenResponseNotDrainedAtMinimumDataRate(ListenOptions listenOptions)
{
var testContext = new TestServiceContext(LoggerFactory);
var heartbeatManager = new HeartbeatManager(testContext.ConnectionManager);
var minRate = new MinDataRate(16384, TimeSpan.FromSeconds(2));
using (var server = new TestServer(context =>
{
context.Features.Get<IHttpMinResponseDataRateFeature>().MinDataRate = minRate;
return Task.CompletedTask;
}, testContext, listenOptions))
{
using (var connection = server.CreateConnection())
{
var transportConnection = connection.TransportConnection;
var outputBufferedTcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
transportConnection.Output.OnWriterCompleted((ex, state) =>
{
((TaskCompletionSource<object>)state).SetResult(null);
}, outputBufferedTcs);
await connection.Send(
"GET / HTTP/1.1",
"Host:",
"Connection: close",
"",
"");
// Wait for the drain timeout to be set.
await outputBufferedTcs.Task.DefaultTimeout();
testContext.MockSystemClock.UtcNow +=
Heartbeat.Interval +
TimeSpan.FromSeconds(testContext.ServerOptions.Limits.MaxResponseBufferSize.Value * 2 / minRate.BytesPerSecond);
heartbeatManager.OnHeartbeat(testContext.SystemClock.UtcNow);
Assert.Null(transportConnection.AbortReason);
testContext.MockSystemClock.UtcNow += TimeSpan.FromTicks(1);
heartbeatManager.OnHeartbeat(testContext.SystemClock.UtcNow);
Assert.NotNull(transportConnection.AbortReason);
Assert.Equal(CoreStrings.ConnectionTimedBecauseResponseMininumDataRateNotSatisfied, transportConnection.AbortReason.Message);
Assert.Single(TestApplicationErrorLogger.Messages, w => w.EventId.Id == 28 && w.LogLevel == LogLevel.Information);
}
}
}
}
}

View File

@ -107,17 +107,19 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
serviceContext.ServerOptions.Limits.MaxResponseBufferSize = 5; serviceContext.ServerOptions.Limits.MaxResponseBufferSize = 5;
var cts = new CancellationTokenSource(); var cts = new CancellationTokenSource();
var appTcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously); var appTcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
var writeReturnedTcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously); var writeStartedTcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
using (var server = new TestServer(async context => using (var server = new TestServer(async context =>
{ {
try try
{ {
await context.Response.WriteAsync("hello", cts.Token).DefaultTimeout(); await context.Response.WriteAsync("hello", cts.Token).DefaultTimeout();
writeReturnedTcs.TrySetResult(null);
var task = context.Response.WriteAsync("world", cts.Token); var task = context.Response.WriteAsync("world", cts.Token);
Assert.False(task.IsCompleted); Assert.False(task.IsCompleted);
writeStartedTcs.TrySetResult(null);
await task.DefaultTimeout(); await task.DefaultTimeout();
} }
catch (Exception ex) catch (Exception ex)
@ -127,7 +129,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
finally finally
{ {
appTcs.TrySetResult(null); appTcs.TrySetResult(null);
writeReturnedTcs.TrySetCanceled(); writeStartedTcs.TrySetCanceled();
} }
}, serviceContext)) }, serviceContext))
{ {
@ -146,7 +148,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
"5", "5",
"hello"); "hello");
await writeReturnedTcs.Task.DefaultTimeout(); await writeStartedTcs.Task.DefaultTimeout();
cts.Cancel(); cts.Cancel();

View File

@ -9,31 +9,32 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTrans
{ {
public class InMemoryConnection : StreamBackedTestConnection public class InMemoryConnection : StreamBackedTestConnection
{ {
private readonly InMemoryTransportConnection _transportConnection;
public InMemoryConnection(InMemoryTransportConnection transportConnection) public InMemoryConnection(InMemoryTransportConnection transportConnection)
: base(new RawStream(transportConnection.Output, transportConnection.Input)) : base(new RawStream(transportConnection.Output, transportConnection.Input))
{ {
_transportConnection = transportConnection; TransportConnection = transportConnection;
} }
public InMemoryTransportConnection TransportConnection { get; }
public override void Reset() public override void Reset()
{ {
_transportConnection.Input.Complete(new ConnectionResetException(string.Empty)); TransportConnection.Input.Complete(new ConnectionResetException(string.Empty));
_transportConnection.OnClosed(); TransportConnection.OnClosed();
} }
public override void ShutdownSend() public override void ShutdownSend()
{ {
_transportConnection.Input.Complete(); TransportConnection.Input.Complete();
_transportConnection.OnClosed(); TransportConnection.OnClosed();
} }
public override void Dispose() public override void Dispose()
{ {
_transportConnection.Input.Complete(); TransportConnection.Input.Complete();
_transportConnection.Output.Complete(); TransportConnection.Output.Complete();
_transportConnection.OnClosed(); TransportConnection.OnClosed();
base.Dispose(); base.Dispose();
} }
} }

View File

@ -31,9 +31,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTrans
public override PipeScheduler InputWriterScheduler => PipeScheduler.ThreadPool; public override PipeScheduler InputWriterScheduler => PipeScheduler.ThreadPool;
public override PipeScheduler OutputReaderScheduler => PipeScheduler.ThreadPool; public override PipeScheduler OutputReaderScheduler => PipeScheduler.ThreadPool;
public ConnectionAbortedException AbortReason { get; private set; }
public override void Abort(ConnectionAbortedException abortReason) public override void Abort(ConnectionAbortedException abortReason)
{ {
Input.Complete(abortReason); Input.Complete(abortReason);
AbortReason = abortReason;
} }
public void OnClosed() public void OnClosed()

View File

@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Connections.Features;
using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core; 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.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal;
@ -735,9 +736,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests
var connectionFeatures = new FeatureCollection(); var connectionFeatures = new FeatureCollection();
connectionFeatures.Set(Mock.Of<IConnectionLifetimeFeature>()); connectionFeatures.Set(Mock.Of<IConnectionLifetimeFeature>());
connectionFeatures.Set(Mock.Of<IBytesWrittenFeature>());
var http1Connection = new Http1Connection(new Http1ConnectionContext var http1Connection = new Http1Connection(new HttpConnectionContext
{ {
ServiceContext = serviceContext, ServiceContext = serviceContext,
ConnectionContext = Mock.Of<ConnectionContext>(), ConnectionContext = Mock.Of<ConnectionContext>(),

View File

@ -2,13 +2,14 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
namespace Microsoft.AspNetCore.Testing namespace Microsoft.AspNetCore.Testing
{ {
public class TestHttp1Connection : Http1Connection public class TestHttp1Connection : Http1Connection
{ {
public TestHttp1Connection(Http1ConnectionContext context) public TestHttp1Connection(HttpConnectionContext context)
: base(context) : base(context)
{ {
} }

View File

@ -20,8 +20,7 @@ namespace CodeGenerator
"ITransportSchedulerFeature", "ITransportSchedulerFeature",
"IConnectionLifetimeFeature", "IConnectionLifetimeFeature",
"IConnectionHeartbeatFeature", "IConnectionHeartbeatFeature",
"IConnectionLifetimeNotificationFeature", "IConnectionLifetimeNotificationFeature"
"IBytesWrittenFeature",
}; };
var usings = $@" var usings = $@"