diff --git a/src/Servers/Connections.Abstractions/ref/Microsoft.AspNetCore.Connections.Abstractions.netstandard2.0.cs b/src/Servers/Connections.Abstractions/ref/Microsoft.AspNetCore.Connections.Abstractions.netstandard2.0.cs index 6ae8b7206a..1189ad64cf 100644 --- a/src/Servers/Connections.Abstractions/ref/Microsoft.AspNetCore.Connections.Abstractions.netstandard2.0.cs +++ b/src/Servers/Connections.Abstractions/ref/Microsoft.AspNetCore.Connections.Abstractions.netstandard2.0.cs @@ -100,6 +100,10 @@ namespace Microsoft.AspNetCore.Connections } namespace Microsoft.AspNetCore.Connections.Features { + public partial interface IConnectionCompleteFeature + { + void OnCompleted(System.Func callback, object state); + } public partial interface IConnectionHeartbeatFeature { void OnHeartbeat(System.Action action, object state); diff --git a/src/Servers/Connections.Abstractions/src/Features/IConnectionCompleteFeature.cs b/src/Servers/Connections.Abstractions/src/Features/IConnectionCompleteFeature.cs new file mode 100644 index 0000000000..94587ae6e8 --- /dev/null +++ b/src/Servers/Connections.Abstractions/src/Features/IConnectionCompleteFeature.cs @@ -0,0 +1,22 @@ +// 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.Threading.Tasks; + +namespace Microsoft.AspNetCore.Connections.Features +{ + /// + /// Represents the completion action for a connection. + /// + public interface IConnectionCompleteFeature + { + /// + /// Registers a callback to be invoked after a connection has fully completed processing. This is + /// intended for resource cleanup. + /// + /// The callback to invoke after the connection has completed processing. + /// The state to pass into the callback. + void OnCompleted(Func callback, object state); + } +} diff --git a/src/Servers/Kestrel/Core/src/Internal/ConnectionDispatcher.cs b/src/Servers/Kestrel/Core/src/Internal/ConnectionDispatcher.cs index 0e0108417b..0c0cca1573 100644 --- a/src/Servers/Kestrel/Core/src/Internal/ConnectionDispatcher.cs +++ b/src/Servers/Kestrel/Core/src/Internal/ConnectionDispatcher.cs @@ -82,6 +82,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal } finally { + await connectionContext.CompleteAsync(); + Log.ConnectionStop(connectionContext.ConnectionId); KestrelEventSource.Log.ConnectionStop(connectionContext); diff --git a/src/Servers/Kestrel/Core/test/ConnectionDispatcherTests.cs b/src/Servers/Kestrel/Core/test/ConnectionDispatcherTests.cs index efc53c9228..1d1c3b46e3 100644 --- a/src/Servers/Kestrel/Core/test/ConnectionDispatcherTests.cs +++ b/src/Servers/Kestrel/Core/test/ConnectionDispatcherTests.cs @@ -7,9 +7,11 @@ using System.IO.Pipelines; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.Logging; using Moq; using Xunit; @@ -69,5 +71,50 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests mockPipeWriter.Verify(m => m.Complete(It.IsAny()), Times.Once()); mockPipeReader.Verify(m => m.Complete(It.IsAny()), Times.Once()); } + + [Fact] + public async Task OnConnectionFiresOnCompleted() + { + var serviceContext = new TestServiceContext(); + var dispatcher = new ConnectionDispatcher(serviceContext, _ => Task.CompletedTask); + + var connection = new Mock { CallBase = true }.Object; + connection.ConnectionClosed = new CancellationToken(canceled: true); + var completeFeature = connection.Features.Get(); + + Assert.NotNull(completeFeature); + object stateObject = new object(); + object callbackState = null; + completeFeature.OnCompleted(state => { callbackState = state; return Task.CompletedTask; }, stateObject); + + await dispatcher.OnConnection(connection); + + Assert.Equal(stateObject, callbackState); + } + + [Fact] + public async Task OnConnectionOnCompletedExceptionCaught() + { + var serviceContext = new TestServiceContext(); + var dispatcher = new ConnectionDispatcher(serviceContext, _ => Task.CompletedTask); + + var connection = new Mock { CallBase = true }.Object; + connection.ConnectionClosed = new CancellationToken(canceled: true); + var completeFeature = connection.Features.Get(); + var mockLogger = new Mock(); + connection.Logger = mockLogger.Object; + + Assert.NotNull(completeFeature); + object stateObject = new object(); + object callbackState = null; + completeFeature.OnCompleted(state => { callbackState = state; throw new InvalidTimeZoneException(); }, stateObject); + + await dispatcher.OnConnection(connection); + + Assert.Equal(stateObject, callbackState); + var log = mockLogger.Invocations.First(); + Assert.Equal("An error occured running an IConnectionCompleteFeature.OnCompleted callback.", log.Arguments[2].ToString()); + Assert.IsType(log.Arguments[3]); + } } } diff --git a/src/Servers/Kestrel/Transport.Abstractions/ref/Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.csproj b/src/Servers/Kestrel/Transport.Abstractions/ref/Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.csproj index 8bf0f0c690..ea00244772 100644 --- a/src/Servers/Kestrel/Transport.Abstractions/ref/Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.csproj +++ b/src/Servers/Kestrel/Transport.Abstractions/ref/Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.csproj @@ -6,5 +6,6 @@ + diff --git a/src/Servers/Kestrel/Transport.Abstractions/ref/Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.netcoreapp3.0.cs b/src/Servers/Kestrel/Transport.Abstractions/ref/Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.netcoreapp3.0.cs index bacbaaadba..5420d07eac 100644 --- a/src/Servers/Kestrel/Transport.Abstractions/ref/Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.netcoreapp3.0.cs +++ b/src/Servers/Kestrel/Transport.Abstractions/ref/Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.netcoreapp3.0.cs @@ -59,7 +59,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal ThreadPool = 1, Inline = 2, } - public abstract partial class TransportConnection : Microsoft.AspNetCore.Connections.ConnectionContext, Microsoft.AspNetCore.Connections.Features.IConnectionHeartbeatFeature, Microsoft.AspNetCore.Connections.Features.IConnectionIdFeature, Microsoft.AspNetCore.Connections.Features.IConnectionItemsFeature, Microsoft.AspNetCore.Connections.Features.IConnectionLifetimeFeature, Microsoft.AspNetCore.Connections.Features.IConnectionLifetimeNotificationFeature, Microsoft.AspNetCore.Connections.Features.IConnectionTransportFeature, Microsoft.AspNetCore.Connections.Features.IMemoryPoolFeature, Microsoft.AspNetCore.Http.Features.IFeatureCollection, Microsoft.AspNetCore.Http.Features.IHttpConnectionFeature, Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal.IApplicationTransportFeature, Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal.ITransportSchedulerFeature, System.Collections.Generic.IEnumerable>, System.Collections.IEnumerable + public abstract partial class TransportConnection : Microsoft.AspNetCore.Connections.ConnectionContext, Microsoft.AspNetCore.Connections.Features.IConnectionCompleteFeature, Microsoft.AspNetCore.Connections.Features.IConnectionHeartbeatFeature, Microsoft.AspNetCore.Connections.Features.IConnectionIdFeature, Microsoft.AspNetCore.Connections.Features.IConnectionItemsFeature, Microsoft.AspNetCore.Connections.Features.IConnectionLifetimeFeature, Microsoft.AspNetCore.Connections.Features.IConnectionLifetimeNotificationFeature, Microsoft.AspNetCore.Connections.Features.IConnectionTransportFeature, Microsoft.AspNetCore.Connections.Features.IMemoryPoolFeature, Microsoft.AspNetCore.Http.Features.IFeatureCollection, Microsoft.AspNetCore.Http.Features.IHttpConnectionFeature, Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal.IApplicationTransportFeature, Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal.ITransportSchedulerFeature, System.Collections.Generic.IEnumerable>, System.Collections.IEnumerable { protected readonly System.Threading.CancellationTokenSource _connectionClosingCts; public TransportConnection() { } @@ -73,6 +73,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal public override System.Collections.Generic.IDictionary Items { get { throw null; } set { } } public System.Net.IPAddress LocalAddress { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } public int LocalPort { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + protected internal virtual Microsoft.Extensions.Logging.ILogger Logger { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } public virtual System.Buffers.MemoryPool MemoryPool { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } System.Collections.Generic.IDictionary Microsoft.AspNetCore.Connections.Features.IConnectionItemsFeature.Items { get { throw null; } set { } } System.Threading.CancellationToken Microsoft.AspNetCore.Connections.Features.IConnectionLifetimeFeature.ConnectionClosed { get { throw null; } set { } } @@ -96,6 +97,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal public int RemotePort { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } public override System.IO.Pipelines.IDuplexPipe Transport { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } public override void Abort(Microsoft.AspNetCore.Connections.ConnectionAbortedException abortReason) { } + public System.Threading.Tasks.Task CompleteAsync() { throw null; } + void Microsoft.AspNetCore.Connections.Features.IConnectionCompleteFeature.OnCompleted(System.Func callback, object state) { } void Microsoft.AspNetCore.Connections.Features.IConnectionHeartbeatFeature.OnHeartbeat(System.Action action, object state) { } void Microsoft.AspNetCore.Connections.Features.IConnectionLifetimeFeature.Abort() { } void Microsoft.AspNetCore.Connections.Features.IConnectionLifetimeNotificationFeature.RequestClose() { } diff --git a/src/Servers/Kestrel/Transport.Abstractions/src/Internal/TransportConnection.FeatureCollection.cs b/src/Servers/Kestrel/Transport.Abstractions/src/Internal/TransportConnection.FeatureCollection.cs index 2a07874ab7..7f98162507 100644 --- a/src/Servers/Kestrel/Transport.Abstractions/src/Internal/TransportConnection.FeatureCollection.cs +++ b/src/Servers/Kestrel/Transport.Abstractions/src/Internal/TransportConnection.FeatureCollection.cs @@ -1,14 +1,17 @@ -// 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. +using System; using System.Buffers; using System.Collections.Generic; 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.Http.Features; +using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal { @@ -21,12 +24,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal ITransportSchedulerFeature, IConnectionLifetimeFeature, IConnectionHeartbeatFeature, - IConnectionLifetimeNotificationFeature + IConnectionLifetimeNotificationFeature, + IConnectionCompleteFeature { // 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. // See also: tools/CodeGenerator/TransportConnectionFeatureCollection.cs + private Stack, object>> _onCompleted; + private bool _completed; + string IHttpConnectionFeature.ConnectionId { get => ConnectionId; @@ -100,5 +107,82 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal { OnHeartbeat(action, state); } + + void IConnectionCompleteFeature.OnCompleted(Func callback, object state) + { + if (_completed) + { + throw new InvalidOperationException("The connection is already complete."); + } + + if (_onCompleted == null) + { + _onCompleted = new Stack, object>>(); + } + _onCompleted.Push(new KeyValuePair, object>(callback, state)); + } + + public Task CompleteAsync() + { + if (_completed) + { + throw new InvalidOperationException("The connection is already complete."); + } + + _completed = true; + var onCompleted = _onCompleted; + + if (onCompleted == null || onCompleted.Count == 0) + { + return Task.CompletedTask; + } + + return CompleteAsyncMayAwait(onCompleted); + } + + private Task CompleteAsyncMayAwait(Stack, object>> onCompleted) + { + while (onCompleted.TryPop(out var entry)) + { + try + { + var task = entry.Key.Invoke(entry.Value); + if (!ReferenceEquals(task, Task.CompletedTask)) + { + return CompleteAsyncAwaited(task, onCompleted); + } + } + catch (Exception ex) + { + Logger?.LogError(ex, "An error occured running an IConnectionCompleteFeature.OnCompleted callback."); + } + } + + return Task.CompletedTask; + } + + private async Task CompleteAsyncAwaited(Task currentTask, Stack, object>> onCompleted) + { + try + { + await currentTask; + } + catch (Exception ex) + { + Logger?.LogError(ex, "An error occured running an IConnectionCompleteFeature.OnCompleted callback."); + } + + while (onCompleted.TryPop(out var entry)) + { + try + { + await entry.Key.Invoke(entry.Value); + } + catch (Exception ex) + { + Logger?.LogError(ex, "An error occured running an IConnectionCompleteFeature.OnCompleted callback."); + } + } + } } } diff --git a/src/Servers/Kestrel/Transport.Abstractions/src/Internal/TransportConnection.Generated.cs b/src/Servers/Kestrel/Transport.Abstractions/src/Internal/TransportConnection.Generated.cs index 5eba177a89..b5d0122ffb 100644 --- a/src/Servers/Kestrel/Transport.Abstractions/src/Internal/TransportConnection.Generated.cs +++ b/src/Servers/Kestrel/Transport.Abstractions/src/Internal/TransportConnection.Generated.cs @@ -22,6 +22,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal private static readonly Type IConnectionLifetimeFeatureType = typeof(IConnectionLifetimeFeature); private static readonly Type IConnectionHeartbeatFeatureType = typeof(IConnectionHeartbeatFeature); private static readonly Type IConnectionLifetimeNotificationFeatureType = typeof(IConnectionLifetimeNotificationFeature); + private static readonly Type IConnectionCompleteFeatureType = typeof(IConnectionCompleteFeature); private object _currentIHttpConnectionFeature; private object _currentIConnectionIdFeature; @@ -33,6 +34,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal private object _currentIConnectionLifetimeFeature; private object _currentIConnectionHeartbeatFeature; private object _currentIConnectionLifetimeNotificationFeature; + private object _currentIConnectionCompleteFeature; private int _featureRevision; @@ -50,6 +52,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal _currentIConnectionLifetimeFeature = this; _currentIConnectionHeartbeatFeature = this; _currentIConnectionLifetimeNotificationFeature = this; + _currentIConnectionCompleteFeature = this; } @@ -145,6 +148,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal { feature = _currentIConnectionLifetimeNotificationFeature; } + else if (key == IConnectionCompleteFeatureType) + { + feature = _currentIConnectionCompleteFeature; + } else if (MaybeExtra != null) { feature = ExtraFeatureGet(key); @@ -197,6 +204,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal { _currentIConnectionLifetimeNotificationFeature = value; } + else if (key == IConnectionCompleteFeatureType) + { + _currentIConnectionCompleteFeature = value; + } else { ExtraFeatureSet(key, value); @@ -247,6 +258,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal { feature = (TFeature)_currentIConnectionLifetimeNotificationFeature; } + else if (typeof(TFeature) == typeof(IConnectionCompleteFeature)) + { + feature = (TFeature)_currentIConnectionCompleteFeature; + } else if (MaybeExtra != null) { feature = (TFeature)(ExtraFeatureGet(typeof(TFeature))); @@ -298,6 +313,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal { _currentIConnectionLifetimeNotificationFeature = feature; } + else if (typeof(TFeature) == typeof(IConnectionCompleteFeature)) + { + _currentIConnectionCompleteFeature = feature; + } else { ExtraFeatureSet(typeof(TFeature), feature); @@ -346,6 +365,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal { yield return new KeyValuePair(IConnectionLifetimeNotificationFeatureType, _currentIConnectionLifetimeNotificationFeature); } + if (_currentIConnectionCompleteFeature != null) + { + yield return new KeyValuePair(IConnectionCompleteFeatureType, _currentIConnectionCompleteFeature); + } if (MaybeExtra != null) { diff --git a/src/Servers/Kestrel/Transport.Abstractions/src/Internal/TransportConnection.cs b/src/Servers/Kestrel/Transport.Abstractions/src/Internal/TransportConnection.cs index bc9a8b27a0..e3d012cc27 100644 --- a/src/Servers/Kestrel/Transport.Abstractions/src/Internal/TransportConnection.cs +++ b/src/Servers/Kestrel/Transport.Abstractions/src/Internal/TransportConnection.cs @@ -1,4 +1,4 @@ -// 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. using System; @@ -9,6 +9,7 @@ using System.Net; using System.Threading; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal { @@ -35,6 +36,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal public override IFeatureCollection Features => this; + protected internal virtual ILogger Logger { get; set; } + public virtual MemoryPool MemoryPool { get; } public virtual PipeScheduler InputWriterScheduler { get; } public virtual PipeScheduler OutputReaderScheduler { get; } diff --git a/src/Servers/Kestrel/Transport.Abstractions/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.csproj b/src/Servers/Kestrel/Transport.Abstractions/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.csproj index 0d812732eb..2cf09c60d0 100644 --- a/src/Servers/Kestrel/Transport.Abstractions/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.csproj +++ b/src/Servers/Kestrel/Transport.Abstractions/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.csproj @@ -17,6 +17,7 @@ + diff --git a/src/Servers/Kestrel/Transport.Libuv/src/Internal/LibuvConnection.cs b/src/Servers/Kestrel/Transport.Libuv/src/Internal/LibuvConnection.cs index b0f61c8023..adcc5cd09d 100644 --- a/src/Servers/Kestrel/Transport.Libuv/src/Internal/LibuvConnection.cs +++ b/src/Servers/Kestrel/Transport.Libuv/src/Internal/LibuvConnection.cs @@ -43,6 +43,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal LocalPort = localEndPoint?.Port ?? 0; ConnectionClosed = _connectionClosedTokenSource.Token; + Logger = log; Log = log; Thread = thread; } diff --git a/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketConnection.cs b/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketConnection.cs index 3a52cf5358..bfb467f94a 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketConnection.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketConnection.cs @@ -42,6 +42,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal _socket = socket; MemoryPool = memoryPool; _scheduler = scheduler; + Logger = trace; _trace = trace; var localEndPoint = (IPEndPoint)_socket.LocalEndPoint; diff --git a/src/Servers/Kestrel/tools/CodeGenerator/TransportConnectionFeatureCollection.cs b/src/Servers/Kestrel/tools/CodeGenerator/TransportConnectionFeatureCollection.cs index 251af12843..93d0339a53 100644 --- a/src/Servers/Kestrel/tools/CodeGenerator/TransportConnectionFeatureCollection.cs +++ b/src/Servers/Kestrel/tools/CodeGenerator/TransportConnectionFeatureCollection.cs @@ -1,4 +1,4 @@ -// 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. namespace CodeGenerator @@ -20,7 +20,8 @@ namespace CodeGenerator "ITransportSchedulerFeature", "IConnectionLifetimeFeature", "IConnectionHeartbeatFeature", - "IConnectionLifetimeNotificationFeature" + "IConnectionLifetimeNotificationFeature", + "IConnectionCompleteFeature" }; var usings = $@"