diff --git a/benchmarks/Kestrel.Performance/InMemoryTransportBenchmark.cs b/benchmarks/Kestrel.Performance/InMemoryTransportBenchmark.cs index 1816531088..8440783b71 100644 --- a/benchmarks/Kestrel.Performance/InMemoryTransportBenchmark.cs +++ b/benchmarks/Kestrel.Performance/InMemoryTransportBenchmark.cs @@ -3,6 +3,7 @@ using BenchmarkDotNet.Attributes; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; @@ -204,6 +205,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance } } } + + protected override void AbortCore(ConnectionAbortedException abortReason) + { + } } // Copied from https://github.com/aspnet/benchmarks/blob/dev/src/Benchmarks/Middleware/PlaintextMiddleware.cs diff --git a/benchmarks/Kestrel.Performance/Mocks/MockTrace.cs b/benchmarks/Kestrel.Performance/Mocks/MockTrace.cs index a06661cf3a..8386402a78 100644 --- a/benchmarks/Kestrel.Performance/Mocks/MockTrace.cs +++ b/benchmarks/Kestrel.Performance/Mocks/MockTrace.cs @@ -44,6 +44,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance public void RequestBodyDrainTimedOut(string connectionId, string traceIdentifier) { } public void RequestBodyMininumDataRateNotSatisfied(string connectionId, string traceIdentifier, double rate) { } public void ResponseMininumDataRateNotSatisfied(string connectionId, string traceIdentifier) { } + public void ApplicationAbortedConnection(string connectionId, string traceIdentifier) { } public void Http2ConnectionError(string connectionId, Http2ConnectionErrorException ex) { } public void Http2StreamError(string connectionId, Http2StreamErrorException ex) { } public void HPackDecodingError(string connectionId, int streamId, HPackDecodingException ex) { } diff --git a/src/Connections.Abstractions/ConnectionContext.cs b/src/Connections.Abstractions/ConnectionContext.cs index aba4abdfc0..7c505ba108 100644 --- a/src/Connections.Abstractions/ConnectionContext.cs +++ b/src/Connections.Abstractions/ConnectionContext.cs @@ -1,5 +1,9 @@ -using System.Collections.Generic; +// 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.Collections.Generic; using System.IO.Pipelines; +using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Http.Features; namespace Microsoft.AspNetCore.Connections @@ -13,5 +17,13 @@ namespace Microsoft.AspNetCore.Connections public abstract IDictionary Items { get; set; } public abstract IDuplexPipe Transport { get; set; } + + public virtual void Abort(ConnectionAbortedException abortReason) + { + // We expect this to be overridden, but this helps maintain back compat + // with implementations of ConnectionContext that predate the addition of + // ConnectioContext.Abort() + Features.Get()?.Abort(); + } } } diff --git a/src/Connections.Abstractions/DefaultConnectionContext.cs b/src/Connections.Abstractions/DefaultConnectionContext.cs index 1b161c7253..4712eef953 100644 --- a/src/Connections.Abstractions/DefaultConnectionContext.cs +++ b/src/Connections.Abstractions/DefaultConnectionContext.cs @@ -65,7 +65,9 @@ namespace Microsoft.AspNetCore.Connections public CancellationToken ConnectionClosed { get; set; } - public virtual void Abort() + public void Abort() => Abort(abortReason: null); + + public override void Abort(ConnectionAbortedException abortReason) { ThreadPool.QueueUserWorkItem(cts => ((CancellationTokenSource)cts).Cancel(), _connectionClosedTokenSource); } diff --git a/src/Connections.Abstractions/Features/IConnectionLifetimeFeature.cs b/src/Connections.Abstractions/Features/IConnectionLifetimeFeature.cs index 8f804de898..9785205b73 100644 --- a/src/Connections.Abstractions/Features/IConnectionLifetimeFeature.cs +++ b/src/Connections.Abstractions/Features/IConnectionLifetimeFeature.cs @@ -8,6 +8,7 @@ namespace Microsoft.AspNetCore.Connections.Features public interface IConnectionLifetimeFeature { CancellationToken ConnectionClosed { get; set; } + void Abort(); } } diff --git a/src/Kestrel.Core/Adapter/Internal/AdaptedPipeline.cs b/src/Kestrel.Core/Adapter/Internal/AdaptedPipeline.cs index 221c4c1b84..2cbb76e5b1 100644 --- a/src/Kestrel.Core/Adapter/Internal/AdaptedPipeline.cs +++ b/src/Kestrel.Core/Adapter/Internal/AdaptedPipeline.cs @@ -6,7 +6,9 @@ using System.IO; using System.IO.Pipelines; using System.Threading.Tasks; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; +using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Adapter.Internal { @@ -15,23 +17,24 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Adapter.Internal private static readonly int MinAllocBufferSize = KestrelMemoryPool.MinimumSegmentSize / 2; private readonly IDuplexPipe _transport; - private readonly IDuplexPipe _application; public AdaptedPipeline(IDuplexPipe transport, - IDuplexPipe application, Pipe inputPipe, - Pipe outputPipe) + Pipe outputPipe, + IKestrelTrace log) { _transport = transport; - _application = application; Input = inputPipe; Output = outputPipe; + Log = log; } public Pipe Input { get; } public Pipe Output { get; } + public IKestrelTrace Log { get; } + PipeReader IDuplexPipe.Input => Input.Reader; PipeWriter IDuplexPipe.Output => Output.Writer; @@ -47,8 +50,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Adapter.Internal private async Task WriteOutputAsync(Stream stream) { - Exception error = null; - try { if (stream == null) @@ -63,13 +64,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Adapter.Internal try { - if (result.IsCanceled) - { - // Forward the cancellation to the transport pipe - _application.Input.CancelPendingRead(); - break; - } - if (buffer.IsEmpty) { if (result.IsCompleted) @@ -108,7 +102,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Adapter.Internal } catch (Exception ex) { - error = ex; + Log.LogError(0, ex, $"{nameof(AdaptedPipeline)}.{nameof(WriteOutputAsync)}"); } finally { diff --git a/src/Kestrel.Core/CoreStrings.resx b/src/Kestrel.Core/CoreStrings.resx index 0ba93c9219..ee80a16312 100644 --- a/src/Kestrel.Core/CoreStrings.resx +++ b/src/Kestrel.Core/CoreStrings.resx @@ -506,4 +506,16 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l Reading the request body timed out due to data arriving too slowly. See MinRequestBodyDataRate. + + The connection was aborted by the application. + + + The connection was aborted because the server is shutting down and request processing didn't complete within the time specified by HostOptions.ShutdownTimeout. + + + The connection was timed out by the server because the response was not read by the client at the specified minimum data rate. + + + The connection was timed out by the server. + \ No newline at end of file diff --git a/src/Kestrel.Core/Internal/Http/Http1Connection.cs b/src/Kestrel.Core/Internal/Http/Http1Connection.cs index f93a7997fa..61eac8a37c 100644 --- a/src/Kestrel.Core/Internal/Http/Http1Connection.cs +++ b/src/Kestrel.Core/Internal/Http/Http1Connection.cs @@ -45,12 +45,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http _requestHeadersTimeoutTicks = ServerOptions.Limits.RequestHeadersTimeout.Ticks; Output = new Http1OutputProducer( - _context.Application.Input, _context.Transport.Output, _context.ConnectionId, + _context.ConnectionContext, _context.ServiceContext.Log, _context.TimeoutControl, - _context.ConnectionFeatures.Get(), _context.ConnectionFeatures.Get()); } diff --git a/src/Kestrel.Core/Internal/Http/Http1ConnectionContext.cs b/src/Kestrel.Core/Internal/Http/Http1ConnectionContext.cs index 9dbcee29e7..28b5f95ed4 100644 --- a/src/Kestrel.Core/Internal/Http/Http1ConnectionContext.cs +++ b/src/Kestrel.Core/Internal/Http/Http1ConnectionContext.cs @@ -4,6 +4,7 @@ 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; @@ -13,6 +14,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { public string ConnectionId { get; set; } public ServiceContext ServiceContext { get; set; } + public ConnectionContext ConnectionContext { get; set; } public IFeatureCollection ConnectionFeatures { get; set; } public MemoryPool MemoryPool { get; set; } public IPEndPoint RemoteEndPoint { get; set; } diff --git a/src/Kestrel.Core/Internal/Http/Http1OutputProducer.cs b/src/Kestrel.Core/Internal/Http/Http1OutputProducer.cs index 4ef0dff24e..0fd62301bd 100644 --- a/src/Kestrel.Core/Internal/Http/Http1OutputProducer.cs +++ b/src/Kestrel.Core/Internal/Http/Http1OutputProducer.cs @@ -8,6 +8,7 @@ using System.Runtime.CompilerServices; using System.Text; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; @@ -22,9 +23,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http private static readonly ReadOnlyMemory _endChunkedResponseBytes = new ReadOnlyMemory(Encoding.ASCII.GetBytes("0\r\n\r\n")); private readonly string _connectionId; + private readonly ConnectionContext _connectionContext; private readonly ITimeoutControl _timeoutControl; private readonly IKestrelTrace _log; - private readonly IConnectionLifetimeFeature _lifetimeFeature; private readonly IBytesWrittenFeature _transportBytesWrittenFeature; // This locks access to to all of the below fields @@ -36,7 +37,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http private long _totalBytesCommitted; private readonly PipeWriter _pipeWriter; - private readonly PipeReader _outputPipeReader; // https://github.com/dotnet/corefxlab/issues/1334 // Pipelines don't support multiple awaiters on flush @@ -48,21 +48,19 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http private ValueTask _flushTask; public Http1OutputProducer( - PipeReader outputPipeReader, PipeWriter pipeWriter, string connectionId, + ConnectionContext connectionContext, IKestrelTrace log, ITimeoutControl timeoutControl, - IConnectionLifetimeFeature lifetimeFeature, IBytesWrittenFeature transportBytesWrittenFeature) { - _outputPipeReader = outputPipeReader; _pipeWriter = pipeWriter; _connectionId = connectionId; + _connectionContext = connectionContext; _timeoutControl = timeoutControl; _log = log; _flushCompleted = OnFlushCompleted; - _lifetimeFeature = lifetimeFeature; _transportBytesWrittenFeature = transportBytesWrittenFeature; } @@ -169,7 +167,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } } - public void Abort(Exception error) + public void Abort(ConnectionAbortedException error) { // Abort can be called after Dispose if there's a flush timeout. // It's important to still call _lifetimeFeature.Abort() in this case. @@ -181,17 +179,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http return; } + _aborted = true; + _connectionContext.Abort(error); + if (!_completed) { _log.ConnectionDisconnect(_connectionId); _completed = true; - - _outputPipeReader.CancelPendingRead(); - _pipeWriter.Complete(error); + _pipeWriter.Complete(); } - - _aborted = true; - _lifetimeFeature.Abort(); } } diff --git a/src/Kestrel.Core/Internal/Http/HttpProtocol.FeatureCollection.cs b/src/Kestrel.Core/Internal/Http/HttpProtocol.FeatureCollection.cs index 5b9cb5c5d6..235e724ffc 100644 --- a/src/Kestrel.Core/Internal/Http/HttpProtocol.FeatureCollection.cs +++ b/src/Kestrel.Core/Internal/Http/HttpProtocol.FeatureCollection.cs @@ -227,7 +227,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http void IHttpRequestLifetimeFeature.Abort() { - Abort(new ConnectionAbortedException()); + Log.ApplicationAbortedConnection(ConnectionId, TraceIdentifier); + Abort(new ConnectionAbortedException(CoreStrings.ConnectionAbortedByApplication)); } } } diff --git a/src/Kestrel.Core/Internal/Http/HttpProtocol.cs b/src/Kestrel.Core/Internal/Http/HttpProtocol.cs index 4e35490a79..0c759c4451 100644 --- a/src/Kestrel.Core/Internal/Http/HttpProtocol.cs +++ b/src/Kestrel.Core/Internal/Http/HttpProtocol.cs @@ -39,11 +39,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http protected Streams _streams; - protected Stack, object>> _onStarting; - protected Stack, object>> _onCompleted; + private Stack, object>> _onStarting; + private Stack, object>> _onCompleted; - protected volatile int _requestAborted; - protected CancellationTokenSource _abortedCts; + private int _requestAborted; + private volatile int _ioCompleted; + private CancellationTokenSource _abortedCts; private CancellationToken? _manuallySetRequestAbortToken; protected RequestProcessingStatus _requestProcessingStatus; @@ -51,15 +52,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http protected bool _upgradeAvailable; private bool _canHaveBody; private bool _autoChunk; - protected Exception _applicationException; + private Exception _applicationException; private BadHttpRequestException _requestRejectedException; protected HttpVersion _httpVersion; private string _requestId; - protected int _requestHeadersParsed; + private int _requestHeadersParsed; - protected long _responseBytesWritten; + private long _responseBytesWritten; private readonly IHttpProtocolContext _context; @@ -247,7 +248,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http var cts = _abortedCts; return cts != null ? cts.Token : - (_requestAborted == 1) ? new CancellationToken(true) : + (_ioCompleted == 1) ? new CancellationToken(true) : RequestAbortedSource.Token; } set @@ -272,7 +273,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http var cts = LazyInitializer.EnsureInitialized(ref _abortedCts, () => new CancellationTokenSource()) ?? new CancellationTokenSource(); - if (_requestAborted == 1) + if (_ioCompleted == 1) { cts.Cancel(); } @@ -410,32 +411,40 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } } - /// - /// Immediately kill the connection and poison the request and response streams with an error if there is one. - /// - public void Abort(Exception error) + public void OnInputOrOutputCompleted() { - if (Interlocked.Exchange(ref _requestAborted, 1) != 0) + if (Interlocked.Exchange(ref _ioCompleted, 1) != 0) { return; } _keepAlive = false; - // If Abort() isn't called with an exception, there was a FIN. In this case, even though the connection is - // still closed immediately, we allow the app to drain the data in the request buffer. If the request data - // was truncated, MessageBody will complete the RequestBodyPipe with an error. - if (error != null) - { - _streams?.Abort(error); - } - - Output.Abort(error); + Output.Dispose(); // Potentially calling user code. CancelRequestAbortedToken logs any exceptions. ServiceContext.Scheduler.Schedule(state => ((HttpProtocol)state).CancelRequestAbortedToken(), this); } + /// + /// Immediately kill the connection and poison the request and response streams with an error if there is one. + /// + public void Abort(ConnectionAbortedException abortReason) + { + if (Interlocked.Exchange(ref _requestAborted, 1) != 0) + { + return; + } + + _streams?.Abort(abortReason); + + // Abort output prior to calling OnIOCompleted() to give the transport the chance to + // complete the input with the correct error and message. + Output.Abort(abortReason); + + OnInputOrOutputCompleted(); + } + public void OnHeader(Span name, Span value) { _requestHeadersParsed++; @@ -543,7 +552,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http // Run the application code for this request await application.ProcessRequestAsync(httpContext); - if (_requestAborted == 0) + if (_ioCompleted == 0) { VerifyResponseContentLength(); } @@ -579,8 +588,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http // 4XX responses are written by TryProduceInvalidRequestResponse during connection tear down. if (_requestRejectedException == null) { - // If _requestAbort is set, the connection has already been closed. - if (_requestAborted == 0) + if (_ioCompleted == 0) { // Call ProduceEnd() before consuming the rest of the request body to prevent // delaying clients waiting for the chunk terminator: @@ -612,7 +620,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http application.DisposeContext(httpContext, _applicationException); // Even for non-keep-alive requests, try to consume the entire body to avoid RSTs. - if (_requestAborted == 0 && _requestRejectedException == null && !messageBody.IsEmpty) + if (_ioCompleted == 0 && _requestRejectedException == null && !messageBody.IsEmpty) { await messageBody.ConsumeAsync(); } @@ -1010,8 +1018,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http protected Task TryProduceInvalidRequestResponse() { - // If _requestAborted is set, the connection has already been closed. - if (_requestRejectedException != null && _requestAborted == 0) + // If _ioCompleted is set, the connection has already been closed. + if (_requestRejectedException != null && _ioCompleted == 0) { return ProduceEnd(); } diff --git a/src/Kestrel.Core/Internal/Http/IHttpOutputProducer.cs b/src/Kestrel.Core/Internal/Http/IHttpOutputProducer.cs index 0c45253bdc..41dfdbbbec 100644 --- a/src/Kestrel.Core/Internal/Http/IHttpOutputProducer.cs +++ b/src/Kestrel.Core/Internal/Http/IHttpOutputProducer.cs @@ -5,12 +5,13 @@ using System; using System.IO.Pipelines; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Connections; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { public interface IHttpOutputProducer : IDisposable { - void Abort(Exception error); + void Abort(ConnectionAbortedException abortReason); Task WriteAsync(Func callback, T state); Task FlushAsync(CancellationToken cancellationToken); Task Write100ContinueAsync(CancellationToken cancellationToken); diff --git a/src/Kestrel.Core/Internal/Http2/Http2Connection.cs b/src/Kestrel.Core/Internal/Http2/Http2Connection.cs index 549e804d33..94ff8f5c54 100644 --- a/src/Kestrel.Core/Internal/Http2/Http2Connection.cs +++ b/src/Kestrel.Core/Internal/Http2/Http2Connection.cs @@ -89,7 +89,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 public IFeatureCollection ConnectionFeatures => _context.ConnectionFeatures; - public void Abort(Exception ex) + public void OnInputOrOutputCompleted() + { + _stopping = true; + _frameWriter.Abort(ex: null); + } + + public void Abort(ConnectionAbortedException ex) { _stopping = true; _frameWriter.Abort(ex); @@ -202,7 +208,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 { foreach (var stream in _streams.Values) { - stream.Abort(error); + stream.Http2Abort(error); } await _frameWriter.WriteGoAwayAsync(_highestOpenedStreamId, errorCode); @@ -464,7 +470,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 if (_streams.TryGetValue(_incomingFrame.StreamId, out var stream)) { - stream.Abort(error: null); + stream.Abort(abortReason: null); } return Task.CompletedTask; diff --git a/src/Kestrel.Core/Internal/Http2/Http2OutputProducer.cs b/src/Kestrel.Core/Internal/Http2/Http2OutputProducer.cs index 0fe1fbf344..e701654d1e 100644 --- a/src/Kestrel.Core/Internal/Http2/Http2OutputProducer.cs +++ b/src/Kestrel.Core/Internal/Http2/Http2OutputProducer.cs @@ -5,6 +5,7 @@ using System; using System.IO.Pipelines; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; @@ -25,7 +26,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 { } - public void Abort(Exception error) + public void Abort(ConnectionAbortedException error) { // TODO: RST_STREAM? } diff --git a/src/Kestrel.Core/Internal/Http2/Http2Stream.cs b/src/Kestrel.Core/Internal/Http2/Http2Stream.cs index b1f754884a..0234879a37 100644 --- a/src/Kestrel.Core/Internal/Http2/Http2Stream.cs +++ b/src/Kestrel.Core/Internal/Http2/Http2Stream.cs @@ -142,5 +142,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 RequestBodyPipe.Writer.Complete(ex); } } + + // TODO: The HTTP/2 tests expect the request and response streams to be aborted with + // non-ConnectionAbortedExceptions. The abortReasons can include things like + // Http2ConnectionErrorException which don't derive from IOException or + // OperationCanceledException. This is probably not a good idea. + public void Http2Abort(Exception abortReason) + { + _streams?.Abort(abortReason); + + OnInputOrOutputCompleted(); + } } } diff --git a/src/Kestrel.Core/Internal/HttpConnection.cs b/src/Kestrel.Core/Internal/HttpConnection.cs index 54201f30ce..d1a32c6a29 100644 --- a/src/Kestrel.Core/Internal/HttpConnection.cs +++ b/src/Kestrel.Core/Internal/HttpConnection.cs @@ -10,6 +10,8 @@ using System.IO.Pipelines; using System.Net; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Core.Adapter.Internal; @@ -123,9 +125,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal if (_context.ConnectionAdapters.Count > 0) { adaptedPipeline = new AdaptedPipeline(_adaptedTransport, - application, new Pipe(AdaptedInputPipeOptions), - new Pipe(AdaptedOutputPipeOptions)); + new Pipe(AdaptedOutputPipeOptions), + Log); _adaptedTransport = adaptedPipeline; } @@ -222,6 +224,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal LocalEndPoint = LocalEndPoint, RemoteEndPoint = RemoteEndPoint, ServiceContext = _context.ServiceContext, + ConnectionContext = _context.ConnectionContext, TimeoutControl = this, Transport = transport, Application = application @@ -255,15 +258,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal switch (_protocolSelectionState) { case ProtocolSelectionState.Initializing: - CloseUninitializedConnection(); - _protocolSelectionState = ProtocolSelectionState.Stopped; + CloseUninitializedConnection(abortReason: null); + _protocolSelectionState = ProtocolSelectionState.Aborted; break; case ProtocolSelectionState.Selected: _requestProcessor.StopProcessingNextRequest(); - _protocolSelectionState = ProtocolSelectionState.Stopping; break; - case ProtocolSelectionState.Stopping: - case ProtocolSelectionState.Stopped: + case ProtocolSelectionState.Aborted: break; } } @@ -271,28 +272,47 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal return _lifetimeTask; } - public void Abort(Exception ex) + public void OnInputOrOutputCompleted() { lock (_protocolSelectionLock) { switch (_protocolSelectionState) { case ProtocolSelectionState.Initializing: - CloseUninitializedConnection(); + CloseUninitializedConnection(abortReason: null); + _protocolSelectionState = ProtocolSelectionState.Aborted; break; case ProtocolSelectionState.Selected: - case ProtocolSelectionState.Stopping: - _requestProcessor.Abort(ex); + _requestProcessor.OnInputOrOutputCompleted(); break; - case ProtocolSelectionState.Stopped: + case ProtocolSelectionState.Aborted: break; } - _protocolSelectionState = ProtocolSelectionState.Stopped; } } - public Task AbortAsync(Exception ex) + public void Abort(ConnectionAbortedException ex) + { + lock (_protocolSelectionLock) + { + switch (_protocolSelectionState) + { + case ProtocolSelectionState.Initializing: + CloseUninitializedConnection(ex); + break; + case ProtocolSelectionState.Selected: + _requestProcessor.Abort(ex); + break; + case ProtocolSelectionState.Aborted: + break; + } + + _protocolSelectionState = ProtocolSelectionState.Aborted; + } + } + + public Task AbortAsync(ConnectionAbortedException ex) { Abort(ex); @@ -373,7 +393,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal public void Tick(DateTimeOffset now) { - if (_protocolSelectionState == ProtocolSelectionState.Stopped) + if (_protocolSelectionState == ProtocolSelectionState.Aborted) { // It's safe to check for timeouts on a dead connection, // but try not to in order to avoid extraneous logs. @@ -419,7 +439,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal break; case TimeoutAction.AbortConnection: // This is actually supported with HTTP/2! - Abort(new TimeoutException()); + Abort(new ConnectionAbortedException(CoreStrings.ConnectionTimedOutByServer)); break; } } @@ -482,7 +502,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal { RequestTimedOut = true; Log.ResponseMininumDataRateNotSatisfied(_http1Connection.ConnectionIdFeature, _http1Connection.TraceIdentifier); - Abort(new TimeoutException()); + Abort(new ConnectionAbortedException(CoreStrings.ConnectionTimedBecauseResponseMininumDataRateNotSatisfied)); } } } @@ -622,13 +642,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal ResetTimeout(timeSpan.Ticks, TimeoutAction.AbortConnection); } - private void CloseUninitializedConnection() + private void CloseUninitializedConnection(ConnectionAbortedException abortReason) { Debug.Assert(_adaptedTransport != null); - // CancelPendingRead signals the transport directly to close the connection - // without any potential interference from connection adapters. - _context.Application.Input.CancelPendingRead(); + _context.ConnectionContext.Abort(abortReason); _adaptedTransport.Input.Complete(); _adaptedTransport.Output.Complete(); @@ -638,8 +656,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal { Initializing, Selected, - Stopping, - Stopped + Aborted } } } diff --git a/src/Kestrel.Core/Internal/HttpConnectionMiddleware.cs b/src/Kestrel.Core/Internal/HttpConnectionMiddleware.cs index d573c74e23..4ce7c3601c 100644 --- a/src/Kestrel.Core/Internal/HttpConnectionMiddleware.cs +++ b/src/Kestrel.Core/Internal/HttpConnectionMiddleware.cs @@ -1,4 +1,6 @@ -using System; +// 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.Collections.Generic; using System.Net; using System.Threading; @@ -75,11 +77,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal var processingTask = connection.StartRequestProcessing(_application); connectionContext.Transport.Input.OnWriterCompleted( - (error, state) => ((HttpConnection)state).Abort(error), + (_, state) => ((HttpConnection)state).OnInputOrOutputCompleted(), connection); connectionContext.Transport.Output.OnReaderCompleted( - (error, state) => ((HttpConnection)state).Abort(error), + (_, state) => ((HttpConnection)state).OnInputOrOutputCompleted(), connection); await AsTask(lifetimeFeature.ConnectionClosed); diff --git a/src/Kestrel.Core/Internal/IRequestProcessor.cs b/src/Kestrel.Core/Internal/IRequestProcessor.cs index 5b40735ac8..d7385f9b96 100644 --- a/src/Kestrel.Core/Internal/IRequestProcessor.cs +++ b/src/Kestrel.Core/Internal/IRequestProcessor.cs @@ -3,6 +3,7 @@ using System; using System.Threading.Tasks; +using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Hosting.Server; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal @@ -11,6 +12,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal { Task ProcessRequestsAsync(IHttpApplication application); void StopProcessingNextRequest(); - void Abort(Exception ex); + void OnInputOrOutputCompleted(); + void Abort(ConnectionAbortedException ex); } } \ No newline at end of file diff --git a/src/Kestrel.Core/Internal/Infrastructure/HttpConnectionManagerShutdownExtensions.cs b/src/Kestrel.Core/Internal/Infrastructure/HttpConnectionManagerShutdownExtensions.cs index 24400fedef..1b601c919b 100644 --- a/src/Kestrel.Core/Internal/Infrastructure/HttpConnectionManagerShutdownExtensions.cs +++ b/src/Kestrel.Core/Internal/Infrastructure/HttpConnectionManagerShutdownExtensions.cs @@ -26,7 +26,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure public static async Task AbortAllConnectionsAsync(this HttpConnectionManager connectionManager) { var abortTasks = new List(); - var canceledException = new ConnectionAbortedException(); + var canceledException = new ConnectionAbortedException(CoreStrings.ConnectionAbortedDuringServerShutdown); connectionManager.Walk(connection => { diff --git a/src/Kestrel.Core/Internal/Infrastructure/IKestrelTrace.cs b/src/Kestrel.Core/Internal/Infrastructure/IKestrelTrace.cs index e58decaafe..caaf66cfae 100644 --- a/src/Kestrel.Core/Internal/Infrastructure/IKestrelTrace.cs +++ b/src/Kestrel.Core/Internal/Infrastructure/IKestrelTrace.cs @@ -52,6 +52,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure void ResponseMininumDataRateNotSatisfied(string connectionId, string traceIdentifier); + void ApplicationAbortedConnection(string connectionId, string traceIdentifier); + void Http2ConnectionError(string connectionId, Http2ConnectionErrorException ex); void Http2StreamError(string connectionId, Http2StreamErrorException ex); diff --git a/src/Kestrel.Core/Internal/Infrastructure/KestrelTrace.cs b/src/Kestrel.Core/Internal/Infrastructure/KestrelTrace.cs index b9c0e98d5a..3ef1aa1721 100644 --- a/src/Kestrel.Core/Internal/Infrastructure/KestrelTrace.cs +++ b/src/Kestrel.Core/Internal/Infrastructure/KestrelTrace.cs @@ -65,12 +65,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal private static readonly Action _requestBodyMinimumDataRateNotSatisfied = LoggerMessage.Define(LogLevel.Information, new EventId(27, nameof(RequestBodyMininumDataRateNotSatisfied)), @"Connection id ""{ConnectionId}"", Request id ""{TraceIdentifier}"": the request timed out because it was not sent by the client at a minimum of {Rate} bytes/second."); - private static readonly Action _requestBodyNotEntirelyRead = - LoggerMessage.Define(LogLevel.Information, new EventId(32, nameof(RequestBodyNotEntirelyRead)), @"Connection id ""{ConnectionId}"", Request id ""{TraceIdentifier}"": the application completed without reading the entire request body."); - - private static readonly Action _requestBodyDrainTimedOut = - LoggerMessage.Define(LogLevel.Information, new EventId(33, nameof(RequestBodyDrainTimedOut)), @"Connection id ""{ConnectionId}"", Request id ""{TraceIdentifier}"": automatic draining of the request body timed out after taking over 5 seconds."); - private static readonly Action _responseMinimumDataRateNotSatisfied = LoggerMessage.Define(LogLevel.Information, new EventId(28, nameof(ResponseMininumDataRateNotSatisfied)), @"Connection id ""{ConnectionId}"", Request id ""{TraceIdentifier}"": the connection was closed because the response was not read by the client at the specified minimum data rate."); @@ -83,6 +77,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal private static readonly Action _hpackDecodingError = LoggerMessage.Define(LogLevel.Information, new EventId(31, nameof(HPackDecodingError)), @"Connection id ""{ConnectionId}"": HPACK decoding error while decoding headers for stream ID {StreamId}."); + private static readonly Action _requestBodyNotEntirelyRead = + LoggerMessage.Define(LogLevel.Information, new EventId(32, nameof(RequestBodyNotEntirelyRead)), @"Connection id ""{ConnectionId}"", Request id ""{TraceIdentifier}"": the application completed without reading the entire request body."); + + private static readonly Action _requestBodyDrainTimedOut = + LoggerMessage.Define(LogLevel.Information, new EventId(33, nameof(RequestBodyDrainTimedOut)), @"Connection id ""{ConnectionId}"", Request id ""{TraceIdentifier}"": automatic draining of the request body timed out after taking over 5 seconds."); + + private static readonly Action _applicationAbortedConnection = + LoggerMessage.Define(LogLevel.Information, new EventId(34, nameof(RequestBodyDrainTimedOut)), @"Connection id ""{ConnectionId}"", Request id ""{TraceIdentifier}"": the application aborted the connection."); + protected readonly ILogger _logger; public KestrelTrace(ILogger logger) @@ -195,6 +198,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal _responseMinimumDataRateNotSatisfied(_logger, connectionId, traceIdentifier, null); } + public virtual void ApplicationAbortedConnection(string connectionId, string traceIdentifier) + { + _applicationAbortedConnection(_logger, connectionId, traceIdentifier, null); + } + public virtual void Http2ConnectionError(string connectionId, Http2ConnectionErrorException ex) { _http2ConnectionError(_logger, connectionId, ex); diff --git a/src/Kestrel.Core/Properties/CoreStrings.Designer.cs b/src/Kestrel.Core/Properties/CoreStrings.Designer.cs index dab35d664d..c813873491 100644 --- a/src/Kestrel.Core/Properties/CoreStrings.Designer.cs +++ b/src/Kestrel.Core/Properties/CoreStrings.Designer.cs @@ -1820,6 +1820,62 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core internal static string FormatBadRequest_RequestBodyTimeout() => GetString("BadRequest_RequestBodyTimeout"); + /// + /// The connection was aborted by the application. + /// + internal static string ConnectionAbortedByApplication + { + get => GetString("ConnectionAbortedByApplication"); + } + + /// + /// The connection was aborted by the application. + /// + internal static string FormatConnectionAbortedByApplication() + => GetString("ConnectionAbortedByApplication"); + + /// + /// The connection was aborted because the server is shutting down and request processing didn't complete within the time specified by HostOptions.ShutdownTimeout. + /// + internal static string ConnectionAbortedDuringServerShutdown + { + get => GetString("ConnectionAbortedDuringServerShutdown"); + } + + /// + /// The connection was aborted because the server is shutting down and request processing didn't complete within the time specified by HostOptions.ShutdownTimeout. + /// + internal static string FormatConnectionAbortedDuringServerShutdown() + => GetString("ConnectionAbortedDuringServerShutdown"); + + /// + /// The connection was timed out by the server because the response was not read by the client at the specified minimum data rate. + /// + internal static string ConnectionTimedBecauseResponseMininumDataRateNotSatisfied + { + get => GetString("ConnectionTimedBecauseResponseMininumDataRateNotSatisfied"); + } + + /// + /// The connection was timed out by the server because the response was not read by the client at the specified minimum data rate. + /// + internal static string FormatConnectionTimedBecauseResponseMininumDataRateNotSatisfied() + => GetString("ConnectionTimedBecauseResponseMininumDataRateNotSatisfied"); + + /// + /// The connection was timed out by the server. + /// + internal static string ConnectionTimedOutByServer + { + get => GetString("ConnectionTimedOutByServer"); + } + + /// + /// The connection was timed out by the server. + /// + internal static string FormatConnectionTimedOutByServer() + => GetString("ConnectionTimedOutByServer"); + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Kestrel.Transport.Abstractions/Internal/TransportConnection.FeatureCollection.cs b/src/Kestrel.Transport.Abstractions/Internal/TransportConnection.FeatureCollection.cs index 49200bd66a..63f20ad51f 100644 --- a/src/Kestrel.Transport.Abstractions/Internal/TransportConnection.FeatureCollection.cs +++ b/src/Kestrel.Transport.Abstractions/Internal/TransportConnection.FeatureCollection.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.IO.Pipelines; using System.Net; using System.Threading; +using System.Threading.Tasks; using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Http.Features; @@ -84,7 +85,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal set => ConnectionClosed = value; } - void IConnectionLifetimeFeature.Abort() => Abort(); + void IConnectionLifetimeFeature.Abort() => Abort(abortReason: null); long IBytesWrittenFeature.TotalBytesWritten => TotalBytesWritten; } diff --git a/src/Kestrel.Transport.Abstractions/Internal/TransportConnection.cs b/src/Kestrel.Transport.Abstractions/Internal/TransportConnection.cs index 089058a6b0..0631258475 100644 --- a/src/Kestrel.Transport.Abstractions/Internal/TransportConnection.cs +++ b/src/Kestrel.Transport.Abstractions/Internal/TransportConnection.cs @@ -11,21 +11,13 @@ using Microsoft.AspNetCore.Http.Features; namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal { - public partial class TransportConnection : ConnectionContext + public abstract partial class TransportConnection : ConnectionContext { private IDictionary _items; public TransportConnection() { - _currentIConnectionIdFeature = this; - _currentIConnectionTransportFeature = this; - _currentIHttpConnectionFeature = this; - _currentIConnectionItemsFeature = this; - _currentIApplicationTransportFeature = this; - _currentIMemoryPoolFeature = this; - _currentITransportSchedulerFeature = this; - _currentIConnectionLifetimeFeature = this; - _currentIBytesWrittenFeature = this; + FastReset(); } public IPAddress RemoteAddress { get; set; } @@ -63,8 +55,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal public CancellationToken ConnectionClosed { get; set; } - public virtual void Abort() + // DO NOT remove this override to ConnectionContext.Abort(). Doing so would cause + // any TransportConnection that does not override Abort() or calls base.Abort() + // to stack overflow when IConnectionLifetimeFeature.Abort() is called. + public override void Abort(ConnectionAbortedException abortReason) { + AbortCore(abortReason); } + + protected abstract void AbortCore(ConnectionAbortedException abortReason); } } diff --git a/src/Kestrel.Transport.Libuv/Internal/LibuvConnection.cs b/src/Kestrel.Transport.Libuv/Internal/LibuvConnection.cs index a01b019b9d..efb696f0f0 100644 --- a/src/Kestrel.Transport.Libuv/Internal/LibuvConnection.cs +++ b/src/Kestrel.Transport.Libuv/Internal/LibuvConnection.cs @@ -28,6 +28,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal private readonly UvStreamHandle _socket; private readonly CancellationTokenSource _connectionClosedTokenSource = new CancellationTokenSource(); + private volatile ConnectionAbortedException _abortReason; + private MemoryHandle _bufferHandle; public LibuvConnection(UvStreamHandle socket, ILibuvTrace log, LibuvThread thread, IPEndPoint remoteEndPoint, IPEndPoint localEndPoint) @@ -93,7 +95,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal finally { // Now, complete the input so that no more reads can happen - Input.Complete(inputError ?? new ConnectionAbortedException()); + Input.Complete(inputError ?? _abortReason ?? new ConnectionAbortedException()); Output.Complete(outputError); // Make sure it isn't possible for a paused read to resume reading after calling uv_close @@ -114,8 +116,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal } } - public override void Abort() + protected override void AbortCore(ConnectionAbortedException abortReason) { + _abortReason = abortReason; + Output.CancelPendingRead(); + // This cancels any pending I/O. Thread.Post(s => s.Dispose(), _socket); } diff --git a/src/Kestrel.Transport.Libuv/Internal/LibuvOutputConsumer.cs b/src/Kestrel.Transport.Libuv/Internal/LibuvOutputConsumer.cs index 1482bd3359..4666ae198e 100644 --- a/src/Kestrel.Transport.Libuv/Internal/LibuvOutputConsumer.cs +++ b/src/Kestrel.Transport.Libuv/Internal/LibuvOutputConsumer.cs @@ -41,17 +41,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal while (true) { - ReadResult result; - - try - { - result = await _pipe.ReadAsync(); - } - catch - { - // Handled in LibuvConnection.Abort() - return; - } + var result = await _pipe.ReadAsync(); var buffer = result.Buffer; var consumed = buffer.End; diff --git a/src/Kestrel.Transport.Sockets/Internal/SocketConnection.cs b/src/Kestrel.Transport.Sockets/Internal/SocketConnection.cs index 4c067e8cb6..79fb522949 100644 --- a/src/Kestrel.Transport.Sockets/Internal/SocketConnection.cs +++ b/src/Kestrel.Transport.Sockets/Internal/SocketConnection.cs @@ -31,6 +31,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal private readonly object _shutdownLock = new object(); private volatile bool _aborted; + private volatile ConnectionAbortedException _abortReason; private long _totalBytesWritten; internal SocketConnection(Socket socket, MemoryPool memoryPool, PipeScheduler scheduler, ISocketsTrace trace) @@ -91,8 +92,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal } } - public override void Abort() + protected override void AbortCore(ConnectionAbortedException abortReason) { + _abortReason = abortReason; + Output.CancelPendingRead(); + // Try to gracefully close the socket to match libuv behavior. Shutdown(); } @@ -125,7 +129,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal if (!_aborted) { // Calling Dispose after ReceiveAsync can cause an "InvalidArgument" error on *nix. - error = new ConnectionAbortedException(); _trace.ConnectionError(ConnectionId, error); } } @@ -133,7 +136,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal { if (!_aborted) { - error = new ConnectionAbortedException(); _trace.ConnectionError(ConnectionId, error); } } @@ -151,7 +153,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal { if (_aborted) { - error = error ?? new ConnectionAbortedException(); + error = error ?? _abortReason ?? new ConnectionAbortedException(); } Input.Complete(error); @@ -221,10 +223,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal catch (IOException ex) { error = ex; + _trace.ConnectionError(ConnectionId, error); } catch (Exception ex) { error = new IOException(ex.Message, ex); + _trace.ConnectionError(ConnectionId, error); } finally { @@ -239,8 +243,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal { while (true) { - // Wait for data to write from the pipe producer var result = await Output.ReadAsync(); + var buffer = result.Buffer; if (result.IsCanceled) diff --git a/test/Kestrel.Core.Tests/ConnectionDispatcherTests.cs b/test/Kestrel.Core.Tests/ConnectionDispatcherTests.cs index 24f8ded379..c9acd1c658 100644 --- a/test/Kestrel.Core.Tests/ConnectionDispatcherTests.cs +++ b/test/Kestrel.Core.Tests/ConnectionDispatcherTests.cs @@ -1,12 +1,13 @@ -using System; -using System.Buffers; +// 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.Collections.Generic; -using System.IO.Pipelines; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; using Microsoft.AspNetCore.Testing; +using Moq; using Xunit; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests @@ -20,7 +21,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var tcs = new TaskCompletionSource(); var dispatcher = new ConnectionDispatcher(serviceContext, _ => tcs.Task); - var connection = new TransportConnection(); + var connection = Mock.Of(); dispatcher.OnConnection(connection); diff --git a/test/Kestrel.Core.Tests/Http1ConnectionTests.cs b/test/Kestrel.Core.Tests/Http1ConnectionTests.cs index d5237fb3e9..de762a0a7a 100644 --- a/test/Kestrel.Core.Tests/Http1ConnectionTests.cs +++ b/test/Kestrel.Core.Tests/Http1ConnectionTests.cs @@ -11,6 +11,7 @@ using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; @@ -58,6 +59,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests _http1ConnectionContext = new Http1ConnectionContext { ServiceContext = _serviceContext, + ConnectionContext = Mock.Of(), ConnectionFeatures = connectionFeatures, MemoryPool = _pipelineFactory, TimeoutControl = _timeoutControl.Object, diff --git a/test/Kestrel.Core.Tests/HttpConnectionTests.cs b/test/Kestrel.Core.Tests/HttpConnectionTests.cs index 85243bd7ff..091b3cadea 100644 --- a/test/Kestrel.Core.Tests/HttpConnectionTests.cs +++ b/test/Kestrel.Core.Tests/HttpConnectionTests.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.IO.Pipelines; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Core.Adapter.Internal; @@ -38,6 +39,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests _httpConnectionContext = new HttpConnectionContext { ConnectionId = "0123456789", + ConnectionContext = Mock.Of(), ConnectionAdapters = new List(), ConnectionFeatures = connectionFeatures, MemoryPool = _memoryPool, diff --git a/test/Kestrel.Core.Tests/OutputProducerTests.cs b/test/Kestrel.Core.Tests/OutputProducerTests.cs index 3f9750a11e..7f3d566ff5 100644 --- a/test/Kestrel.Core.Tests/OutputProducerTests.cs +++ b/test/Kestrel.Core.Tests/OutputProducerTests.cs @@ -5,6 +5,7 @@ using System; using System.Buffers; using System.IO.Pipelines; using System.Threading; +using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; @@ -61,39 +62,38 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests [Fact] public void AbortsTransportEvenAfterDispose() { - var mockLifetimeFeature = new Mock(); + var mockConnectionContext = new Mock(); - var outputProducer = CreateOutputProducer(lifetimeFeature: mockLifetimeFeature.Object); + var outputProducer = CreateOutputProducer(connectionContext: mockConnectionContext.Object); outputProducer.Dispose(); - mockLifetimeFeature.Verify(f => f.Abort(), Times.Never()); + mockConnectionContext.Verify(f => f.Abort(It.IsAny()), Times.Never()); outputProducer.Abort(null); - mockLifetimeFeature.Verify(f => f.Abort(), Times.Once()); + mockConnectionContext.Verify(f => f.Abort(null), Times.Once()); outputProducer.Abort(null); - mockLifetimeFeature.Verify(f => f.Abort(), Times.Once()); + mockConnectionContext.Verify(f => f.Abort(null), Times.Once()); } private Http1OutputProducer CreateOutputProducer( PipeOptions pipeOptions = null, - IConnectionLifetimeFeature lifetimeFeature = null) + ConnectionContext connectionContext = null) { pipeOptions = pipeOptions ?? new PipeOptions(); - lifetimeFeature = lifetimeFeature ?? Mock.Of(); + connectionContext = connectionContext ?? Mock.Of(); var pipe = new Pipe(pipeOptions); var serviceContext = new TestServiceContext(); var socketOutput = new Http1OutputProducer( - pipe.Reader, pipe.Writer, "0", + connectionContext, serviceContext.Log, Mock.Of(), - lifetimeFeature, Mock.Of()); return socketOutput; diff --git a/test/Kestrel.Core.Tests/TestInput.cs b/test/Kestrel.Core.Tests/TestInput.cs index faa40411c2..b032b334f0 100644 --- a/test/Kestrel.Core.Tests/TestInput.cs +++ b/test/Kestrel.Core.Tests/TestInput.cs @@ -6,6 +6,7 @@ using System.Buffers; using System.IO.Pipelines; using System.Text; using System.Threading.Tasks; +using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; @@ -35,6 +36,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests Http1ConnectionContext = new Http1ConnectionContext { ServiceContext = new TestServiceContext(), + ConnectionContext = Mock.Of(), ConnectionFeatures = connectionFeatures, Application = Application, Transport = Transport, diff --git a/test/Kestrel.FunctionalTests/RequestTests.cs b/test/Kestrel.FunctionalTests/RequestTests.cs index 236629ae1e..4ed1d248b8 100644 --- a/test/Kestrel.FunctionalTests/RequestTests.cs +++ b/test/Kestrel.FunctionalTests/RequestTests.cs @@ -1131,6 +1131,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests [MemberData(nameof(ConnectionAdapterData))] public async Task RequestsCanBeAbortedMidRead(ListenOptions listenOptions) { + const int applicationAbortedConnectionId = 34; + var testContext = new TestServiceContext(LoggerFactory); var readTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -1205,6 +1207,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests // The cancellation token for only the last request should be triggered. var abortedRequestId = await registrationTcs.Task; Assert.Equal(2, abortedRequestId); + + Assert.Single(TestSink.Writes.Where(w => w.LoggerName == "Microsoft.AspNetCore.Server.Kestrel" && + w.EventId == applicationAbortedConnectionId)); } [Theory] diff --git a/test/Kestrel.Transport.Libuv.Tests/LibuvOutputConsumerTests.cs b/test/Kestrel.Transport.Libuv.Tests/LibuvOutputConsumerTests.cs index 89a16caa0d..d2f6a4fb89 100644 --- a/test/Kestrel.Transport.Libuv.Tests/LibuvOutputConsumerTests.cs +++ b/test/Kestrel.Transport.Libuv.Tests/LibuvOutputConsumerTests.cs @@ -7,6 +7,7 @@ using System.Collections.Concurrent; using System.IO.Pipelines; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Core; @@ -738,6 +739,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests var http1Connection = new Http1Connection(new Http1ConnectionContext { ServiceContext = serviceContext, + ConnectionContext = Mock.Of(), ConnectionFeatures = connectionFeatures, MemoryPool = _memoryPool, TimeoutControl = Mock.Of(), @@ -764,12 +766,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests // Without ConfigureAwait(false), xunit will dispatch. await consumer.WriteOutputAsync().ConfigureAwait(false); - http1Connection.Abort(error: null); + http1Connection.Abort(abortReason: null); outputReader.Complete(); } catch (UvException ex) { - http1Connection.Abort(ex); + http1Connection.Abort(new ConnectionAbortedException(ex.Message, ex)); outputReader.Complete(ex); } } diff --git a/test/shared/CompositeKestrelTrace.cs b/test/shared/CompositeKestrelTrace.cs index 8a7a040d42..33110c85d2 100644 --- a/test/shared/CompositeKestrelTrace.cs +++ b/test/shared/CompositeKestrelTrace.cs @@ -166,6 +166,12 @@ namespace Microsoft.AspNetCore.Testing _trace2.ResponseMininumDataRateNotSatisfied(connectionId, traceIdentifier); } + public void ApplicationAbortedConnection(string connectionId, string traceIdentifier) + { + _trace1.ApplicationAbortedConnection(connectionId, traceIdentifier); + _trace2.ApplicationAbortedConnection(connectionId, traceIdentifier); + } + public void Http2ConnectionError(string connectionId, Http2ConnectionErrorException ex) { _trace1.Http2ConnectionError(connectionId, ex);