From 04bf1bf32e13d35d4d7f61f9cbeb9bfa22e0c617 Mon Sep 17 00:00:00 2001 From: David Fowler Date: Thu, 30 May 2019 20:34:26 -0700 Subject: [PATCH] Implement new bedrock listener abstraction and re-plat Kestrel on top (#10321) This is a massive set of changes to Kestrel to remove the existing pubternal transport layer and implement a public facing API for listeners and clients, see the details here #10308. This change only has the server side pieces of the story as I don't want to add the client APIs without having ported SignalR to use them. Here are the highlights: - Transport.Abstractions is empty (will be removed in a separate PR as it requires removing it from a ton of places) - TransportConnection has been moved to Connection.Abstractions (we can decide if we need to consolidate with DefaultConnectionContext in a later PR) - Added FileHandleEndPoint which allows binding to a file handle (could be a pipe or tcp handle) ListenOptions has been gutted for most pubternal API and returns various types of binding information . The source of truth is the EndPoint instance. - Cleaned up a bunch of libuv tests decoupling them from Kestrel.Core ## Breaking changes - Removing pubternal API is itself a breaking change but one that we already planned to do. - We've removed the ability to set the scheduling mode on Kestrel - DisposeAsync was added to ConnectionContext - NoDelay was removed from ListenOptions. This has been moved to each of the transports. One major difference though is that it's no longer localized per endpoint but is global. We'd need a derived EndPoint type (or maybe extend IPEndPoint) to store both the socket options and the binding information. --- ...Connections.Abstractions.netstandard2.0.cs | 65 ++++- .../src/ConnectionContext.cs | 14 + .../src/ConnectionDelegate.cs | 5 +- .../src/DefaultConnectionContext.cs | 16 +- .../Features/IConnectionEndpointFeature.cs | 13 + .../src/Features/IConnectionUserFeature.cs | 5 +- .../src/FileHandleEndPoint.cs | 30 +++ .../src}/FileHandleType.cs | 6 +- .../src/IConnectionBuilder.cs | 5 +- .../src/IConnectionListener.cs | 20 ++ .../src/IConnectionListenerFactory.cs | 17 ++ .../TransportConnection.FeatureCollection.cs | 44 ++++ .../src}/TransportConnection.Generated.cs | 148 +---------- .../src/TransportConnection.cs | 62 +++++ ...soft.AspNetCore.Server.Kestrel.Core.csproj | 2 +- ...tCore.Server.Kestrel.Core.netcoreapp3.0.cs | 15 +- .../src/Adapter/Internal/AdaptedPipeline.cs | 9 +- .../Kestrel/Core/src/AnyIPListenOptions.cs | 4 +- .../Core/src/Internal/ConnectionDispatcher.cs | 127 ++++----- .../Http/Http1ChunkedEncodingMessageBody.cs | 3 +- .../Http/HttpProtocol.FeatureCollection.cs | 7 +- .../src/Internal/Http2/Http2OutputProducer.cs | 3 +- .../Core/src/Internal/Http2/Http2Stream.cs | 3 +- .../Core/src/Internal/HttpConnection.cs | 8 +- .../src/Internal/HttpConnectionMiddleware.cs | 16 +- .../Infrastructure/ConnectionManager.cs | 53 +++- .../ConnectionManagerShutdownExtensions.cs | 53 ---- .../Infrastructure/HeartbeatManager.cs | 4 +- .../Infrastructure/KestrelConnection.cs | 163 +++++++++++- .../Infrastructure/KestrelEventSource.cs | 11 +- .../Core/src/Internal/MemoryPoolExtensions.cs | 31 +++ src/Servers/Kestrel/Core/src/KestrelServer.cs | 53 ++-- .../Kestrel/Core/src/KestrelServerOptions.cs | 9 - src/Servers/Kestrel/Core/src/ListenOptions.cs | 91 ++----- .../Core/src/LocalhostListenOptions.cs | 4 +- ...soft.AspNetCore.Server.Kestrel.Core.csproj | 2 +- .../Kestrel/Core/test/AddressBinderTests.cs | 18 +- .../Core/test/ConnectionDispatcherTests.cs | 62 +++-- .../Kestrel/Core/test/Http1ConnectionTests.cs | 3 +- .../Core/test/HttpConnectionManagerTests.cs | 7 +- .../Core/test/HttpResponseHeadersTests.cs | 4 +- .../Core/test/KestrelServerOptionsTests.cs | 33 +-- .../Kestrel/Core/test/KestrelServerTests.cs | 132 +++++----- ...spNetCore.Server.Kestrel.Core.Tests.csproj | 4 +- .../Kestrel/Core/test/OutputProducerTests.cs | 3 +- .../Kestrel/Core/test/PipeOptionsTests.cs | 40 +-- .../Core/test/PipelineExtensionTests.cs | 3 +- .../Kestrel/Core/test/StartLineTests.cs | 3 +- .../Core/test/TestHelpers/TestInput.cs | 3 +- .../src/WebHostBuilderKestrelExtensions.cs | 4 +- .../test/KestrelConfigurationBuilderTests.cs | 6 - ...oft.AspNetCore.Server.Kestrel.Tests.csproj | 5 +- .../WebHostBuilderKestrelExtensionsTests.cs | 12 +- ...el.Transport.Abstractions.netcoreapp3.0.cs | 110 -------- .../Internal/IApplicationTransportFeature.cs | 12 - .../src/Internal/IConnectionDispatcher.cs | 12 - .../src/Internal/IEndPointInformation.cs | 46 ---- .../src/Internal/ITransport.cs | 15 -- .../src/Internal/ITransportFactory.cs | 10 - .../Internal/ITransportSchedulerFeature.cs | 14 - .../src/Internal/ListenType.cs | 15 -- .../src/Internal/SchedulingMode.cs | 12 - .../TransportConnection.FeatureCollection.cs | 188 ------------- .../src/Internal/TransportConnection.cs | 121 --------- ...rver.Kestrel.Transport.Abstractions.csproj | 2 +- ...Core.Server.Kestrel.Transport.Libuv.csproj | 3 +- ...r.Kestrel.Transport.Libuv.netcoreapp3.0.cs | 3 + .../src/Internal/LibuvConnection.cs | 82 ++++-- ...ransport.cs => LibuvConnectionListener.cs} | 133 ++++++++-- .../src/Internal/LibuvThread.cs | 37 +-- .../src/Internal/LibuvTransportContext.cs | 3 - .../src/Internal/LibuvTransportFactory.cs | 18 +- .../Transport.Libuv/src/Internal/Listener.cs | 44 ++-- .../src/Internal/ListenerContext.cs | 84 ++++-- .../src/Internal/ListenerPrimary.cs | 6 +- .../src/Internal/ListenerSecondary.cs | 14 +- .../src/LibuvTransportOptions.cs | 18 +- ...Core.Server.Kestrel.Transport.Libuv.csproj | 10 +- .../src/WebHostBuilderLibuvExtensions.cs | 6 +- .../test/LibuvConnectionTests.cs | 160 +++++------- .../test/LibuvOutputConsumerTests.cs | 45 +++- .../Transport.Libuv/test/LibuvThreadTests.cs | 6 +- .../test/LibuvTransportTests.cs | 169 +++++++++--- .../test/ListenerPrimaryTests.cs | 247 ++++++++---------- .../TestHelpers/MockConnectionDispatcher.cs | 31 --- .../TestHelpers/TestLibuvTransportContext.cs | 1 - ...re.Server.Kestrel.Transport.Sockets.csproj | 2 +- ...Kestrel.Transport.Sockets.netcoreapp3.0.cs | 9 +- .../src/Internal/SocketConnection.cs | 85 ++++-- ...re.Server.Kestrel.Transport.Sockets.csproj | 7 +- .../src/SocketConnectionListener.cs | 139 ++++++++++ .../Transport.Sockets/src/SocketTransport.cs | 205 --------------- .../src/SocketTransportFactory.cs | 46 +--- .../src/SocketTransportOptions.cs | 18 +- .../src/WebHostBuilderSocketExtensions.cs | 6 +- .../ChunkWriterBenchmark.cs | 3 +- .../Http1ConnectionBenchmark.cs | 3 +- ...Http1ConnectionParsingOverheadBenchmark.cs | 3 +- .../Http1ReadingBenchmark.cs | 3 +- .../Http1WritingBenchmark.cs | 3 +- .../HttpProtocolFeatureCollection.cs | 6 +- .../InMemoryTransportBenchmark.cs | 66 +++-- ...pNetCore.Server.Kestrel.Performance.csproj | 1 + .../PipeThroughputBenchmark.cs | 5 +- .../RequestParsingBenchmark.cs | 3 +- .../ResponseHeaderCollectionBenchmark.cs | 4 +- .../Kestrel/samples/Http2SampleApp/Program.cs | 4 - .../samples/PlaintextApp/PlaintextApp.csproj | 3 +- .../Kestrel/samples/PlaintextApp/Startup.cs | 5 +- .../Kestrel/samples/SampleApp/Startup.cs | 11 +- .../Kestrel/samples/SystemdTestApp/Startup.cs | 6 +- .../shared/test/TaskTimeoutExtensions.cs | 10 + .../Kestrel/shared/test/TestServiceContext.cs | 3 +- .../DiagnosticMemoryPoolFactory.cs | 7 +- .../test/TransportTestHelpers/TestServer.cs | 6 +- .../MaxRequestBufferSizeTests.cs | 2 +- .../ChunkedRequestTests.cs | 2 +- .../Http2/Http2TestBase.cs | 42 ++- .../InMemory.FunctionalTests.csproj | 3 + .../KeepAliveTimeoutTests.cs | 15 ++ .../RequestHeadersTimeoutTests.cs | 8 + .../InMemoryTransportConnection.cs | 162 +++++++++++- .../TestTransport/InMemoryTransportFactory.cs | 61 +++-- .../TestTransport/TestServer.cs | 34 +-- .../TransportSelector.cs | 11 +- .../TransportSelector.cs | 11 +- .../tools/CodeGenerator/CodeGenerator.csproj | 2 +- .../TransportConnectionFeatureCollection.cs | 10 +- .../Buffers.MemoryPool/MemoryPoolFactory.cs} | 9 +- .../Buffers.MemoryPool/SlabMemoryPool.cs | 5 + src/SignalR/SignalR.sln | 7 + .../Client/src/HttpConnectionFactory.cs | 4 +- .../FunctionalTests/HubConnectionTests.cs | 2 +- .../HubConnectionTests.ConnectionLifecycle.cs | 6 +- .../UnitTests/HubConnectionTests.Helpers.cs | 2 +- .../test/UnitTests/HubConnectionTests.cs | 2 +- .../Client/test/UnitTests/TestConnection.cs | 4 +- ...e.Http.Connections.Client.netcoreapp3.0.cs | 2 +- ....Http.Connections.Client.netstandard2.0.cs | 2 +- .../src/HttpConnection.cs | 2 +- .../testassets/Tests.Utils/TaskExtensions.cs | 10 + .../samples/ClientSample/Tcp/TcpConnection.cs | 4 +- .../Tcp/TcpHubConnectionBuilderExtensions.cs | 2 +- 143 files changed, 2128 insertions(+), 2110 deletions(-) create mode 100644 src/Servers/Connections.Abstractions/src/Features/IConnectionEndpointFeature.cs create mode 100644 src/Servers/Connections.Abstractions/src/FileHandleEndPoint.cs rename src/Servers/{Kestrel/Transport.Abstractions/src/Internal => Connections.Abstractions/src}/FileHandleType.cs (52%) create mode 100644 src/Servers/Connections.Abstractions/src/IConnectionListener.cs create mode 100644 src/Servers/Connections.Abstractions/src/IConnectionListenerFactory.cs create mode 100644 src/Servers/Connections.Abstractions/src/TransportConnection.FeatureCollection.cs rename src/Servers/{Kestrel/Transport.Abstractions/src/Internal => Connections.Abstractions/src}/TransportConnection.Generated.cs (54%) create mode 100644 src/Servers/Connections.Abstractions/src/TransportConnection.cs delete mode 100644 src/Servers/Kestrel/Core/src/Internal/Infrastructure/ConnectionManagerShutdownExtensions.cs create mode 100644 src/Servers/Kestrel/Core/src/Internal/MemoryPoolExtensions.cs delete mode 100644 src/Servers/Kestrel/Transport.Abstractions/src/Internal/IApplicationTransportFeature.cs delete mode 100644 src/Servers/Kestrel/Transport.Abstractions/src/Internal/IConnectionDispatcher.cs delete mode 100644 src/Servers/Kestrel/Transport.Abstractions/src/Internal/IEndPointInformation.cs delete mode 100644 src/Servers/Kestrel/Transport.Abstractions/src/Internal/ITransport.cs delete mode 100644 src/Servers/Kestrel/Transport.Abstractions/src/Internal/ITransportFactory.cs delete mode 100644 src/Servers/Kestrel/Transport.Abstractions/src/Internal/ITransportSchedulerFeature.cs delete mode 100644 src/Servers/Kestrel/Transport.Abstractions/src/Internal/ListenType.cs delete mode 100644 src/Servers/Kestrel/Transport.Abstractions/src/Internal/SchedulingMode.cs delete mode 100644 src/Servers/Kestrel/Transport.Abstractions/src/Internal/TransportConnection.FeatureCollection.cs delete mode 100644 src/Servers/Kestrel/Transport.Abstractions/src/Internal/TransportConnection.cs rename src/Servers/Kestrel/Transport.Libuv/src/Internal/{LibuvTransport.cs => LibuvConnectionListener.cs} (51%) delete mode 100644 src/Servers/Kestrel/Transport.Libuv/test/TestHelpers/MockConnectionDispatcher.cs create mode 100644 src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionListener.cs delete mode 100644 src/Servers/Kestrel/Transport.Sockets/src/SocketTransport.cs rename src/{Servers/Kestrel/Transport.Abstractions/src/Internal/KestrelMemoryPool.cs => Shared/Buffers.MemoryPool/MemoryPoolFactory.cs} (69%) 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 1189ad64cf..bb446f937d 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 @@ -30,12 +30,16 @@ namespace Microsoft.AspNetCore.Connections public abstract partial class ConnectionContext { protected ConnectionContext() { } + public virtual System.Threading.CancellationToken ConnectionClosed { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } public abstract string ConnectionId { get; set; } public abstract Microsoft.AspNetCore.Http.Features.IFeatureCollection Features { get; } public abstract System.Collections.Generic.IDictionary Items { get; set; } + public virtual System.Net.EndPoint LocalEndPoint { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + public virtual System.Net.EndPoint RemoteEndPoint { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } public abstract System.IO.Pipelines.IDuplexPipe Transport { get; set; } public virtual void Abort() { } public virtual void Abort(Microsoft.AspNetCore.Connections.ConnectionAbortedException abortReason) { } + public virtual System.Threading.Tasks.ValueTask DisposeAsync() { throw null; } } public delegate System.Threading.Tasks.Task ConnectionDelegate(Microsoft.AspNetCore.Connections.ConnectionContext connection); public abstract partial class ConnectionHandler @@ -70,20 +74,35 @@ namespace Microsoft.AspNetCore.Connections public ConnectionResetException(string message) { } public ConnectionResetException(string message, System.Exception inner) { } } - public partial class DefaultConnectionContext : Microsoft.AspNetCore.Connections.ConnectionContext, Microsoft.AspNetCore.Connections.Features.IConnectionIdFeature, Microsoft.AspNetCore.Connections.Features.IConnectionItemsFeature, Microsoft.AspNetCore.Connections.Features.IConnectionLifetimeFeature, Microsoft.AspNetCore.Connections.Features.IConnectionTransportFeature, Microsoft.AspNetCore.Connections.Features.IConnectionUserFeature, System.IDisposable + public partial class DefaultConnectionContext : Microsoft.AspNetCore.Connections.ConnectionContext, Microsoft.AspNetCore.Connections.Features.IConnectionEndPointFeature, Microsoft.AspNetCore.Connections.Features.IConnectionIdFeature, Microsoft.AspNetCore.Connections.Features.IConnectionItemsFeature, Microsoft.AspNetCore.Connections.Features.IConnectionLifetimeFeature, Microsoft.AspNetCore.Connections.Features.IConnectionTransportFeature, Microsoft.AspNetCore.Connections.Features.IConnectionUserFeature, System.IDisposable { public DefaultConnectionContext() { } public DefaultConnectionContext(string id) { } public DefaultConnectionContext(string id, System.IO.Pipelines.IDuplexPipe transport, System.IO.Pipelines.IDuplexPipe application) { } public System.IO.Pipelines.IDuplexPipe Application { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } - public System.Threading.CancellationToken ConnectionClosed { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + public override System.Threading.CancellationToken ConnectionClosed { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } public override string ConnectionId { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } public override Microsoft.AspNetCore.Http.Features.IFeatureCollection Features { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } public override System.Collections.Generic.IDictionary Items { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + public override System.Net.EndPoint LocalEndPoint { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + public override System.Net.EndPoint RemoteEndPoint { [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 System.Security.Claims.ClaimsPrincipal User { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } public override void Abort(Microsoft.AspNetCore.Connections.ConnectionAbortedException abortReason) { } public void Dispose() { } + public override System.Threading.Tasks.ValueTask DisposeAsync() { throw null; } + } + public partial class FileHandleEndPoint : System.Net.EndPoint + { + public FileHandleEndPoint(ulong fileHandle, Microsoft.AspNetCore.Connections.FileHandleType fileHandleType) { } + public ulong FileHandle { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public Microsoft.AspNetCore.Connections.FileHandleType FileHandleType { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + } + public enum FileHandleType + { + Auto = 0, + Tcp = 1, + Pipe = 2, } public partial interface IConnectionBuilder { @@ -91,12 +110,49 @@ namespace Microsoft.AspNetCore.Connections Microsoft.AspNetCore.Connections.ConnectionDelegate Build(); Microsoft.AspNetCore.Connections.IConnectionBuilder Use(System.Func middleware); } + public partial interface IConnectionListener + { + System.Net.EndPoint EndPoint { get; } + System.Threading.Tasks.ValueTask AcceptAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + System.Threading.Tasks.ValueTask DisposeAsync(); + System.Threading.Tasks.ValueTask UnbindAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + } + public partial interface IConnectionListenerFactory + { + System.Threading.Tasks.ValueTask BindAsync(System.Net.EndPoint endpoint, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + } [System.FlagsAttribute] public enum TransferFormat { Binary = 1, Text = 2, } + public abstract partial class TransportConnection : Microsoft.AspNetCore.Connections.ConnectionContext, Microsoft.AspNetCore.Connections.Features.IConnectionIdFeature, Microsoft.AspNetCore.Connections.Features.IConnectionItemsFeature, Microsoft.AspNetCore.Connections.Features.IConnectionLifetimeFeature, Microsoft.AspNetCore.Connections.Features.IConnectionTransportFeature, Microsoft.AspNetCore.Connections.Features.IMemoryPoolFeature, Microsoft.AspNetCore.Http.Features.IFeatureCollection, System.Collections.Generic.IEnumerable>, System.Collections.IEnumerable + { + public TransportConnection() { } + public System.IO.Pipelines.IDuplexPipe Application { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + public override System.Threading.CancellationToken ConnectionClosed { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + public override string ConnectionId { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + public override Microsoft.AspNetCore.Http.Features.IFeatureCollection Features { get { throw null; } } + public override System.Collections.Generic.IDictionary Items { get { throw null; } set { } } + public override System.Net.EndPoint LocalEndPoint { [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 { } } + System.IO.Pipelines.IDuplexPipe Microsoft.AspNetCore.Connections.Features.IConnectionTransportFeature.Transport { get { throw null; } set { } } + System.Buffers.MemoryPool Microsoft.AspNetCore.Connections.Features.IMemoryPoolFeature.MemoryPool { get { throw null; } } + bool Microsoft.AspNetCore.Http.Features.IFeatureCollection.IsReadOnly { get { throw null; } } + object Microsoft.AspNetCore.Http.Features.IFeatureCollection.this[System.Type key] { get { throw null; } set { } } + int Microsoft.AspNetCore.Http.Features.IFeatureCollection.Revision { get { throw null; } } + public override System.Net.EndPoint RemoteEndPoint { [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) { } + void Microsoft.AspNetCore.Connections.Features.IConnectionLifetimeFeature.Abort() { } + TFeature Microsoft.AspNetCore.Http.Features.IFeatureCollection.Get() { throw null; } + void Microsoft.AspNetCore.Http.Features.IFeatureCollection.Set(TFeature feature) { } + System.Collections.Generic.IEnumerator> System.Collections.Generic.IEnumerable>.GetEnumerator() { throw null; } + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; } + } } namespace Microsoft.AspNetCore.Connections.Features { @@ -104,6 +160,11 @@ namespace Microsoft.AspNetCore.Connections.Features { void OnCompleted(System.Func callback, object state); } + public partial interface IConnectionEndPointFeature + { + System.Net.EndPoint LocalEndPoint { get; set; } + System.Net.EndPoint RemoteEndPoint { get; set; } + } public partial interface IConnectionHeartbeatFeature { void OnHeartbeat(System.Action action, object state); diff --git a/src/Servers/Connections.Abstractions/src/ConnectionContext.cs b/src/Servers/Connections.Abstractions/src/ConnectionContext.cs index a709a5f891..cecf3513c7 100644 --- a/src/Servers/Connections.Abstractions/src/ConnectionContext.cs +++ b/src/Servers/Connections.Abstractions/src/ConnectionContext.cs @@ -3,6 +3,9 @@ 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; @@ -18,6 +21,12 @@ namespace Microsoft.AspNetCore.Connections public abstract IDuplexPipe Transport { get; set; } + public virtual CancellationToken ConnectionClosed { get; set; } + + public virtual EndPoint LocalEndPoint { get; set; } + + public virtual EndPoint RemoteEndPoint { get; set; } + public virtual void Abort(ConnectionAbortedException abortReason) { // We expect this to be overridden, but this helps maintain back compat @@ -27,5 +36,10 @@ namespace Microsoft.AspNetCore.Connections } public virtual void Abort() => Abort(new ConnectionAbortedException("The connection was aborted by the application via ConnectionContext.Abort().")); + + public virtual ValueTask DisposeAsync() + { + return default; + } } } diff --git a/src/Servers/Connections.Abstractions/src/ConnectionDelegate.cs b/src/Servers/Connections.Abstractions/src/ConnectionDelegate.cs index f0d64d1587..dff0384f60 100644 --- a/src/Servers/Connections.Abstractions/src/ConnectionDelegate.cs +++ b/src/Servers/Connections.Abstractions/src/ConnectionDelegate.cs @@ -1,4 +1,7 @@ -using System.Threading.Tasks; +// 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.Threading.Tasks; namespace Microsoft.AspNetCore.Connections { diff --git a/src/Servers/Connections.Abstractions/src/DefaultConnectionContext.cs b/src/Servers/Connections.Abstractions/src/DefaultConnectionContext.cs index fab7c929e2..81a56478ba 100644 --- a/src/Servers/Connections.Abstractions/src/DefaultConnectionContext.cs +++ b/src/Servers/Connections.Abstractions/src/DefaultConnectionContext.cs @@ -4,8 +4,10 @@ using System; using System.Collections.Generic; using System.IO.Pipelines; +using System.Net; using System.Security.Claims; using System.Threading; +using System.Threading.Tasks; using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Http.Features; @@ -17,7 +19,8 @@ namespace Microsoft.AspNetCore.Connections IConnectionItemsFeature, IConnectionTransportFeature, IConnectionUserFeature, - IConnectionLifetimeFeature + IConnectionLifetimeFeature, + IConnectionEndPointFeature { private CancellationTokenSource _connectionClosedTokenSource = new CancellationTokenSource(); @@ -42,6 +45,7 @@ namespace Microsoft.AspNetCore.Connections Features.Set(this); Features.Set(this); Features.Set(this); + Features.Set(this); } public DefaultConnectionContext(string id, IDuplexPipe transport, IDuplexPipe application) @@ -63,7 +67,9 @@ namespace Microsoft.AspNetCore.Connections public override IDuplexPipe Transport { get; set; } - public CancellationToken ConnectionClosed { get; set; } + public override CancellationToken ConnectionClosed { get; set; } + public override EndPoint LocalEndPoint { get; set; } + public override EndPoint RemoteEndPoint { get; set; } public override void Abort(ConnectionAbortedException abortReason) { @@ -74,5 +80,11 @@ namespace Microsoft.AspNetCore.Connections { _connectionClosedTokenSource.Dispose(); } + + public override ValueTask DisposeAsync() + { + _connectionClosedTokenSource.Dispose(); + return base.DisposeAsync(); + } } } diff --git a/src/Servers/Connections.Abstractions/src/Features/IConnectionEndpointFeature.cs b/src/Servers/Connections.Abstractions/src/Features/IConnectionEndpointFeature.cs new file mode 100644 index 0000000000..7c44146ede --- /dev/null +++ b/src/Servers/Connections.Abstractions/src/Features/IConnectionEndpointFeature.cs @@ -0,0 +1,13 @@ +// 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.Net; + +namespace Microsoft.AspNetCore.Connections.Features +{ + public interface IConnectionEndPointFeature + { + EndPoint LocalEndPoint { get; set; } + EndPoint RemoteEndPoint { get; set; } + } +} diff --git a/src/Servers/Connections.Abstractions/src/Features/IConnectionUserFeature.cs b/src/Servers/Connections.Abstractions/src/Features/IConnectionUserFeature.cs index 3efb362fc7..7698693a54 100644 --- a/src/Servers/Connections.Abstractions/src/Features/IConnectionUserFeature.cs +++ b/src/Servers/Connections.Abstractions/src/Features/IConnectionUserFeature.cs @@ -1,3 +1,6 @@ +// 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.Security.Claims; namespace Microsoft.AspNetCore.Connections.Features @@ -6,4 +9,4 @@ namespace Microsoft.AspNetCore.Connections.Features { ClaimsPrincipal User { get; set; } } -} \ No newline at end of file +} diff --git a/src/Servers/Connections.Abstractions/src/FileHandleEndPoint.cs b/src/Servers/Connections.Abstractions/src/FileHandleEndPoint.cs new file mode 100644 index 0000000000..41f4d50812 --- /dev/null +++ b/src/Servers/Connections.Abstractions/src/FileHandleEndPoint.cs @@ -0,0 +1,30 @@ +// 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; + +namespace Microsoft.AspNetCore.Connections +{ + public class FileHandleEndPoint : EndPoint + { + public FileHandleEndPoint(ulong fileHandle, FileHandleType fileHandleType) + { + FileHandle = fileHandle; + FileHandleType = fileHandleType; + + switch (fileHandleType) + { + case FileHandleType.Auto: + case FileHandleType.Tcp: + case FileHandleType.Pipe: + break; + default: + throw new NotSupportedException(); + } + } + + public ulong FileHandle { get; } + public FileHandleType FileHandleType { get; } + } +} diff --git a/src/Servers/Kestrel/Transport.Abstractions/src/Internal/FileHandleType.cs b/src/Servers/Connections.Abstractions/src/FileHandleType.cs similarity index 52% rename from src/Servers/Kestrel/Transport.Abstractions/src/Internal/FileHandleType.cs rename to src/Servers/Connections.Abstractions/src/FileHandleType.cs index bb70e4ec34..f16935e044 100644 --- a/src/Servers/Kestrel/Transport.Abstractions/src/Internal/FileHandleType.cs +++ b/src/Servers/Connections.Abstractions/src/FileHandleType.cs @@ -1,10 +1,10 @@ -// 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 Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal +namespace Microsoft.AspNetCore.Connections { /// - /// Enumerates the types. + /// Enumerates the types. /// public enum FileHandleType { diff --git a/src/Servers/Connections.Abstractions/src/IConnectionBuilder.cs b/src/Servers/Connections.Abstractions/src/IConnectionBuilder.cs index 4825748292..5fe3ec25a0 100644 --- a/src/Servers/Connections.Abstractions/src/IConnectionBuilder.cs +++ b/src/Servers/Connections.Abstractions/src/IConnectionBuilder.cs @@ -1,4 +1,7 @@ -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; namespace Microsoft.AspNetCore.Connections { diff --git a/src/Servers/Connections.Abstractions/src/IConnectionListener.cs b/src/Servers/Connections.Abstractions/src/IConnectionListener.cs new file mode 100644 index 0000000000..c9d0564447 --- /dev/null +++ b/src/Servers/Connections.Abstractions/src/IConnectionListener.cs @@ -0,0 +1,20 @@ +// 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.Net; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Connections +{ + public interface IConnectionListener + { + EndPoint EndPoint { get; } + + ValueTask AcceptAsync(CancellationToken cancellationToken = default); + + ValueTask UnbindAsync(CancellationToken cancellationToken = default); + + ValueTask DisposeAsync(); + } +} diff --git a/src/Servers/Connections.Abstractions/src/IConnectionListenerFactory.cs b/src/Servers/Connections.Abstractions/src/IConnectionListenerFactory.cs new file mode 100644 index 0000000000..b28724e1dd --- /dev/null +++ b/src/Servers/Connections.Abstractions/src/IConnectionListenerFactory.cs @@ -0,0 +1,17 @@ +// 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.Net; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Connections +{ + public interface IConnectionListenerFactory + { + ValueTask BindAsync(EndPoint endpoint, CancellationToken cancellationToken = default); + } +} diff --git a/src/Servers/Connections.Abstractions/src/TransportConnection.FeatureCollection.cs b/src/Servers/Connections.Abstractions/src/TransportConnection.FeatureCollection.cs new file mode 100644 index 0000000000..fc5443ecfc --- /dev/null +++ b/src/Servers/Connections.Abstractions/src/TransportConnection.FeatureCollection.cs @@ -0,0 +1,44 @@ +// 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.Collections.Generic; +using System.IO.Pipelines; +using System.Threading; +using Microsoft.AspNetCore.Connections.Features; + +namespace Microsoft.AspNetCore.Connections +{ + public partial class TransportConnection : IConnectionIdFeature, + IConnectionTransportFeature, + IConnectionItemsFeature, + IMemoryPoolFeature, + IConnectionLifetimeFeature + { + // 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 + + MemoryPool IMemoryPoolFeature.MemoryPool => MemoryPool; + + IDuplexPipe IConnectionTransportFeature.Transport + { + get => Transport; + set => Transport = value; + } + + IDictionary IConnectionItemsFeature.Items + { + get => Items; + set => Items = value; + } + + CancellationToken IConnectionLifetimeFeature.ConnectionClosed + { + get => ConnectionClosed; + set => ConnectionClosed = value; + } + + void IConnectionLifetimeFeature.Abort() => Abort(new ConnectionAbortedException("The connection was aborted by the application via IConnectionLifetimeFeature.Abort().")); + } +} diff --git a/src/Servers/Kestrel/Transport.Abstractions/src/Internal/TransportConnection.Generated.cs b/src/Servers/Connections.Abstractions/src/TransportConnection.Generated.cs similarity index 54% rename from src/Servers/Kestrel/Transport.Abstractions/src/Internal/TransportConnection.Generated.cs rename to src/Servers/Connections.Abstractions/src/TransportConnection.Generated.cs index b5d0122ffb..eb6f2ba253 100644 --- a/src/Servers/Kestrel/Transport.Abstractions/src/Internal/TransportConnection.Generated.cs +++ b/src/Servers/Connections.Abstractions/src/TransportConnection.Generated.cs @@ -8,33 +8,21 @@ using System.Collections.Generic; using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Http.Features; -namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal +namespace Microsoft.AspNetCore.Connections { public partial class TransportConnection : IFeatureCollection { - private static readonly Type IHttpConnectionFeatureType = typeof(IHttpConnectionFeature); private static readonly Type IConnectionIdFeatureType = typeof(IConnectionIdFeature); private static readonly Type IConnectionTransportFeatureType = typeof(IConnectionTransportFeature); private static readonly Type IConnectionItemsFeatureType = typeof(IConnectionItemsFeature); private static readonly Type IMemoryPoolFeatureType = typeof(IMemoryPoolFeature); - private static readonly Type IApplicationTransportFeatureType = typeof(IApplicationTransportFeature); - private static readonly Type ITransportSchedulerFeatureType = typeof(ITransportSchedulerFeature); 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; private object _currentIConnectionTransportFeature; private object _currentIConnectionItemsFeature; private object _currentIMemoryPoolFeature; - private object _currentIApplicationTransportFeature; - private object _currentITransportSchedulerFeature; private object _currentIConnectionLifetimeFeature; - private object _currentIConnectionHeartbeatFeature; - private object _currentIConnectionLifetimeNotificationFeature; - private object _currentIConnectionCompleteFeature; private int _featureRevision; @@ -42,17 +30,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal private void FastReset() { - _currentIHttpConnectionFeature = this; _currentIConnectionIdFeature = this; _currentIConnectionTransportFeature = this; _currentIConnectionItemsFeature = this; _currentIMemoryPoolFeature = this; - _currentIApplicationTransportFeature = this; - _currentITransportSchedulerFeature = this; _currentIConnectionLifetimeFeature = this; - _currentIConnectionHeartbeatFeature = this; - _currentIConnectionLifetimeNotificationFeature = this; - _currentIConnectionCompleteFeature = this; } @@ -108,11 +90,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal get { object feature = null; - if (key == IHttpConnectionFeatureType) - { - feature = _currentIHttpConnectionFeature; - } - else if (key == IConnectionIdFeatureType) + if (key == IConnectionIdFeatureType) { feature = _currentIConnectionIdFeature; } @@ -128,30 +106,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal { feature = _currentIMemoryPoolFeature; } - else if (key == IApplicationTransportFeatureType) - { - feature = _currentIApplicationTransportFeature; - } - else if (key == ITransportSchedulerFeatureType) - { - feature = _currentITransportSchedulerFeature; - } else if (key == IConnectionLifetimeFeatureType) { feature = _currentIConnectionLifetimeFeature; } - else if (key == IConnectionHeartbeatFeatureType) - { - feature = _currentIConnectionHeartbeatFeature; - } - else if (key == IConnectionLifetimeNotificationFeatureType) - { - feature = _currentIConnectionLifetimeNotificationFeature; - } - else if (key == IConnectionCompleteFeatureType) - { - feature = _currentIConnectionCompleteFeature; - } else if (MaybeExtra != null) { feature = ExtraFeatureGet(key); @@ -164,11 +122,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal { _featureRevision++; - if (key == IHttpConnectionFeatureType) - { - _currentIHttpConnectionFeature = value; - } - else if (key == IConnectionIdFeatureType) + if (key == IConnectionIdFeatureType) { _currentIConnectionIdFeature = value; } @@ -184,30 +138,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal { _currentIMemoryPoolFeature = value; } - else if (key == IApplicationTransportFeatureType) - { - _currentIApplicationTransportFeature = value; - } - else if (key == ITransportSchedulerFeatureType) - { - _currentITransportSchedulerFeature = value; - } else if (key == IConnectionLifetimeFeatureType) { _currentIConnectionLifetimeFeature = value; } - else if (key == IConnectionHeartbeatFeatureType) - { - _currentIConnectionHeartbeatFeature = value; - } - else if (key == IConnectionLifetimeNotificationFeatureType) - { - _currentIConnectionLifetimeNotificationFeature = value; - } - else if (key == IConnectionCompleteFeatureType) - { - _currentIConnectionCompleteFeature = value; - } else { ExtraFeatureSet(key, value); @@ -218,11 +152,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal TFeature IFeatureCollection.Get() { TFeature feature = default; - if (typeof(TFeature) == typeof(IHttpConnectionFeature)) - { - feature = (TFeature)_currentIHttpConnectionFeature; - } - else if (typeof(TFeature) == typeof(IConnectionIdFeature)) + if (typeof(TFeature) == typeof(IConnectionIdFeature)) { feature = (TFeature)_currentIConnectionIdFeature; } @@ -238,30 +168,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal { feature = (TFeature)_currentIMemoryPoolFeature; } - else if (typeof(TFeature) == typeof(IApplicationTransportFeature)) - { - feature = (TFeature)_currentIApplicationTransportFeature; - } - else if (typeof(TFeature) == typeof(ITransportSchedulerFeature)) - { - feature = (TFeature)_currentITransportSchedulerFeature; - } else if (typeof(TFeature) == typeof(IConnectionLifetimeFeature)) { feature = (TFeature)_currentIConnectionLifetimeFeature; } - else if (typeof(TFeature) == typeof(IConnectionHeartbeatFeature)) - { - feature = (TFeature)_currentIConnectionHeartbeatFeature; - } - else if (typeof(TFeature) == typeof(IConnectionLifetimeNotificationFeature)) - { - feature = (TFeature)_currentIConnectionLifetimeNotificationFeature; - } - else if (typeof(TFeature) == typeof(IConnectionCompleteFeature)) - { - feature = (TFeature)_currentIConnectionCompleteFeature; - } else if (MaybeExtra != null) { feature = (TFeature)(ExtraFeatureGet(typeof(TFeature))); @@ -273,11 +183,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal void IFeatureCollection.Set(TFeature feature) { _featureRevision++; - if (typeof(TFeature) == typeof(IHttpConnectionFeature)) - { - _currentIHttpConnectionFeature = feature; - } - else if (typeof(TFeature) == typeof(IConnectionIdFeature)) + if (typeof(TFeature) == typeof(IConnectionIdFeature)) { _currentIConnectionIdFeature = feature; } @@ -293,30 +199,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal { _currentIMemoryPoolFeature = feature; } - else if (typeof(TFeature) == typeof(IApplicationTransportFeature)) - { - _currentIApplicationTransportFeature = feature; - } - else if (typeof(TFeature) == typeof(ITransportSchedulerFeature)) - { - _currentITransportSchedulerFeature = feature; - } else if (typeof(TFeature) == typeof(IConnectionLifetimeFeature)) { _currentIConnectionLifetimeFeature = feature; } - else if (typeof(TFeature) == typeof(IConnectionHeartbeatFeature)) - { - _currentIConnectionHeartbeatFeature = feature; - } - else if (typeof(TFeature) == typeof(IConnectionLifetimeNotificationFeature)) - { - _currentIConnectionLifetimeNotificationFeature = feature; - } - else if (typeof(TFeature) == typeof(IConnectionCompleteFeature)) - { - _currentIConnectionCompleteFeature = feature; - } else { ExtraFeatureSet(typeof(TFeature), feature); @@ -325,10 +211,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal private IEnumerable> FastEnumerable() { - if (_currentIHttpConnectionFeature != null) - { - yield return new KeyValuePair(IHttpConnectionFeatureType, _currentIHttpConnectionFeature); - } if (_currentIConnectionIdFeature != null) { yield return new KeyValuePair(IConnectionIdFeatureType, _currentIConnectionIdFeature); @@ -345,30 +227,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal { yield return new KeyValuePair(IMemoryPoolFeatureType, _currentIMemoryPoolFeature); } - if (_currentIApplicationTransportFeature != null) - { - yield return new KeyValuePair(IApplicationTransportFeatureType, _currentIApplicationTransportFeature); - } - if (_currentITransportSchedulerFeature != null) - { - yield return new KeyValuePair(ITransportSchedulerFeatureType, _currentITransportSchedulerFeature); - } if (_currentIConnectionLifetimeFeature != null) { yield return new KeyValuePair(IConnectionLifetimeFeatureType, _currentIConnectionLifetimeFeature); } - if (_currentIConnectionHeartbeatFeature != null) - { - yield return new KeyValuePair(IConnectionHeartbeatFeatureType, _currentIConnectionHeartbeatFeature); - } - if (_currentIConnectionLifetimeNotificationFeature != null) - { - yield return new KeyValuePair(IConnectionLifetimeNotificationFeatureType, _currentIConnectionLifetimeNotificationFeature); - } - if (_currentIConnectionCompleteFeature != null) - { - yield return new KeyValuePair(IConnectionCompleteFeatureType, _currentIConnectionCompleteFeature); - } if (MaybeExtra != null) { diff --git a/src/Servers/Connections.Abstractions/src/TransportConnection.cs b/src/Servers/Connections.Abstractions/src/TransportConnection.cs new file mode 100644 index 0000000000..4cef0363c9 --- /dev/null +++ b/src/Servers/Connections.Abstractions/src/TransportConnection.cs @@ -0,0 +1,62 @@ +// 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 Microsoft.AspNetCore.Http.Features; + +namespace Microsoft.AspNetCore.Connections +{ + public abstract partial class TransportConnection : ConnectionContext + { + private IDictionary _items; + + public TransportConnection() + { + FastReset(); + } + + public override EndPoint LocalEndPoint { get; set; } + public override EndPoint RemoteEndPoint { get; set; } + + public override string ConnectionId { get; set; } + + public override IFeatureCollection Features => this; + + public virtual MemoryPool MemoryPool { get; } + + public override IDuplexPipe Transport { get; set; } + + public IDuplexPipe Application { get; set; } + + public override IDictionary Items + { + get + { + // Lazily allocate connection metadata + return _items ?? (_items = new ConnectionItems()); + } + set + { + _items = value; + } + } + + public override CancellationToken ConnectionClosed { get; set; } + + // 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. + // That said, all derived types should override this method should override + // this implementation of Abort because canceling pending output reads is not + // sufficient to abort the connection if there is backpressure. + public override void Abort(ConnectionAbortedException abortReason) + { + Application.Input.CancelPendingRead(); + } + } +} diff --git a/src/Servers/Kestrel/Core/ref/Microsoft.AspNetCore.Server.Kestrel.Core.csproj b/src/Servers/Kestrel/Core/ref/Microsoft.AspNetCore.Server.Kestrel.Core.csproj index a7e5f1b75a..367e32ffec 100644 --- a/src/Servers/Kestrel/Core/ref/Microsoft.AspNetCore.Server.Kestrel.Core.csproj +++ b/src/Servers/Kestrel/Core/ref/Microsoft.AspNetCore.Server.Kestrel.Core.csproj @@ -7,7 +7,7 @@ - + diff --git a/src/Servers/Kestrel/Core/ref/Microsoft.AspNetCore.Server.Kestrel.Core.netcoreapp3.0.cs b/src/Servers/Kestrel/Core/ref/Microsoft.AspNetCore.Server.Kestrel.Core.netcoreapp3.0.cs index 1c23c2dc98..8891d7feb7 100644 --- a/src/Servers/Kestrel/Core/ref/Microsoft.AspNetCore.Server.Kestrel.Core.netcoreapp3.0.cs +++ b/src/Servers/Kestrel/Core/ref/Microsoft.AspNetCore.Server.Kestrel.Core.netcoreapp3.0.cs @@ -87,7 +87,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core } public partial class KestrelServer : Microsoft.AspNetCore.Hosting.Server.IServer, System.IDisposable { - public KestrelServer(Microsoft.Extensions.Options.IOptions options, Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal.ITransportFactory transportFactory, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory) { } + public KestrelServer(Microsoft.Extensions.Options.IOptions options, Microsoft.AspNetCore.Connections.IConnectionListenerFactory transportFactory, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory) { } public Microsoft.AspNetCore.Http.Features.IFeatureCollection Features { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } public Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerOptions Options { get { throw null; } } public void Dispose() { } @@ -118,7 +118,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core public KestrelServerOptions() { } public bool AddServerHeader { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } public bool AllowSynchronousIO { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } - public Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal.SchedulingMode ApplicationSchedulingMode { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } public System.IServiceProvider ApplicationServices { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } public Microsoft.AspNetCore.Server.Kestrel.KestrelConfigurationLoader ConfigurationLoader { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } public bool DisableStringReuse { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } @@ -140,19 +139,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core public void ListenUnixSocket(string socketPath) { } public void ListenUnixSocket(string socketPath, System.Action configure) { } } - public partial class ListenOptions : Microsoft.AspNetCore.Connections.IConnectionBuilder, Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal.IEndPointInformation + public partial class ListenOptions : Microsoft.AspNetCore.Connections.IConnectionBuilder { internal ListenOptions() { } public System.IServiceProvider ApplicationServices { get { throw null; } } public System.Collections.Generic.List ConnectionAdapters { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } - public ulong FileHandle { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } - public Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal.FileHandleType HandleType { get { throw null; } set { } } - public System.Net.IPEndPoint IPEndPoint { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + public System.Net.EndPoint EndPoint { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public ulong FileHandle { get { throw null; } } + public System.Net.IPEndPoint IPEndPoint { get { throw null; } } public Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerOptions KestrelServerOptions { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } - public bool NoDelay { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } public Microsoft.AspNetCore.Server.Kestrel.Core.HttpProtocols Protocols { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } - public string SocketPath { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } - public Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal.ListenType Type { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public string SocketPath { get { throw null; } } public Microsoft.AspNetCore.Connections.ConnectionDelegate Build() { throw null; } public override string ToString() { throw null; } public Microsoft.AspNetCore.Connections.IConnectionBuilder Use(System.Func middleware) { throw null; } diff --git a/src/Servers/Kestrel/Core/src/Adapter/Internal/AdaptedPipeline.cs b/src/Servers/Kestrel/Core/src/Adapter/Internal/AdaptedPipeline.cs index 8d9a35b456..8043ca28fe 100644 --- a/src/Servers/Kestrel/Core/src/Adapter/Internal/AdaptedPipeline.cs +++ b/src/Servers/Kestrel/Core/src/Adapter/Internal/AdaptedPipeline.cs @@ -6,26 +6,27 @@ using System.IO; using System.IO.Pipelines; using System.Threading.Tasks; 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 { internal class AdaptedPipeline : IDuplexPipe { - private static readonly int MinAllocBufferSize = KestrelMemoryPool.MinimumSegmentSize / 2; + private readonly int _minAllocBufferSize; private readonly IDuplexPipe _transport; public AdaptedPipeline(IDuplexPipe transport, Pipe inputPipe, Pipe outputPipe, - IKestrelTrace log) + IKestrelTrace log, + int minAllocBufferSize) { _transport = transport; Input = inputPipe; Output = outputPipe; Log = log; + _minAllocBufferSize = minAllocBufferSize; } public Pipe Input { get; } @@ -115,7 +116,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Adapter.Internal while (true) { - var outputBuffer = Input.Writer.GetMemory(MinAllocBufferSize); + var outputBuffer = Input.Writer.GetMemory(_minAllocBufferSize); var bytesRead = await stream.ReadAsync(outputBuffer); Input.Writer.Advance(bytesRead); diff --git a/src/Servers/Kestrel/Core/src/AnyIPListenOptions.cs b/src/Servers/Kestrel/Core/src/AnyIPListenOptions.cs index e2319b4977..555dc4af1d 100644 --- a/src/Servers/Kestrel/Core/src/AnyIPListenOptions.cs +++ b/src/Servers/Kestrel/Core/src/AnyIPListenOptions.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; @@ -29,7 +29,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core context.Logger.LogDebug(CoreStrings.FormatFallbackToIPv4Any(IPEndPoint.Port)); // for machines that do not support IPv6 - IPEndPoint = new IPEndPoint(IPAddress.Any, IPEndPoint.Port); + EndPoint = new IPEndPoint(IPAddress.Any, IPEndPoint.Port); await base.BindAsync(context).ConfigureAwait(false); } } diff --git a/src/Servers/Kestrel/Core/src/Internal/ConnectionDispatcher.cs b/src/Servers/Kestrel/Core/src/Internal/ConnectionDispatcher.cs index 0c0cca1573..4e354d8113 100644 --- a/src/Servers/Kestrel/Core/src/Internal/ConnectionDispatcher.cs +++ b/src/Servers/Kestrel/Core/src/Internal/ConnectionDispatcher.cs @@ -2,23 +2,21 @@ // 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.IO.Pipelines; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; 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.Internal { - internal class ConnectionDispatcher : IConnectionDispatcher + internal class ConnectionDispatcher { private static long _lastConnectionId = long.MinValue; private readonly ServiceContext _serviceContext; private readonly ConnectionDelegate _connectionDelegate; + private readonly TaskCompletionSource _acceptLoopTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); public ConnectionDispatcher(ServiceContext serviceContext, ConnectionDelegate connectionDelegate) { @@ -28,26 +26,47 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal private IKestrelTrace Log => _serviceContext.Log; - public Task OnConnection(TransportConnection connection) + public Task StartAcceptingConnections(IConnectionListener listener) { - // REVIEW: Unfortunately, we still need to use the service context to create the pipes since the settings - // for the scheduler and limits are specified here - var inputOptions = GetInputPipeOptions(_serviceContext, connection.MemoryPool, connection.InputWriterScheduler); - var outputOptions = GetOutputPipeOptions(_serviceContext, connection.MemoryPool, connection.OutputReaderScheduler); - - var pair = DuplexPipe.CreateConnectionPair(inputOptions, outputOptions); - - // Set the transport and connection id - connection.ConnectionId = CorrelationIdGenerator.GetNextId(); - connection.Transport = pair.Transport; - - // This *must* be set before returning from OnConnection - connection.Application = pair.Application; - - return Execute(new KestrelConnection(connection)); + ThreadPool.UnsafeQueueUserWorkItem(StartAcceptingConnectionsCore, listener, preferLocal: false); + return _acceptLoopTcs.Task; } - private async Task Execute(KestrelConnection connection) + private void StartAcceptingConnectionsCore(IConnectionListener listener) + { + // REVIEW: Multiple accept loops in parallel? + _ = AcceptConnectionsAsync(); + + async Task AcceptConnectionsAsync() + { + try + { + while (true) + { + var connection = await listener.AcceptAsync(); + + if (connection == null) + { + // We're done listening + break; + } + + _ = Execute(new KestrelConnection(connection, _serviceContext.Log)); + } + } + catch (Exception ex) + { + // REVIEW: If the accept loop ends should this trigger a server shutdown? It will manifest as a hang + Log.LogCritical(0, ex, "The connection listener failed to accept any new connections."); + } + finally + { + _acceptLoopTcs.TrySetResult(null); + } + } + } + + internal async Task Execute(KestrelConnection connection) { var id = Interlocked.Increment(ref _lastConnectionId); var connectionContext = connection.TransportConnection; @@ -69,25 +88,19 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal { Log.LogCritical(0, ex, $"{nameof(ConnectionDispatcher)}.{nameof(Execute)}() {connectionContext.ConnectionId}"); } - finally - { - // Complete the transport PipeReader and PipeWriter after calling into application code - connectionContext.Transport.Input.Complete(); - connectionContext.Transport.Output.Complete(); - } - - // Wait for the transport to close - await CancellationTokenAsTask(connectionContext.ConnectionClosed); } } finally { - await connectionContext.CompleteAsync(); + await connection.FireOnCompletedAsync(); Log.ConnectionStop(connectionContext.ConnectionId); KestrelEventSource.Log.ConnectionStop(connectionContext); - connection.Complete(); + // Dispose the transport connection, this needs to happen before removing it from the + // connection manager so that we only signal completion of this connection after the transport + // is properly torn down. + await connection.TransportConnection.DisposeAsync(); _serviceContext.ConnectionManager.RemoveConnection(id); } @@ -102,55 +115,5 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal return null; } - - private static Task CancellationTokenAsTask(CancellationToken token) - { - if (token.IsCancellationRequested) - { - return Task.CompletedTask; - } - - // Transports already dispatch prior to tripping ConnectionClosed - // since application code can register to this token. - var tcs = new TaskCompletionSource(); - token.Register(state => ((TaskCompletionSource)state).SetResult(null), tcs); - return tcs.Task; - } - - // Internal for testing - internal static PipeOptions GetInputPipeOptions(ServiceContext serviceContext, MemoryPool memoryPool, PipeScheduler writerScheduler) => new PipeOptions - ( - pool: memoryPool, - readerScheduler: serviceContext.Scheduler, - writerScheduler: writerScheduler, - pauseWriterThreshold: serviceContext.ServerOptions.Limits.MaxRequestBufferSize ?? 0, - resumeWriterThreshold: serviceContext.ServerOptions.Limits.MaxRequestBufferSize ?? 0, - useSynchronizationContext: false, - minimumSegmentSize: KestrelMemoryPool.MinimumSegmentSize - ); - - internal static PipeOptions GetOutputPipeOptions(ServiceContext serviceContext, MemoryPool memoryPool, PipeScheduler readerScheduler) => new PipeOptions - ( - pool: memoryPool, - readerScheduler: readerScheduler, - writerScheduler: serviceContext.Scheduler, - pauseWriterThreshold: GetOutputResponseBufferSize(serviceContext), - resumeWriterThreshold: GetOutputResponseBufferSize(serviceContext), - useSynchronizationContext: false, - minimumSegmentSize: KestrelMemoryPool.MinimumSegmentSize - ); - - private static long GetOutputResponseBufferSize(ServiceContext serviceContext) - { - var bufferSize = serviceContext.ServerOptions.Limits.MaxResponseBufferSize; - if (bufferSize == 0) - { - // 0 = no buffering so we need to configure the pipe so the writer waits on the reader directly - return 1; - } - - // null means that we have no back pressure - return bufferSize ?? 0; - } } } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/Http1ChunkedEncodingMessageBody.cs b/src/Servers/Kestrel/Core/src/Internal/Http/Http1ChunkedEncodingMessageBody.cs index 7c1327cd73..db9e78aa2f 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/Http1ChunkedEncodingMessageBody.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/Http1ChunkedEncodingMessageBody.cs @@ -9,7 +9,6 @@ using System.IO.Pipelines; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; -using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { @@ -546,7 +545,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http pauseWriterThreshold: 1, resumeWriterThreshold: 1, useSynchronizationContext: false, - minimumSegmentSize: KestrelMemoryPool.MinimumSegmentSize + minimumSegmentSize: context.MemoryPool.GetMinimumSegmentSize() )); } } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.FeatureCollection.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.FeatureCollection.cs index 11fcadc074..27b385888c 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.FeatureCollection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.FeatureCollection.cs @@ -13,7 +13,6 @@ using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Server.Kestrel.Core.Features; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; -using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http @@ -107,8 +106,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { RequestBody = value; var requestPipeReader = new StreamPipeReader(RequestBody, new StreamPipeReaderAdapterOptions( - minimumSegmentSize: KestrelMemoryPool.MinimumSegmentSize, - minimumReadThreshold: KestrelMemoryPool.MinimumSegmentSize / 4, + minimumSegmentSize: _context.MemoryPool.GetMinimumSegmentSize(), + minimumReadThreshold: _context.MemoryPool.GetMinimumAllocSize(), _context.MemoryPool)); RequestBodyPipeReader = requestPipeReader; @@ -264,7 +263,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http set { ResponseBody = value; - var responsePipeWriter = new StreamPipeWriter(ResponseBody, minimumSegmentSize: KestrelMemoryPool.MinimumSegmentSize, _context.MemoryPool); + var responsePipeWriter = new StreamPipeWriter(ResponseBody, minimumSegmentSize: _context.MemoryPool.GetMinimumSegmentSize(), _context.MemoryPool); ResponsePipeWriter = responsePipeWriter; // The StreamPipeWrapper needs to be disposed as it hold onto blocks of memory diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2OutputProducer.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2OutputProducer.cs index fec48783ef..4f481850d7 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2OutputProducer.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2OutputProducer.cs @@ -12,7 +12,6 @@ using Microsoft.AspNetCore.Internal; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.FlowControl; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; -using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 { @@ -363,7 +362,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 pauseWriterThreshold: 1, resumeWriterThreshold: 1, useSynchronizationContext: false, - minimumSegmentSize: KestrelMemoryPool.MinimumSegmentSize + minimumSegmentSize: pool.GetMinimumSegmentSize() )); } } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs index 5a27a5641f..d9407a8318 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs @@ -12,7 +12,6 @@ using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.FlowControl; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; -using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; @@ -494,7 +493,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 pauseWriterThreshold: windowSize + 1, resumeWriterThreshold: windowSize + 1, useSynchronizationContext: false, - minimumSegmentSize: KestrelMemoryPool.MinimumSegmentSize + minimumSegmentSize: _context.MemoryPool.GetMinimumSegmentSize() )); private (StreamCompletionFlags OldState, StreamCompletionFlags NewState) ApplyCompletionFlag(StreamCompletionFlags completionState) diff --git a/src/Servers/Kestrel/Core/src/Internal/HttpConnection.cs b/src/Servers/Kestrel/Core/src/Internal/HttpConnection.cs index 2bb90b1ae0..2d7b3c7633 100644 --- a/src/Servers/Kestrel/Core/src/Internal/HttpConnection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/HttpConnection.cs @@ -18,7 +18,6 @@ using Microsoft.AspNetCore.Server.Kestrel.Core.Features; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2; 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.Internal @@ -62,7 +61,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal pauseWriterThreshold: _context.ServiceContext.ServerOptions.Limits.MaxRequestBufferSize ?? 0, resumeWriterThreshold: _context.ServiceContext.ServerOptions.Limits.MaxRequestBufferSize ?? 0, useSynchronizationContext: false, - minimumSegmentSize: KestrelMemoryPool.MinimumSegmentSize + minimumSegmentSize: MemoryPool.GetMinimumSegmentSize() ); internal PipeOptions AdaptedOutputPipeOptions => new PipeOptions @@ -73,7 +72,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal pauseWriterThreshold: _context.ServiceContext.ServerOptions.Limits.MaxResponseBufferSize ?? 0, resumeWriterThreshold: _context.ServiceContext.ServerOptions.Limits.MaxResponseBufferSize ?? 0, useSynchronizationContext: false, - minimumSegmentSize: KestrelMemoryPool.MinimumSegmentSize + minimumSegmentSize: MemoryPool.GetMinimumSegmentSize() ); private IKestrelTrace Log => _context.ServiceContext.Log; @@ -94,7 +93,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal adaptedPipeline = new AdaptedPipeline(_adaptedTransport, new Pipe(AdaptedInputPipeOptions), new Pipe(AdaptedOutputPipeOptions), - Log); + Log, + MemoryPool.GetMinimumAllocSize()); _adaptedTransport = adaptedPipeline; } diff --git a/src/Servers/Kestrel/Core/src/Internal/HttpConnectionMiddleware.cs b/src/Servers/Kestrel/Core/src/Internal/HttpConnectionMiddleware.cs index 76b2f3c8e0..efae39ce71 100644 --- a/src/Servers/Kestrel/Core/src/Internal/HttpConnectionMiddleware.cs +++ b/src/Servers/Kestrel/Core/src/Internal/HttpConnectionMiddleware.cs @@ -47,20 +47,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal Transport = connectionContext.Transport }; - var connectionFeature = connectionContext.Features.Get(); - - if (connectionFeature != null) - { - if (connectionFeature.LocalIpAddress != null) - { - httpConnectionContext.LocalEndPoint = new IPEndPoint(connectionFeature.LocalIpAddress, connectionFeature.LocalPort); - } - - if (connectionFeature.RemoteIpAddress != null) - { - httpConnectionContext.RemoteEndPoint = new IPEndPoint(connectionFeature.RemoteIpAddress, connectionFeature.RemotePort); - } - } + httpConnectionContext.LocalEndPoint = connectionContext.LocalEndPoint as IPEndPoint; + httpConnectionContext.RemoteEndPoint = connectionContext.RemoteEndPoint as IPEndPoint; var connection = new HttpConnection(httpConnectionContext); diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/ConnectionManager.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/ConnectionManager.cs index cb402facea..05bb0f0726 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/ConnectionManager.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/ConnectionManager.cs @@ -1,8 +1,12 @@ -// 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.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Connections; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure { @@ -37,10 +41,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure public void RemoveConnection(long id) { - if (!_connectionReferences.TryRemove(id, out _)) + if (!_connectionReferences.TryRemove(id, out var reference)) { throw new ArgumentException(nameof(id)); } + + if (reference.TryGetConnection(out var connection)) + { + connection.Complete(); + } } public void Walk(Action callback) @@ -64,6 +73,46 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure } } + public async Task CloseAllConnectionsAsync(CancellationToken token) + { + var closeTasks = new List(); + + Walk(connection => + { + connection.RequestClose(); + closeTasks.Add(connection.ExecutionTask); + }); + + var allClosedTask = Task.WhenAll(closeTasks.ToArray()); + return await Task.WhenAny(allClosedTask, CancellationTokenAsTask(token)).ConfigureAwait(false) == allClosedTask; + } + + public async Task AbortAllConnectionsAsync() + { + var abortTasks = new List(); + + Walk(connection => + { + connection.TransportConnection.Abort(new ConnectionAbortedException(CoreStrings.ConnectionAbortedDuringServerShutdown)); + abortTasks.Add(connection.ExecutionTask); + }); + + var allAbortedTask = Task.WhenAll(abortTasks.ToArray()); + return await Task.WhenAny(allAbortedTask, Task.Delay(1000)).ConfigureAwait(false) == allAbortedTask; + } + + private static Task CancellationTokenAsTask(CancellationToken token) + { + if (token.IsCancellationRequested) + { + return Task.CompletedTask; + } + + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + token.Register(() => tcs.SetResult(null)); + return tcs.Task; + } + private static ResourceCounter GetCounter(long? number) => number.HasValue ? ResourceCounter.Quota(number.Value) diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/ConnectionManagerShutdownExtensions.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/ConnectionManagerShutdownExtensions.cs deleted file mode 100644 index 5877cbcf55..0000000000 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/ConnectionManagerShutdownExtensions.cs +++ /dev/null @@ -1,53 +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.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Connections; - -namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure -{ - internal static class ConnectionManagerShutdownExtensions - { - public static async Task CloseAllConnectionsAsync(this ConnectionManager connectionManager, CancellationToken token) - { - var closeTasks = new List(); - - connectionManager.Walk(connection => - { - connection.TransportConnection.RequestClose(); - closeTasks.Add(connection.ExecutionTask); - }); - - var allClosedTask = Task.WhenAll(closeTasks.ToArray()); - return await Task.WhenAny(allClosedTask, CancellationTokenAsTask(token)).ConfigureAwait(false) == allClosedTask; - } - - public static async Task AbortAllConnectionsAsync(this ConnectionManager connectionManager) - { - var abortTasks = new List(); - - connectionManager.Walk(connection => - { - connection.TransportConnection.Abort(new ConnectionAbortedException(CoreStrings.ConnectionAbortedDuringServerShutdown)); - abortTasks.Add(connection.ExecutionTask); - }); - - var allAbortedTask = Task.WhenAll(abortTasks.ToArray()); - return await Task.WhenAny(allAbortedTask, Task.Delay(1000)).ConfigureAwait(false) == allAbortedTask; - } - - private static Task CancellationTokenAsTask(CancellationToken token) - { - if (token.IsCancellationRequested) - { - return Task.CompletedTask; - } - - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - token.Register(() => tcs.SetResult(null)); - return tcs.Task; - } - } -} diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/HeartbeatManager.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/HeartbeatManager.cs index 8fcd25fe98..18635e1f27 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/HeartbeatManager.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/HeartbeatManager.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; @@ -35,7 +35,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure private void WalkCallback(KestrelConnection connection) { - connection.TransportConnection.TickHeartbeat(); + connection.TickHeartbeat(); } } } diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelConnection.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelConnection.cs index a253aa5c32..ccb51230c5 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelConnection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelConnection.cs @@ -1,25 +1,170 @@ // 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.Threading; using System.Threading.Tasks; -using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Connections.Features; +using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure { - internal class KestrelConnection + internal class KestrelConnection : IConnectionHeartbeatFeature, IConnectionCompleteFeature, IConnectionLifetimeNotificationFeature { - private TaskCompletionSource _executionTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + private List<(Action handler, object state)> _heartbeatHandlers; + private readonly object _heartbeatLock = new object(); - public KestrelConnection(TransportConnection transportConnection) + private Stack, object>> _onCompleted; + private bool _completed; + + private readonly CancellationTokenSource _connectionClosingCts = new CancellationTokenSource(); + private readonly TaskCompletionSource _completionTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + public KestrelConnection(ConnectionContext connectionContext, ILogger logger) { - TransportConnection = transportConnection; - ExecutionTask = _executionTcs.Task; + Logger = logger; + TransportConnection = connectionContext; + + // Set a connection id if the transport didn't set one + TransportConnection.ConnectionId ??= CorrelationIdGenerator.GetNextId(); + connectionContext.Features.Set(this); + connectionContext.Features.Set(this); + connectionContext.Features.Set(this); + ConnectionClosedRequested = _connectionClosingCts.Token; } - public TransportConnection TransportConnection { get; } + private ILogger Logger { get; } - public Task ExecutionTask { get; } + public ConnectionContext TransportConnection { get; set; } + public CancellationToken ConnectionClosedRequested { get; set; } + public Task ExecutionTask => _completionTcs.Task; - internal void Complete() => _executionTcs.TrySetResult(null); + public void TickHeartbeat() + { + lock (_heartbeatLock) + { + if (_heartbeatHandlers == null) + { + return; + } + + foreach (var (handler, state) in _heartbeatHandlers) + { + handler(state); + } + } + } + + public void OnHeartbeat(Action action, object state) + { + lock (_heartbeatLock) + { + if (_heartbeatHandlers == null) + { + _heartbeatHandlers = new List<(Action handler, object state)>(); + } + + _heartbeatHandlers.Add((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 FireOnCompletedAsync() + { + 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."); + } + } + } + + public void RequestClose() + { + try + { + _connectionClosingCts.Cancel(); + } + catch (ObjectDisposedException) + { + // There's a race where the token could be disposed + // swallow the exception and no-op + } + } + + public void Complete() + { + _completionTcs.TrySetResult(null); + + _connectionClosingCts.Dispose(); + } } } diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelEventSource.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelEventSource.cs index ae6a03915b..fdabf48247 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelEventSource.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelEventSource.cs @@ -2,10 +2,9 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Diagnostics.Tracing; -using System.Net; using System.Runtime.CompilerServices; +using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; -using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure { @@ -27,15 +26,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure // - Avoid renaming methods or parameters marked with EventAttribute. EventSource uses these to form the event object. [NonEvent] - public void ConnectionStart(TransportConnection connection) + public void ConnectionStart(ConnectionContext connection) { // avoid allocating strings unless this event source is enabled if (IsEnabled()) { ConnectionStart( connection.ConnectionId, - connection.LocalAddress != null ? new IPEndPoint(connection.LocalAddress, connection.LocalPort).ToString() : null, - connection.RemoteAddress != null ? new IPEndPoint(connection.RemoteAddress, connection.RemotePort).ToString() : null); + connection.LocalEndPoint?.ToString(), + connection.RemoteEndPoint?.ToString()); } } @@ -54,7 +53,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure } [NonEvent] - public void ConnectionStop(TransportConnection connection) + public void ConnectionStop(ConnectionContext connection) { if (IsEnabled()) { diff --git a/src/Servers/Kestrel/Core/src/Internal/MemoryPoolExtensions.cs b/src/Servers/Kestrel/Core/src/Internal/MemoryPoolExtensions.cs new file mode 100644 index 0000000000..d3b1f30a02 --- /dev/null +++ b/src/Servers/Kestrel/Core/src/Internal/MemoryPoolExtensions.cs @@ -0,0 +1,31 @@ +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal +{ + internal static class MemoryPoolExtensions + { + /// + /// Computes a minimum segment size + /// + /// + /// + public static int GetMinimumSegmentSize(this MemoryPool pool) + { + if (pool == null) + { + return 4096; + } + + return Math.Min(4096, pool.MaxBufferSize); + } + + public static int GetMinimumAllocSize(this MemoryPool pool) + { + // 1/2 of a segment + return pool.GetMinimumSegmentSize() / 2; + } + } +} diff --git a/src/Servers/Kestrel/Core/src/KestrelServer.cs b/src/Servers/Kestrel/Core/src/KestrelServer.cs index 51b444ada2..0481df8fad 100644 --- a/src/Servers/Kestrel/Core/src/KestrelServer.cs +++ b/src/Servers/Kestrel/Core/src/KestrelServer.cs @@ -6,13 +6,13 @@ using System.Collections.Generic; using System.IO.Pipelines; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server.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.Infrastructure; -using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -20,23 +20,21 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core { public class KestrelServer : IServer { - private readonly List _transports = new List(); + private readonly List<(IConnectionListener, Task)> _transports = new List<(IConnectionListener, Task)>(); private readonly IServerAddressesFeature _serverAddresses; - private readonly ITransportFactory _transportFactory; + private readonly IConnectionListenerFactory _transportFactory; private bool _hasStarted; private int _stopping; private readonly TaskCompletionSource _stoppedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); -#pragma warning disable PUB0001 // Pubternal type in public API - public KestrelServer(IOptions options, ITransportFactory transportFactory, ILoggerFactory loggerFactory) -#pragma warning restore PUB0001 + public KestrelServer(IOptions options, IConnectionListenerFactory transportFactory, ILoggerFactory loggerFactory) : this(transportFactory, CreateServiceContext(options, loggerFactory)) { } // For testing - internal KestrelServer(ITransportFactory transportFactory, ServiceContext serviceContext) + internal KestrelServer(IConnectionListenerFactory transportFactory, ServiceContext serviceContext) { if (transportFactory == null) { @@ -79,27 +77,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core DebuggerWrapper.Singleton, trace); - // TODO: This logic will eventually move into the IConnectionHandler and off - // the service context once we get to https://github.com/aspnet/KestrelHttpServer/issues/1662 - PipeScheduler scheduler = null; - switch (serverOptions.ApplicationSchedulingMode) - { - case SchedulingMode.Default: - case SchedulingMode.ThreadPool: - scheduler = PipeScheduler.ThreadPool; - break; - case SchedulingMode.Inline: - scheduler = PipeScheduler.Inline; - break; - default: - throw new NotSupportedException(CoreStrings.FormatUnknownTransportMode(serverOptions.ApplicationSchedulingMode)); - } - return new ServiceContext { Log = trace, HttpParser = new HttpParser(trace.IsEnabled(LogLevel.Information)), - Scheduler = scheduler, + Scheduler = PipeScheduler.ThreadPool, SystemClock = heartbeatManager, DateHeaderValueManager = dateHeaderValueManager, ConnectionManager = connectionManager, @@ -138,12 +120,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core ServiceContext.Heartbeat?.Start(); - async Task OnBind(ListenOptions endpoint) + async Task OnBind(ListenOptions options) { // Add the HTTP middleware as the terminal connection middleware - endpoint.UseHttpServer(endpoint.ConnectionAdapters, ServiceContext, application, endpoint.Protocols); + options.UseHttpServer(options.ConnectionAdapters, ServiceContext, application, options.Protocols); - var connectionDelegate = endpoint.Build(); + var connectionDelegate = options.Build(); // Add the connection limit middleware if (Options.Limits.MaxConcurrentConnections.HasValue) @@ -152,10 +134,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core } var connectionDispatcher = new ConnectionDispatcher(ServiceContext, connectionDelegate); - var transport = _transportFactory.Create(endpoint, connectionDispatcher); - _transports.Add(transport); + var transport = await _transportFactory.BindAsync(options.EndPoint).ConfigureAwait(false); - await transport.BindAsync().ConfigureAwait(false); + // Update the endpoint + options.EndPoint = transport.EndPoint; + var acceptLoopTask = connectionDispatcher.StartAcceptingConnections(transport); + + _transports.Add((transport, acceptLoopTask)); } await AddressBinder.BindAsync(_serverAddresses, Options, Trace, OnBind).ConfigureAwait(false); @@ -182,8 +167,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core var tasks = new Task[_transports.Count]; for (int i = 0; i < _transports.Count; i++) { - tasks[i] = _transports[i].UnbindAsync(); + (IConnectionListener listener, Task acceptLoop) = _transports[i]; + tasks[i] = Task.WhenAll(listener.UnbindAsync(cancellationToken).AsTask(), acceptLoop); } + await Task.WhenAll(tasks).ConfigureAwait(false); if (!await ConnectionManager.CloseAllConnectionsAsync(cancellationToken).ConfigureAwait(false)) @@ -198,8 +185,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core for (int i = 0; i < _transports.Count; i++) { - tasks[i] = _transports[i].StopAsync(); + (IConnectionListener listener, Task acceptLoop) = _transports[i]; + tasks[i] = listener.DisposeAsync().AsTask(); } + await Task.WhenAll(tasks).ConfigureAwait(false); ServiceContext.Heartbeat?.Dispose(); diff --git a/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs b/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs index bbe5d30b9a..01a94fb0e7 100644 --- a/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs +++ b/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs @@ -10,7 +10,6 @@ using Microsoft.AspNetCore.Certificates.Generation; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; using Microsoft.AspNetCore.Server.Kestrel.Https; -using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -38,14 +37,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core /// public bool AddServerHeader { get; set; } = true; - /// - /// Gets or sets a value that determines how Kestrel should schedule user callbacks. - /// - /// The default mode is -#pragma warning disable PUB0001 // Pubternal type in public API - public SchedulingMode ApplicationSchedulingMode { get; set; } = SchedulingMode.Default; -#pragma warning restore PUB0001 // Pubternal type in public API - /// /// Gets or sets a value that controls whether synchronous IO is allowed for the and /// diff --git a/src/Servers/Kestrel/Core/src/ListenOptions.cs b/src/Servers/Kestrel/Core/src/ListenOptions.cs index b3e4ec21ce..3a850c25f9 100644 --- a/src/Servers/Kestrel/Core/src/ListenOptions.cs +++ b/src/Servers/Kestrel/Core/src/ListenOptions.cs @@ -1,15 +1,15 @@ -// 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.Collections.Generic; using System.Linq; using System.Net; +using System.Net.Sockets; using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Server.Kestrel.Core.Adapter.Internal; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; -using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; namespace Microsoft.AspNetCore.Server.Kestrel.Core { @@ -17,21 +17,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core /// Describes either an , Unix domain socket path, or a file descriptor for an already open /// socket that Kestrel should bind to or open. /// - public class ListenOptions : IEndPointInformation, IConnectionBuilder + public class ListenOptions : IConnectionBuilder { - private FileHandleType _handleType; internal readonly List> _middleware = new List>(); internal ListenOptions(IPEndPoint endPoint) { - Type = ListenType.IPEndPoint; - IPEndPoint = endPoint; + EndPoint = endPoint; } internal ListenOptions(string socketPath) { - Type = ListenType.SocketPath; - SocketPath = socketPath; + EndPoint = new UnixDomainSocketEndPoint(socketPath); } internal ListenOptions(ulong fileHandle) @@ -41,73 +38,29 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core internal ListenOptions(ulong fileHandle, FileHandleType handleType) { - Type = ListenType.FileHandle; - FileHandle = fileHandle; - switch (handleType) - { - case FileHandleType.Auto: - case FileHandleType.Tcp: - case FileHandleType.Pipe: - _handleType = handleType; - break; - default: - throw new NotSupportedException(); - } + EndPoint = new FileHandleEndPoint(fileHandle, handleType); } - /// - /// The type of interface being described: either an , Unix domain socket path, or a file descriptor. - /// -#pragma warning disable PUB0001 // Pubternal type in public API - public ListenType Type { get; } -#pragma warning restore PUB0001 // Pubternal type in public API - -#pragma warning disable PUB0001 // Pubternal type in public API - public FileHandleType HandleType -#pragma warning restore PUB0001 // Pubternal type in public API - { - get => _handleType; - set - { - if (value == _handleType) - { - return; - } - if (Type != ListenType.FileHandle || _handleType != FileHandleType.Auto) - { - throw new InvalidOperationException(); - } - - switch (value) - { - case FileHandleType.Tcp: - case FileHandleType.Pipe: - _handleType = value; - break; - default: - throw new ArgumentException(nameof(HandleType)); - } - } - } + public EndPoint EndPoint { get; internal set; } // IPEndPoint is mutable so port 0 can be updated to the bound port. /// /// The to bind to. - /// Only set if the is . + /// Only set if the is . /// - public IPEndPoint IPEndPoint { get; set; } + public IPEndPoint IPEndPoint => EndPoint as IPEndPoint; /// /// The absolute path to a Unix domain socket to bind to. - /// Only set if the is . + /// Only set if the is . /// - public string SocketPath { get; } + public string SocketPath => (EndPoint as UnixDomainSocketEndPoint)?.ToString(); /// /// A file descriptor for the socket to open. - /// Only set if the is . + /// Only set if the is . /// - public ulong FileHandle { get; } + public ulong FileHandle => (EndPoint as FileHandleEndPoint)?.FileHandle ?? 0; /// /// Enables an to resolve and use services registered by the application during startup. @@ -115,14 +68,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core /// public KestrelServerOptions KestrelServerOptions { get; internal set; } - /// - /// Set to false to enable Nagle's algorithm for all connections. - /// - /// - /// Defaults to true. - /// - public bool NoDelay { get; set; } = true; - /// /// The protocols enabled on this endpoint. /// @@ -153,13 +98,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core ? "https" : "http"; - switch (Type) + switch (EndPoint) { - case ListenType.IPEndPoint: + case IPEndPoint _: return $"{scheme}://{IPEndPoint}"; - case ListenType.SocketPath: - return $"{scheme}://unix:{SocketPath}"; - case ListenType.FileHandle: + case UnixDomainSocketEndPoint _: + return $"{scheme}://unix:{EndPoint}"; + case FileHandleEndPoint _: return $"{scheme}://"; default: throw new InvalidOperationException(); diff --git a/src/Servers/Kestrel/Core/src/LocalhostListenOptions.cs b/src/Servers/Kestrel/Core/src/LocalhostListenOptions.cs index 80e008c78b..c7e5f47cad 100644 --- a/src/Servers/Kestrel/Core/src/LocalhostListenOptions.cs +++ b/src/Servers/Kestrel/Core/src/LocalhostListenOptions.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; @@ -76,9 +76,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core { var options = new ListenOptions(new IPEndPoint(address, IPEndPoint.Port)) { - HandleType = HandleType, KestrelServerOptions = KestrelServerOptions, - NoDelay = NoDelay, Protocols = Protocols, }; diff --git a/src/Servers/Kestrel/Core/src/Microsoft.AspNetCore.Server.Kestrel.Core.csproj b/src/Servers/Kestrel/Core/src/Microsoft.AspNetCore.Server.Kestrel.Core.csproj index a528049cf6..8161334613 100644 --- a/src/Servers/Kestrel/Core/src/Microsoft.AspNetCore.Server.Kestrel.Core.csproj +++ b/src/Servers/Kestrel/Core/src/Microsoft.AspNetCore.Server.Kestrel.Core.csproj @@ -19,7 +19,7 @@ - + diff --git a/src/Servers/Kestrel/Core/test/AddressBinderTests.cs b/src/Servers/Kestrel/Core/test/AddressBinderTests.cs index 74b067cae3..76d5172207 100644 --- a/src/Servers/Kestrel/Core/test/AddressBinderTests.cs +++ b/src/Servers/Kestrel/Core/test/AddressBinderTests.cs @@ -4,12 +4,13 @@ using System; using System.IO; using System.Net; +using System.Net.Sockets; using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Http.Internal; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; -using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; using Microsoft.AspNetCore.Testing; +using Microsoft.AspNetCore.Testing.xunit; using Microsoft.Extensions.Logging.Abstractions; using Xunit; @@ -53,10 +54,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests [InlineData("contoso.com")] public void ParseAddressDefaultsToAnyIPOnInvalidIPAddress(string host) { - var options = new KestrelServerOptions(); var listenOptions = AddressBinder.ParseAddress($"http://{host}", out var https); Assert.IsType(listenOptions); - Assert.Equal(ListenType.IPEndPoint, listenOptions.Type); + Assert.IsType(listenOptions.EndPoint); Assert.Equal(IPAddress.IPv6Any, listenOptions.IPEndPoint.Address); Assert.Equal(80, listenOptions.IPEndPoint.Port); Assert.False(https); @@ -65,21 +65,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests [Fact] public void ParseAddressLocalhost() { - var options = new KestrelServerOptions(); var listenOptions = AddressBinder.ParseAddress("http://localhost", out var https); Assert.IsType(listenOptions); - Assert.Equal(ListenType.IPEndPoint, listenOptions.Type); + Assert.IsType(listenOptions.EndPoint); Assert.Equal(IPAddress.Loopback, listenOptions.IPEndPoint.Address); Assert.Equal(80, listenOptions.IPEndPoint.Port); Assert.False(https); } - [Fact] + [OSSkipCondition(OperatingSystems.Windows, WindowsVersions.Win7, WindowsVersions.Win8, WindowsVersions.Win81, WindowsVersions.Win2008R2, SkipReason = "UnixDomainSocketEndPoint is not supported on older versions of Windows")] + [ConditionalFact] public void ParseAddressUnixPipe() { - var options = new KestrelServerOptions(); var listenOptions = AddressBinder.ParseAddress("http://unix:/tmp/kestrel-test.sock", out var https); - Assert.Equal(ListenType.SocketPath, listenOptions.Type); + Assert.IsType(listenOptions.EndPoint); Assert.Equal("/tmp/kestrel-test.sock", listenOptions.SocketPath); Assert.False(https); } @@ -92,9 +91,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests [InlineData("https://127.0.0.1", "127.0.0.1", 443, true)] public void ParseAddressIP(string address, string ip, int port, bool isHttps) { - var options = new KestrelServerOptions(); var listenOptions = AddressBinder.ParseAddress(address, out var https); - Assert.Equal(ListenType.IPEndPoint, listenOptions.Type); + Assert.IsType(listenOptions.EndPoint); Assert.Equal(IPAddress.Parse(ip), listenOptions.IPEndPoint.Address); Assert.Equal(port, listenOptions.IPEndPoint.Port); Assert.Equal(isHttps, https); diff --git a/src/Servers/Kestrel/Core/test/ConnectionDispatcherTests.cs b/src/Servers/Kestrel/Core/test/ConnectionDispatcherTests.cs index 1d1c3b46e3..5302cccba7 100644 --- a/src/Servers/Kestrel/Core/test/ConnectionDispatcherTests.cs +++ b/src/Servers/Kestrel/Core/test/ConnectionDispatcherTests.cs @@ -3,13 +3,14 @@ using System; using System.Collections.Generic; -using System.IO.Pipelines; using System.Linq; +using System.Net; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; -using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Logging; using Moq; @@ -30,7 +31,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var connection = new Mock { CallBase = true }.Object; connection.ConnectionClosed = new CancellationToken(canceled: true); - dispatcher.OnConnection(connection); + _ = dispatcher.Execute(new KestrelConnection(connection, Mock.Of())); // The scope should be created var scopeObjects = ((TestKestrelTrace)serviceContext.Log) @@ -51,25 +52,21 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests } [Fact] - public async Task OnConnectionCompletesTransportPipesAfterReturning() + public async Task StartAcceptingConnectionsAsyncLogsIfAcceptAsyncThrows() { var serviceContext = new TestServiceContext(); + var logger = ((TestKestrelTrace)serviceContext.Log).Logger; + logger.ThrowOnCriticalErrors = false; + var dispatcher = new ConnectionDispatcher(serviceContext, _ => Task.CompletedTask); - var mockConnection = new Mock { CallBase = true }; - mockConnection.Object.ConnectionClosed = new CancellationToken(canceled: true); - var mockPipeReader = new Mock(); - var mockPipeWriter = new Mock(); - var mockPipe = new Mock(); - mockPipe.Setup(m => m.Input).Returns(mockPipeReader.Object); - mockPipe.Setup(m => m.Output).Returns(mockPipeWriter.Object); - mockConnection.Setup(m => m.Transport).Returns(mockPipe.Object); - var connection = mockConnection.Object; + await dispatcher.StartAcceptingConnections(new ThrowingListener()); - await dispatcher.OnConnection(connection); - - mockPipeWriter.Verify(m => m.Complete(It.IsAny()), Times.Once()); - mockPipeReader.Verify(m => m.Complete(It.IsAny()), Times.Once()); + Assert.Equal(1, logger.CriticalErrorsLogged); + var critical = logger.Messages.SingleOrDefault(m => m.LogLevel == LogLevel.Critical); + Assert.NotNull(critical); + Assert.IsType(critical.Exception); + Assert.Equal("Unexpected error listening", critical.Exception.Message); } [Fact] @@ -80,14 +77,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var connection = new Mock { CallBase = true }.Object; connection.ConnectionClosed = new CancellationToken(canceled: true); - var completeFeature = connection.Features.Get(); + var kestrelConnection = new KestrelConnection(connection, Mock.Of()); + var completeFeature = kestrelConnection.TransportConnection.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); + await dispatcher.Execute(kestrelConnection); Assert.Equal(stateObject, callbackState); } @@ -100,21 +98,41 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests 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; + var kestrelConnection = new KestrelConnection(connection, mockLogger.Object); + var completeFeature = kestrelConnection.TransportConnection.Features.Get(); Assert.NotNull(completeFeature); object stateObject = new object(); object callbackState = null; completeFeature.OnCompleted(state => { callbackState = state; throw new InvalidTimeZoneException(); }, stateObject); - await dispatcher.OnConnection(connection); + await dispatcher.Execute(kestrelConnection); 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]); } + + private class ThrowingListener : IConnectionListener + { + public EndPoint EndPoint { get; set; } + + public ValueTask AcceptAsync(CancellationToken cancellationToken = default) + { + throw new InvalidOperationException("Unexpected error listening"); + } + + public ValueTask DisposeAsync() + { + return default; + } + + public ValueTask UnbindAsync(CancellationToken cancellationToken = default) + { + return default; + } + } } } diff --git a/src/Servers/Kestrel/Core/test/Http1ConnectionTests.cs b/src/Servers/Kestrel/Core/test/Http1ConnectionTests.cs index 494e4531e6..042a136b3e 100644 --- a/src/Servers/Kestrel/Core/test/Http1ConnectionTests.cs +++ b/src/Servers/Kestrel/Core/test/Http1ConnectionTests.cs @@ -19,7 +19,6 @@ 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.Infrastructure; -using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; @@ -43,7 +42,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests public Http1ConnectionTests() { - _pipelineFactory = KestrelMemoryPool.Create(); + _pipelineFactory = MemoryPoolFactory.Create(); var options = new PipeOptions(_pipelineFactory, readerScheduler: PipeScheduler.Inline, writerScheduler: PipeScheduler.Inline, useSynchronizationContext: false); var pair = DuplexPipe.CreateConnectionPair(options, options); diff --git a/src/Servers/Kestrel/Core/test/HttpConnectionManagerTests.cs b/src/Servers/Kestrel/Core/test/HttpConnectionManagerTests.cs index 809c719e5b..b7c7df6a1d 100644 --- a/src/Servers/Kestrel/Core/test/HttpConnectionManagerTests.cs +++ b/src/Servers/Kestrel/Core/test/HttpConnectionManagerTests.cs @@ -3,8 +3,9 @@ using System; using System.Runtime.CompilerServices; +using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; -using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; +using Microsoft.Extensions.Logging; using Moq; using Xunit; @@ -38,9 +39,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests ConnectionManager httpConnectionManager, Mock trace) { - var mock = new Mock(); + var mock = new Mock() { CallBase = true }; mock.Setup(m => m.ConnectionId).Returns(connectionId); - var httpConnection = new KestrelConnection(mock.Object); + var httpConnection = new KestrelConnection(mock.Object, Mock.Of()); httpConnectionManager.AddConnection(0, httpConnection); diff --git a/src/Servers/Kestrel/Core/test/HttpResponseHeadersTests.cs b/src/Servers/Kestrel/Core/test/HttpResponseHeadersTests.cs index 4aad2e3561..6a4e1c2352 100644 --- a/src/Servers/Kestrel/Core/test/HttpResponseHeadersTests.cs +++ b/src/Servers/Kestrel/Core/test/HttpResponseHeadersTests.cs @@ -2,6 +2,7 @@ // 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.Globalization; using System.IO.Pipelines; @@ -9,7 +10,6 @@ using Microsoft.AspNetCore.Http; 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.Transport.Abstractions.Internal; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Primitives; using Xunit; @@ -21,7 +21,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests [Fact] public void InitialDictionaryIsEmpty() { - using (var memoryPool = KestrelMemoryPool.Create()) + using (var memoryPool = MemoryPoolFactory.Create()) { var options = new PipeOptions(memoryPool, readerScheduler: PipeScheduler.Inline, writerScheduler: PipeScheduler.Inline, useSynchronizationContext: false); var pair = DuplexPipe.CreateConnectionPair(options, options); diff --git a/src/Servers/Kestrel/Core/test/KestrelServerOptionsTests.cs b/src/Servers/Kestrel/Core/test/KestrelServerOptionsTests.cs index 02e52db7f0..c8243b6025 100644 --- a/src/Servers/Kestrel/Core/test/KestrelServerOptionsTests.cs +++ b/src/Servers/Kestrel/Core/test/KestrelServerOptionsTests.cs @@ -8,20 +8,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests { public class KestrelServerOptionsTests { - [Fact] - public void NoDelayDefaultsToTrue() - { - var o1 = new KestrelServerOptions(); - o1.Listen(IPAddress.Loopback, 0); - o1.Listen(IPAddress.Loopback, 0, d => - { - d.NoDelay = false; - }); - - Assert.True(o1.ListenOptions[0].NoDelay); - Assert.False(o1.ListenOptions[1].NoDelay); - } - [Fact] public void AllowSynchronousIODefaultsToFalse() { @@ -36,33 +22,32 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var options = new KestrelServerOptions(); options.ListenLocalhost(5000); - Assert.True(options.ListenOptions[0].NoDelay); + Assert.Equal(HttpProtocols.Http1AndHttp2, options.ListenOptions[0].Protocols); options.ConfigureEndpointDefaults(opt => { - opt.NoDelay = false; + opt.Protocols = HttpProtocols.Http1; }); options.Listen(new IPEndPoint(IPAddress.Loopback, 5000), opt => { // ConfigureEndpointDefaults runs before this callback - Assert.False(opt.NoDelay); + Assert.Equal(HttpProtocols.Http1, opt.Protocols); }); - Assert.False(options.ListenOptions[1].NoDelay); + Assert.Equal(HttpProtocols.Http1, options.ListenOptions[1].Protocols); options.ListenLocalhost(5000, opt => { - Assert.False(opt.NoDelay); - opt.NoDelay = true; // Can be overriden + Assert.Equal(HttpProtocols.Http1, opt.Protocols); + opt.Protocols = HttpProtocols.Http2; // Can be overriden }); - Assert.True(options.ListenOptions[2].NoDelay); - + Assert.Equal(HttpProtocols.Http2, options.ListenOptions[2].Protocols); options.ListenAnyIP(5000, opt => { - Assert.False(opt.NoDelay); + opt.Protocols = HttpProtocols.Http2; }); - Assert.False(options.ListenOptions[3].NoDelay); + Assert.Equal(HttpProtocols.Http2, options.ListenOptions[3].Protocols); } } } diff --git a/src/Servers/Kestrel/Core/test/KestrelServerTests.cs b/src/Servers/Kestrel/Core/test/KestrelServerTests.cs index 180c7e9bf8..8af4397809 100644 --- a/src/Servers/Kestrel/Core/test/KestrelServerTests.cs +++ b/src/Servers/Kestrel/Core/test/KestrelServerTests.cs @@ -7,11 +7,11 @@ using System.Net; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server.Features; 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.AspNetCore.Testing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -203,7 +203,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var mockLoggerFactory = new Mock(); var mockLogger = new Mock(); mockLoggerFactory.Setup(m => m.CreateLogger(It.IsAny())).Returns(mockLogger.Object); - new KestrelServer(Options.Create(null), Mock.Of(), mockLoggerFactory.Object); + new KestrelServer(Options.Create(null), Mock.Of(), mockLoggerFactory.Object); mockLoggerFactory.Verify(factory => factory.CreateLogger("Microsoft.AspNetCore.Server.Kestrel")); } @@ -233,21 +233,26 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var unbind = new SemaphoreSlim(0); var stop = new SemaphoreSlim(0); - var mockTransport = new Mock(); - mockTransport - .Setup(transport => transport.BindAsync()) - .Returns(Task.CompletedTask); - mockTransport - .Setup(transport => transport.UnbindAsync()) - .Returns(async () => await unbind.WaitAsync()); - mockTransport - .Setup(transport => transport.StopAsync()) - .Returns(async () => await stop.WaitAsync()); - - var mockTransportFactory = new Mock(); + var mockTransport = new Mock(); + var mockTransportFactory = new Mock(); mockTransportFactory - .Setup(transportFactory => transportFactory.Create(It.IsAny(), It.IsAny())) - .Returns(mockTransport.Object); + .Setup(transportFactory => transportFactory.BindAsync(It.IsAny(), It.IsAny())) + .Returns((e, token) => + { + mockTransport + .Setup(transport => transport.AcceptAsync(It.IsAny())) + .Returns(new ValueTask((ConnectionContext)null)); + mockTransport + .Setup(transport => transport.UnbindAsync(It.IsAny())) + .Returns(() => new ValueTask(unbind.WaitAsync())); + mockTransport + .Setup(transport => transport.DisposeAsync()) + .Returns(() => new ValueTask(stop.WaitAsync())); + mockTransport + .Setup(transport => transport.EndPoint).Returns(e); + + return new ValueTask(mockTransport.Object); + }); var mockLoggerFactory = new Mock(); var mockLogger = new Mock(); @@ -255,9 +260,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var server = new KestrelServer(Options.Create(options), mockTransportFactory.Object, mockLoggerFactory.Object); await server.StartAsync(new DummyApplication(), CancellationToken.None); - var stopTask1 = server.StopAsync(default(CancellationToken)); - var stopTask2 = server.StopAsync(default(CancellationToken)); - var stopTask3 = server.StopAsync(default(CancellationToken)); + var stopTask1 = server.StopAsync(default); + var stopTask2 = server.StopAsync(default); + var stopTask3 = server.StopAsync(default); Assert.False(stopTask1.IsCompleted); Assert.False(stopTask2.IsCompleted); @@ -268,8 +273,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await Task.WhenAll(new[] { stopTask1, stopTask2, stopTask3 }).DefaultTimeout(); - mockTransport.Verify(transport => transport.UnbindAsync(), Times.Once); - mockTransport.Verify(transport => transport.StopAsync(), Times.Once); + mockTransport.Verify(transport => transport.UnbindAsync(It.IsAny()), Times.Once); } [Fact] @@ -286,25 +290,27 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var unbind = new SemaphoreSlim(0); var unbindException = new InvalidOperationException(); - var mockTransport = new Mock(); - mockTransport - .Setup(transport => transport.BindAsync()) - .Returns(Task.CompletedTask); - mockTransport - .Setup(transport => transport.UnbindAsync()) - .Returns(async () => - { - await unbind.WaitAsync(); - throw unbindException; - }); - mockTransport - .Setup(transport => transport.StopAsync()) - .Returns(Task.CompletedTask); - - var mockTransportFactory = new Mock(); + var mockTransport = new Mock(); + var mockTransportFactory = new Mock(); mockTransportFactory - .Setup(transportFactory => transportFactory.Create(It.IsAny(), It.IsAny())) - .Returns(mockTransport.Object); + .Setup(transportFactory => transportFactory.BindAsync(It.IsAny(), It.IsAny())) + .Returns((e, token) => + { + mockTransport + .Setup(transport => transport.AcceptAsync(It.IsAny())) + .Returns(new ValueTask((ConnectionContext)null)); + mockTransport + .Setup(transport => transport.UnbindAsync(It.IsAny())) + .Returns(async () => + { + await unbind.WaitAsync(); + throw unbindException; + }); + mockTransport + .Setup(transport => transport.EndPoint).Returns(e); + + return new ValueTask(mockTransport.Object); + }); var mockLoggerFactory = new Mock(); var mockLogger = new Mock(); @@ -312,9 +318,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var server = new KestrelServer(Options.Create(options), mockTransportFactory.Object, mockLoggerFactory.Object); await server.StartAsync(new DummyApplication(), CancellationToken.None); - var stopTask1 = server.StopAsync(default(CancellationToken)); - var stopTask2 = server.StopAsync(default(CancellationToken)); - var stopTask3 = server.StopAsync(default(CancellationToken)); + var stopTask1 = server.StopAsync(default); + var stopTask2 = server.StopAsync(default); + var stopTask3 = server.StopAsync(default); Assert.False(stopTask1.IsCompleted); Assert.False(stopTask2.IsCompleted); @@ -327,7 +333,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests Assert.Same(unbindException, await Assert.ThrowsAsync(() => stopTask2.TimeoutAfter(timeout))); Assert.Same(unbindException, await Assert.ThrowsAsync(() => stopTask3.TimeoutAfter(timeout))); - mockTransport.Verify(transport => transport.UnbindAsync(), Times.Once); + mockTransport.Verify(transport => transport.UnbindAsync(It.IsAny()), Times.Once); } [Fact] @@ -343,21 +349,23 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var unbindTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var mockTransport = new Mock(); - mockTransport - .Setup(transport => transport.BindAsync()) - .Returns(Task.CompletedTask); - mockTransport - .Setup(transport => transport.UnbindAsync()) - .Returns(unbindTcs.Task); - mockTransport - .Setup(transport => transport.StopAsync()) - .Returns(Task.CompletedTask); - - var mockTransportFactory = new Mock(); + var mockTransport = new Mock(); + var mockTransportFactory = new Mock(); mockTransportFactory - .Setup(transportFactory => transportFactory.Create(It.IsAny(), It.IsAny())) - .Returns(mockTransport.Object); + .Setup(transportFactory => transportFactory.BindAsync(It.IsAny(), It.IsAny())) + .Returns((e, token) => + { + mockTransport + .Setup(transport => transport.AcceptAsync(It.IsAny())) + .Returns(new ValueTask((ConnectionContext)null)); + mockTransport + .Setup(transport => transport.UnbindAsync(It.IsAny())) + .Returns(new ValueTask(unbindTcs.Task)); + mockTransport + .Setup(transport => transport.EndPoint).Returns(e); + + return new ValueTask(mockTransport.Object); + }); var mockLoggerFactory = new Mock(); var mockLogger = new Mock(); @@ -384,7 +392,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await stopTask2.DefaultTimeout(); await continuationTask.DefaultTimeout(); - mockTransport.Verify(transport => transport.UnbindAsync(), Times.Once); + mockTransport.Verify(transport => transport.UnbindAsync(It.IsAny()), Times.Once); } [Fact] @@ -438,11 +446,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests server.StartAsync(new DummyApplication(context => Task.CompletedTask), CancellationToken.None).GetAwaiter().GetResult(); } - private class MockTransportFactory : ITransportFactory + private class MockTransportFactory : IConnectionListenerFactory { - public ITransport Create(IEndPointInformation endPointInformation, IConnectionDispatcher handler) + public ValueTask BindAsync(EndPoint endpoint, CancellationToken cancellationToken = default) { - return Mock.Of(); + var mock = new Mock(); + mock.Setup(m => m.EndPoint).Returns(endpoint); + return new ValueTask(mock.Object); } } } diff --git a/src/Servers/Kestrel/Core/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests.csproj b/src/Servers/Kestrel/Core/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests.csproj index b334a87ed9..fa82cf0beb 100644 --- a/src/Servers/Kestrel/Core/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests.csproj +++ b/src/Servers/Kestrel/Core/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests.csproj @@ -12,12 +12,12 @@ + - + - diff --git a/src/Servers/Kestrel/Core/test/OutputProducerTests.cs b/src/Servers/Kestrel/Core/test/OutputProducerTests.cs index 749e2473a5..53aa300f1c 100644 --- a/src/Servers/Kestrel/Core/test/OutputProducerTests.cs +++ b/src/Servers/Kestrel/Core/test/OutputProducerTests.cs @@ -9,7 +9,6 @@ using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Server.Kestrel.Core.Features; 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.AspNetCore.Testing; using Moq; using Xunit; @@ -22,7 +21,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests public OutputProducerTests() { - _memoryPool = KestrelMemoryPool.Create(); + _memoryPool = MemoryPoolFactory.Create(); } public void Dispose() diff --git a/src/Servers/Kestrel/Core/test/PipeOptionsTests.cs b/src/Servers/Kestrel/Core/test/PipeOptionsTests.cs index 1f5b377360..0b06bec0e9 100644 --- a/src/Servers/Kestrel/Core/test/PipeOptionsTests.cs +++ b/src/Servers/Kestrel/Core/test/PipeOptionsTests.cs @@ -1,9 +1,8 @@ -// 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.IO.Pipelines; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; -using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; using Microsoft.AspNetCore.Testing; using Moq; using Xunit; @@ -12,43 +11,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests { public class PipeOptionsTests { - [Theory] - [InlineData(10, 10, 10)] - [InlineData(0, 1, 1)] - [InlineData(null, 0, 0)] - public void OutputPipeOptionsConfiguredCorrectly(long? maxResponseBufferSize, long expectedMaximumSizeLow, long expectedMaximumSizeHigh) - { - var serviceContext = new TestServiceContext(); - serviceContext.ServerOptions.Limits.MaxResponseBufferSize = maxResponseBufferSize; - serviceContext.Scheduler = PipeScheduler.ThreadPool; - - var mockScheduler = Mock.Of(); - var outputPipeOptions = ConnectionDispatcher.GetOutputPipeOptions(serviceContext, KestrelMemoryPool.Create(), readerScheduler: mockScheduler); - - Assert.Equal(expectedMaximumSizeLow, outputPipeOptions.ResumeWriterThreshold); - Assert.Equal(expectedMaximumSizeHigh, outputPipeOptions.PauseWriterThreshold); - Assert.Same(mockScheduler, outputPipeOptions.ReaderScheduler); - Assert.Same(serviceContext.Scheduler, outputPipeOptions.WriterScheduler); - } - - [Theory] - [InlineData(10, 10, 10)] - [InlineData(null, 0, 0)] - public void InputPipeOptionsConfiguredCorrectly(long? maxRequestBufferSize, long expectedMaximumSizeLow, long expectedMaximumSizeHigh) - { - var serviceContext = new TestServiceContext(); - serviceContext.ServerOptions.Limits.MaxRequestBufferSize = maxRequestBufferSize; - serviceContext.Scheduler = PipeScheduler.ThreadPool; - - var mockScheduler = Mock.Of(); - var inputPipeOptions = ConnectionDispatcher.GetInputPipeOptions(serviceContext, KestrelMemoryPool.Create(), writerScheduler: mockScheduler); - - Assert.Equal(expectedMaximumSizeLow, inputPipeOptions.ResumeWriterThreshold); - Assert.Equal(expectedMaximumSizeHigh, inputPipeOptions.PauseWriterThreshold); - Assert.Same(serviceContext.Scheduler, inputPipeOptions.ReaderScheduler); - Assert.Same(mockScheduler, inputPipeOptions.WriterScheduler); - } - [Theory] [InlineData(10, 10, 10)] [InlineData(null, 0, 0)] diff --git a/src/Servers/Kestrel/Core/test/PipelineExtensionTests.cs b/src/Servers/Kestrel/Core/test/PipelineExtensionTests.cs index 1e5f82677d..e2dfc2efd4 100644 --- a/src/Servers/Kestrel/Core/test/PipelineExtensionTests.cs +++ b/src/Servers/Kestrel/Core/test/PipelineExtensionTests.cs @@ -6,7 +6,6 @@ using System.Buffers; using System.IO.Pipelines; using System.Text; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; -using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; using Xunit; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests @@ -17,7 +16,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests private const int _ulongMaxValueLength = 20; private readonly Pipe _pipe; - private readonly MemoryPool _memoryPool = KestrelMemoryPool.Create(); + private readonly MemoryPool _memoryPool = MemoryPoolFactory.Create(); public PipelineExtensionTests() { diff --git a/src/Servers/Kestrel/Core/test/StartLineTests.cs b/src/Servers/Kestrel/Core/test/StartLineTests.cs index 9b9656fbba..bd4d5f4d5b 100644 --- a/src/Servers/Kestrel/Core/test/StartLineTests.cs +++ b/src/Servers/Kestrel/Core/test/StartLineTests.cs @@ -10,7 +10,6 @@ using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; -using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; using Moq; using Xunit; @@ -500,7 +499,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests public StartLineTests() { - MemoryPool = KestrelMemoryPool.Create(); + MemoryPool = MemoryPoolFactory.Create(); var options = new PipeOptions(MemoryPool, readerScheduler: PipeScheduler.Inline, writerScheduler: PipeScheduler.Inline, useSynchronizationContext: false); var pair = DuplexPipe.CreateConnectionPair(options, options); Transport = pair.Transport; diff --git a/src/Servers/Kestrel/Core/test/TestHelpers/TestInput.cs b/src/Servers/Kestrel/Core/test/TestHelpers/TestInput.cs index 81f046b10d..3a8ce70e67 100644 --- a/src/Servers/Kestrel/Core/test/TestHelpers/TestInput.cs +++ b/src/Servers/Kestrel/Core/test/TestHelpers/TestInput.cs @@ -12,7 +12,6 @@ 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.Infrastructure; -using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; using Microsoft.AspNetCore.Testing; using Moq; @@ -24,7 +23,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests public TestInput() { - _memoryPool = KestrelMemoryPool.Create(); + _memoryPool = MemoryPoolFactory.Create(); var options = new PipeOptions(pool: _memoryPool, readerScheduler: PipeScheduler.Inline, writerScheduler: PipeScheduler.Inline, useSynchronizationContext: false); var pair = DuplexPipe.CreateConnectionPair(options, options); Transport = pair.Transport; diff --git a/src/Servers/Kestrel/Kestrel/src/WebHostBuilderKestrelExtensions.cs b/src/Servers/Kestrel/Kestrel/src/WebHostBuilderKestrelExtensions.cs index 62411b168d..3616679369 100644 --- a/src/Servers/Kestrel/Kestrel/src/WebHostBuilderKestrelExtensions.cs +++ b/src/Servers/Kestrel/Kestrel/src/WebHostBuilderKestrelExtensions.cs @@ -2,10 +2,10 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; -using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; using Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -29,7 +29,7 @@ namespace Microsoft.AspNetCore.Hosting return hostBuilder.ConfigureServices(services => { // Don't override an already-configured transport - services.TryAddSingleton(); + services.TryAddSingleton(); services.AddTransient, KestrelServerOptionsSetup>(); services.AddSingleton(); diff --git a/src/Servers/Kestrel/Kestrel/test/KestrelConfigurationBuilderTests.cs b/src/Servers/Kestrel/Kestrel/test/KestrelConfigurationBuilderTests.cs index c2bb493845..12d32431ae 100644 --- a/src/Servers/Kestrel/Kestrel/test/KestrelConfigurationBuilderTests.cs +++ b/src/Servers/Kestrel/Kestrel/test/KestrelConfigurationBuilderTests.cs @@ -133,7 +133,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Tests serverOptions.ConfigureEndpointDefaults(opt => { - opt.NoDelay = false; opt.Protocols = HttpProtocols.Http2; }); @@ -156,13 +155,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Tests Assert.True(opt.IsHttps); Assert.NotNull(opt.HttpsOptions.ServerCertificate); Assert.Equal(ClientCertificateMode.RequireCertificate, opt.HttpsOptions.ClientCertificateMode); - Assert.False(opt.ListenOptions.NoDelay); Assert.Equal(HttpProtocols.Http2, opt.ListenOptions.Protocols); }) .LocalhostEndpoint(5002, opt => { ran2 = true; - Assert.False(opt.NoDelay); Assert.Equal(HttpProtocols.Http2, opt.Protocols); }) .Load(); @@ -181,7 +178,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Tests serverOptions.ConfigureEndpointDefaults(opt => { - opt.NoDelay = false; opt.UseHttps(TestResources.GetTestCertificate()); }); @@ -202,12 +198,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Tests ran1 = true; Assert.True(opt.IsHttps); Assert.Equal(ClientCertificateMode.RequireCertificate, opt.HttpsOptions.ClientCertificateMode); - Assert.False(opt.ListenOptions.NoDelay); }) .LocalhostEndpoint(5002, opt => { ran2 = true; - Assert.False(opt.NoDelay); }) .Load(); diff --git a/src/Servers/Kestrel/Kestrel/test/Microsoft.AspNetCore.Server.Kestrel.Tests.csproj b/src/Servers/Kestrel/Kestrel/test/Microsoft.AspNetCore.Server.Kestrel.Tests.csproj index 814c905495..cb32fe57c1 100644 --- a/src/Servers/Kestrel/Kestrel/test/Microsoft.AspNetCore.Server.Kestrel.Tests.csproj +++ b/src/Servers/Kestrel/Kestrel/test/Microsoft.AspNetCore.Server.Kestrel.Tests.csproj @@ -1,4 +1,4 @@ - + netcoreapp3.0 @@ -12,7 +12,7 @@ - + @@ -20,7 +20,6 @@ - diff --git a/src/Servers/Kestrel/Kestrel/test/WebHostBuilderKestrelExtensionsTests.cs b/src/Servers/Kestrel/Kestrel/test/WebHostBuilderKestrelExtensionsTests.cs index 7ea16d4c85..fd5c31d032 100644 --- a/src/Servers/Kestrel/Kestrel/test/WebHostBuilderKestrelExtensionsTests.cs +++ b/src/Servers/Kestrel/Kestrel/test/WebHostBuilderKestrelExtensionsTests.cs @@ -1,9 +1,9 @@ // 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; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Server.Kestrel.Core; -using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; using Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal; using Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets; using Microsoft.Extensions.DependencyInjection; @@ -57,7 +57,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Tests .UseKestrel() .Configure(app => { }); - Assert.IsType(hostBuilder.Build().Services.GetService()); + Assert.IsType(hostBuilder.Build().Services.GetService()); } [Fact] @@ -68,14 +68,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Tests .UseLibuv() .Configure(app => { }); - Assert.IsType(hostBuilder.Build().Services.GetService()); + Assert.IsType(hostBuilder.Build().Services.GetService()); var hostBuilderReversed = new WebHostBuilder() .UseLibuv() .UseKestrel() .Configure(app => { }); - Assert.IsType(hostBuilderReversed.Build().Services.GetService()); + Assert.IsType(hostBuilderReversed.Build().Services.GetService()); } [Fact] @@ -86,14 +86,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Tests .UseSockets() .Configure(app => { }); - Assert.IsType(hostBuilder.Build().Services.GetService()); + Assert.IsType(hostBuilder.Build().Services.GetService()); var hostBuilderReversed = new WebHostBuilder() .UseSockets() .UseKestrel() .Configure(app => { }); - Assert.IsType(hostBuilderReversed.Build().Services.GetService()); + Assert.IsType(hostBuilderReversed.Build().Services.GetService()); } } } 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 5420d07eac..618082bc4a 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 @@ -1,113 +1,3 @@ // 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.Transport.Abstractions.Internal -{ - public enum FileHandleType - { - Auto = 0, - Tcp = 1, - Pipe = 2, - } - public partial interface IApplicationTransportFeature - { - System.IO.Pipelines.IDuplexPipe Application { get; set; } - } - public partial interface IConnectionDispatcher - { - System.Threading.Tasks.Task OnConnection(Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal.TransportConnection connection); - } - public partial interface IEndPointInformation - { - ulong FileHandle { get; } - Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal.FileHandleType HandleType { get; set; } - System.Net.IPEndPoint IPEndPoint { get; set; } - bool NoDelay { get; } - string SocketPath { get; } - Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal.ListenType Type { get; } - } - public partial interface ITransport - { - System.Threading.Tasks.Task BindAsync(); - System.Threading.Tasks.Task StopAsync(); - System.Threading.Tasks.Task UnbindAsync(); - } - public partial interface ITransportFactory - { - Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal.ITransport Create(Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal.IEndPointInformation endPointInformation, Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal.IConnectionDispatcher dispatcher); - } - public partial interface ITransportSchedulerFeature - { - System.IO.Pipelines.PipeScheduler InputWriterScheduler { get; } - System.IO.Pipelines.PipeScheduler OutputReaderScheduler { get; } - } - public static partial class KestrelMemoryPool - { - public static readonly int MinimumSegmentSize; - public static System.Buffers.MemoryPool Create() { throw null; } - public static System.Buffers.MemoryPool CreateSlabMemoryPool() { throw null; } - } - public enum ListenType - { - IPEndPoint = 0, - SocketPath = 1, - FileHandle = 2, - } - public enum SchedulingMode - { - Default = 0, - ThreadPool = 1, - Inline = 2, - } - 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() { } - public System.IO.Pipelines.IDuplexPipe Application { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } - public System.Threading.CancellationToken ConnectionClosed { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } - public System.Threading.CancellationToken ConnectionClosedRequested { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } - public override string ConnectionId { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } - public override Microsoft.AspNetCore.Http.Features.IFeatureCollection Features { get { throw null; } } - public System.IO.Pipelines.PipeWriter Input { get { throw null; } } - public virtual System.IO.Pipelines.PipeScheduler InputWriterScheduler { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } - 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 { } } - System.Threading.CancellationToken Microsoft.AspNetCore.Connections.Features.IConnectionLifetimeNotificationFeature.ConnectionClosedRequested { get { throw null; } set { } } - System.IO.Pipelines.IDuplexPipe Microsoft.AspNetCore.Connections.Features.IConnectionTransportFeature.Transport { get { throw null; } set { } } - System.Buffers.MemoryPool Microsoft.AspNetCore.Connections.Features.IMemoryPoolFeature.MemoryPool { get { throw null; } } - bool Microsoft.AspNetCore.Http.Features.IFeatureCollection.IsReadOnly { get { throw null; } } - object Microsoft.AspNetCore.Http.Features.IFeatureCollection.this[System.Type key] { get { throw null; } set { } } - int Microsoft.AspNetCore.Http.Features.IFeatureCollection.Revision { get { throw null; } } - string Microsoft.AspNetCore.Http.Features.IHttpConnectionFeature.ConnectionId { get { throw null; } set { } } - System.Net.IPAddress Microsoft.AspNetCore.Http.Features.IHttpConnectionFeature.LocalIpAddress { get { throw null; } set { } } - int Microsoft.AspNetCore.Http.Features.IHttpConnectionFeature.LocalPort { get { throw null; } set { } } - System.Net.IPAddress Microsoft.AspNetCore.Http.Features.IHttpConnectionFeature.RemoteIpAddress { get { throw null; } set { } } - int Microsoft.AspNetCore.Http.Features.IHttpConnectionFeature.RemotePort { get { throw null; } set { } } - System.IO.Pipelines.IDuplexPipe Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal.IApplicationTransportFeature.Application { get { throw null; } set { } } - System.IO.Pipelines.PipeScheduler Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal.ITransportSchedulerFeature.InputWriterScheduler { get { throw null; } } - System.IO.Pipelines.PipeScheduler Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal.ITransportSchedulerFeature.OutputReaderScheduler { get { throw null; } } - public System.IO.Pipelines.PipeReader Output { get { throw null; } } - public virtual System.IO.Pipelines.PipeScheduler OutputReaderScheduler { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } - public System.Net.IPAddress RemoteAddress { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } - 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() { } - TFeature Microsoft.AspNetCore.Http.Features.IFeatureCollection.Get() { throw null; } - void Microsoft.AspNetCore.Http.Features.IFeatureCollection.Set(TFeature feature) { } - public void OnHeartbeat(System.Action action, object state) { } - public void RequestClose() { } - System.Collections.Generic.IEnumerator> System.Collections.Generic.IEnumerable>.GetEnumerator() { throw null; } - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; } - public void TickHeartbeat() { } - } -} diff --git a/src/Servers/Kestrel/Transport.Abstractions/src/Internal/IApplicationTransportFeature.cs b/src/Servers/Kestrel/Transport.Abstractions/src/Internal/IApplicationTransportFeature.cs deleted file mode 100644 index 8aa8328a6b..0000000000 --- a/src/Servers/Kestrel/Transport.Abstractions/src/Internal/IApplicationTransportFeature.cs +++ /dev/null @@ -1,12 +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.IO.Pipelines; - -namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal -{ - public interface IApplicationTransportFeature - { - IDuplexPipe Application { get; set; } - } -} diff --git a/src/Servers/Kestrel/Transport.Abstractions/src/Internal/IConnectionDispatcher.cs b/src/Servers/Kestrel/Transport.Abstractions/src/Internal/IConnectionDispatcher.cs deleted file mode 100644 index 813c541d1a..0000000000 --- a/src/Servers/Kestrel/Transport.Abstractions/src/Internal/IConnectionDispatcher.cs +++ /dev/null @@ -1,12 +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.Threading.Tasks; - -namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal -{ - public interface IConnectionDispatcher - { - Task OnConnection(TransportConnection connection); - } -} diff --git a/src/Servers/Kestrel/Transport.Abstractions/src/Internal/IEndPointInformation.cs b/src/Servers/Kestrel/Transport.Abstractions/src/Internal/IEndPointInformation.cs deleted file mode 100644 index 1b7abfa497..0000000000 --- a/src/Servers/Kestrel/Transport.Abstractions/src/Internal/IEndPointInformation.cs +++ /dev/null @@ -1,46 +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.Net; - -namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal -{ - public interface IEndPointInformation - { - /// - /// The type of interface being described: either an , Unix domain socket path, or a file descriptor. - /// - ListenType Type { get; } - - // IPEndPoint is mutable so port 0 can be updated to the bound port. - /// - /// The to bind to. - /// Only set if is . - /// - IPEndPoint IPEndPoint { get; set; } - - /// - /// The absolute path to a Unix domain socket to bind to. - /// Only set if is . - /// - string SocketPath { get; } - - /// - /// A file descriptor for the socket to open. - /// Only set if is . - /// - ulong FileHandle { get; } - - // HandleType is mutable so it can be re-specified later. - /// - /// The type of file descriptor being used. - /// Only set if is . - /// - FileHandleType HandleType { get; set; } - - /// - /// Set to false to enable Nagle's algorithm for all connections. - /// - bool NoDelay { get; } - } -} diff --git a/src/Servers/Kestrel/Transport.Abstractions/src/Internal/ITransport.cs b/src/Servers/Kestrel/Transport.Abstractions/src/Internal/ITransport.cs deleted file mode 100644 index 5a6dc0c20c..0000000000 --- a/src/Servers/Kestrel/Transport.Abstractions/src/Internal/ITransport.cs +++ /dev/null @@ -1,15 +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.Threading.Tasks; - -namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal -{ - public interface ITransport - { - // Can only be called once per ITransport - Task BindAsync(); - Task UnbindAsync(); - Task StopAsync(); - } -} diff --git a/src/Servers/Kestrel/Transport.Abstractions/src/Internal/ITransportFactory.cs b/src/Servers/Kestrel/Transport.Abstractions/src/Internal/ITransportFactory.cs deleted file mode 100644 index 4037467e87..0000000000 --- a/src/Servers/Kestrel/Transport.Abstractions/src/Internal/ITransportFactory.cs +++ /dev/null @@ -1,10 +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. - -namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal -{ - public interface ITransportFactory - { - ITransport Create(IEndPointInformation endPointInformation, IConnectionDispatcher dispatcher); - } -} diff --git a/src/Servers/Kestrel/Transport.Abstractions/src/Internal/ITransportSchedulerFeature.cs b/src/Servers/Kestrel/Transport.Abstractions/src/Internal/ITransportSchedulerFeature.cs deleted file mode 100644 index c4df6d5a37..0000000000 --- a/src/Servers/Kestrel/Transport.Abstractions/src/Internal/ITransportSchedulerFeature.cs +++ /dev/null @@ -1,14 +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.IO.Pipelines; - -namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal -{ - public interface ITransportSchedulerFeature - { - PipeScheduler InputWriterScheduler { get; } - - PipeScheduler OutputReaderScheduler { get; } - } -} diff --git a/src/Servers/Kestrel/Transport.Abstractions/src/Internal/ListenType.cs b/src/Servers/Kestrel/Transport.Abstractions/src/Internal/ListenType.cs deleted file mode 100644 index 3616f1967e..0000000000 --- a/src/Servers/Kestrel/Transport.Abstractions/src/Internal/ListenType.cs +++ /dev/null @@ -1,15 +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. - -namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal -{ - /// - /// Enumerates the types. - /// - public enum ListenType - { - IPEndPoint, - SocketPath, - FileHandle - } -} diff --git a/src/Servers/Kestrel/Transport.Abstractions/src/Internal/SchedulingMode.cs b/src/Servers/Kestrel/Transport.Abstractions/src/Internal/SchedulingMode.cs deleted file mode 100644 index 881006087c..0000000000 --- a/src/Servers/Kestrel/Transport.Abstractions/src/Internal/SchedulingMode.cs +++ /dev/null @@ -1,12 +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. - -namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal -{ - public enum SchedulingMode - { - Default, - ThreadPool, - Inline - } -} diff --git a/src/Servers/Kestrel/Transport.Abstractions/src/Internal/TransportConnection.FeatureCollection.cs b/src/Servers/Kestrel/Transport.Abstractions/src/Internal/TransportConnection.FeatureCollection.cs deleted file mode 100644 index 7f98162507..0000000000 --- a/src/Servers/Kestrel/Transport.Abstractions/src/Internal/TransportConnection.FeatureCollection.cs +++ /dev/null @@ -1,188 +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.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 -{ - public partial class TransportConnection : IHttpConnectionFeature, - IConnectionIdFeature, - IConnectionTransportFeature, - IConnectionItemsFeature, - IMemoryPoolFeature, - IApplicationTransportFeature, - ITransportSchedulerFeature, - IConnectionLifetimeFeature, - IConnectionHeartbeatFeature, - 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; - set => ConnectionId = value; - } - - IPAddress IHttpConnectionFeature.RemoteIpAddress - { - get => RemoteAddress; - set => RemoteAddress = value; - } - - IPAddress IHttpConnectionFeature.LocalIpAddress - { - get => LocalAddress; - set => LocalAddress = value; - } - - int IHttpConnectionFeature.RemotePort - { - get => RemotePort; - set => RemotePort = value; - } - - int IHttpConnectionFeature.LocalPort - { - get => LocalPort; - set => LocalPort = value; - } - - MemoryPool IMemoryPoolFeature.MemoryPool => MemoryPool; - - IDuplexPipe IConnectionTransportFeature.Transport - { - get => Transport; - set => Transport = value; - } - - IDuplexPipe IApplicationTransportFeature.Application - { - get => Application; - set => Application = value; - } - - IDictionary IConnectionItemsFeature.Items - { - get => Items; - set => Items = value; - } - - PipeScheduler ITransportSchedulerFeature.InputWriterScheduler => InputWriterScheduler; - PipeScheduler ITransportSchedulerFeature.OutputReaderScheduler => OutputReaderScheduler; - - CancellationToken IConnectionLifetimeFeature.ConnectionClosed - { - get => ConnectionClosed; - set => ConnectionClosed = value; - } - - CancellationToken IConnectionLifetimeNotificationFeature.ConnectionClosedRequested - { - get => ConnectionClosedRequested; - set => ConnectionClosedRequested = value; - } - - void IConnectionLifetimeFeature.Abort() => Abort(new ConnectionAbortedException("The connection was aborted by the application via IConnectionLifetimeFeature.Abort().")); - - void IConnectionLifetimeNotificationFeature.RequestClose() => RequestClose(); - - void IConnectionHeartbeatFeature.OnHeartbeat(System.Action action, object state) - { - 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.cs b/src/Servers/Kestrel/Transport.Abstractions/src/Internal/TransportConnection.cs deleted file mode 100644 index e3d012cc27..0000000000 --- a/src/Servers/Kestrel/Transport.Abstractions/src/Internal/TransportConnection.cs +++ /dev/null @@ -1,121 +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.Buffers; -using System.Collections.Generic; -using System.IO.Pipelines; -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 -{ - public abstract partial class TransportConnection : ConnectionContext - { - private IDictionary _items; - private List<(Action handler, object state)> _heartbeatHandlers; - private readonly object _heartbeatLock = new object(); - protected readonly CancellationTokenSource _connectionClosingCts = new CancellationTokenSource(); - - public TransportConnection() - { - FastReset(); - - ConnectionClosedRequested = _connectionClosingCts.Token; - } - - public IPAddress RemoteAddress { get; set; } - public int RemotePort { get; set; } - public IPAddress LocalAddress { get; set; } - public int LocalPort { get; set; } - - public override string ConnectionId { get; set; } - - 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; } - - public override IDuplexPipe Transport { get; set; } - public IDuplexPipe Application { get; set; } - - public override IDictionary Items - { - get - { - // Lazily allocate connection metadata - return _items ?? (_items = new ConnectionItems()); - } - set - { - _items = value; - } - } - - public PipeWriter Input => Application.Output; - public PipeReader Output => Application.Input; - - public CancellationToken ConnectionClosed { get; set; } - - public CancellationToken ConnectionClosedRequested { get; set; } - - public void TickHeartbeat() - { - lock (_heartbeatLock) - { - if (_heartbeatHandlers == null) - { - return; - } - - foreach (var (handler, state) in _heartbeatHandlers) - { - handler(state); - } - } - } - - public void OnHeartbeat(Action action, object state) - { - lock (_heartbeatLock) - { - if (_heartbeatHandlers == null) - { - _heartbeatHandlers = new List<(Action handler, object state)>(); - } - - _heartbeatHandlers.Add((action, state)); - } - } - - // 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. - // That said, all derived types should override this method should override - // this implementation of Abort because canceling pending output reads is not - // sufficient to abort the connection if there is backpressure. - public override void Abort(ConnectionAbortedException abortReason) - { - Output.CancelPendingRead(); - } - - public void RequestClose() - { - try - { - _connectionClosingCts.Cancel(); - } - catch (ObjectDisposedException) - { - // There's a race where the token could be disposed - // swallow the exception and no-op - } - } - } -} 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 9f9270d232..a20f6d2640 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 @@ -1,4 +1,4 @@ - + Transport abstractions for the ASP.NET Core Kestrel cross-platform web server. diff --git a/src/Servers/Kestrel/Transport.Libuv/ref/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.csproj b/src/Servers/Kestrel/Transport.Libuv/ref/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.csproj index 381c885bdd..366ac6ec5c 100644 --- a/src/Servers/Kestrel/Transport.Libuv/ref/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.csproj +++ b/src/Servers/Kestrel/Transport.Libuv/ref/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.csproj @@ -7,8 +7,9 @@ - + + diff --git a/src/Servers/Kestrel/Transport.Libuv/ref/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.netcoreapp3.0.cs b/src/Servers/Kestrel/Transport.Libuv/ref/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.netcoreapp3.0.cs index 8e37f8714d..f24337e79d 100644 --- a/src/Servers/Kestrel/Transport.Libuv/ref/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.netcoreapp3.0.cs +++ b/src/Servers/Kestrel/Transport.Libuv/ref/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.netcoreapp3.0.cs @@ -14,6 +14,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv public partial class LibuvTransportOptions { public LibuvTransportOptions() { } + public long? MaxReadBufferSize { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + public long? MaxWriteBufferSize { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + public bool NoDelay { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } public int ThreadCount { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } } } diff --git a/src/Servers/Kestrel/Transport.Libuv/src/Internal/LibuvConnection.cs b/src/Servers/Kestrel/Transport.Libuv/src/Internal/LibuvConnection.cs index adcc5cd09d..962359bd8f 100644 --- a/src/Servers/Kestrel/Transport.Libuv/src/Internal/LibuvConnection.cs +++ b/src/Servers/Kestrel/Transport.Libuv/src/Internal/LibuvConnection.cs @@ -9,15 +9,14 @@ using System.Net; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; -using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; using Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal.Networking; using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal { - internal partial class LibuvConnection : TransportConnection, IDisposable + internal partial class LibuvConnection : TransportConnection { - private static readonly int MinAllocBufferSize = KestrelMemoryPool.MinimumSegmentSize / 2; + private static readonly int MinAllocBufferSize = SlabMemoryPool.BlockSize / 2; private static readonly Action _readCallback = (handle, status, state) => ReadCallback(handle, status, state); @@ -31,31 +30,55 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal private volatile ConnectionAbortedException _abortReason; private MemoryHandle _bufferHandle; + private Task _processingTask; - public LibuvConnection(UvStreamHandle socket, ILibuvTrace log, LibuvThread thread, IPEndPoint remoteEndPoint, IPEndPoint localEndPoint) + public LibuvConnection(UvStreamHandle socket, + ILibuvTrace log, + LibuvThread thread, + IPEndPoint remoteEndPoint, + IPEndPoint localEndPoint, + PipeOptions inputOptions = null, + PipeOptions outputOptions = null, + long? maxReadBufferSize = null, + long? maxWriteBufferSize = null) { _socket = socket; - RemoteAddress = remoteEndPoint?.Address; - RemotePort = remoteEndPoint?.Port ?? 0; - - LocalAddress = localEndPoint?.Address; - LocalPort = localEndPoint?.Port ?? 0; + LocalEndPoint = localEndPoint; + RemoteEndPoint = remoteEndPoint; ConnectionClosed = _connectionClosedTokenSource.Token; - Logger = log; Log = log; Thread = thread; + + maxReadBufferSize ??= 0; + maxWriteBufferSize ??= 0; + + inputOptions ??= new PipeOptions(MemoryPool, PipeScheduler.ThreadPool, Thread, maxReadBufferSize.Value, maxReadBufferSize.Value / 2, useSynchronizationContext: false); + outputOptions ??= new PipeOptions(MemoryPool, Thread, PipeScheduler.ThreadPool, maxWriteBufferSize.Value, maxWriteBufferSize.Value / 2, useSynchronizationContext: false); + + var pair = DuplexPipe.CreateConnectionPair(inputOptions, outputOptions); + + // Set the transport and connection id + Transport = pair.Transport; + Application = pair.Application; } + public PipeWriter Input => Application.Output; + + public PipeReader Output => Application.Input; + public LibuvOutputConsumer OutputConsumer { get; set; } private ILibuvTrace Log { get; } private LibuvThread Thread { get; } public override MemoryPool MemoryPool => Thread.MemoryPool; - public override PipeScheduler InputWriterScheduler => Thread; - public override PipeScheduler OutputReaderScheduler => Thread; - public async Task Start() + public void Start() + { + _processingTask = StartCore(); + } + + private async Task StartCore() { try { @@ -96,7 +119,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal } finally { - inputError = inputError ?? _abortReason ?? new ConnectionAbortedException("The libuv transport's send loop completed gracefully."); + inputError ??= _abortReason ?? new ConnectionAbortedException("The libuv transport's send loop completed gracefully."); // Now, complete the input so that no more reads can happen Input.Complete(inputError); @@ -111,7 +134,22 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal // We're done with the socket now _socket.Dispose(); - ThreadPool.UnsafeQueueUserWorkItem(state => ((LibuvConnection)state).CancelConnectionClosedToken(), this); + + // Fire the connection closed token and wait for it to complete + var waitForConnectionClosedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + ThreadPool.UnsafeQueueUserWorkItem(state => + { + (var connection, var tcs) = state; + + connection.CancelConnectionClosedToken(); + + tcs.TrySetResult(null); + }, + (this, waitForConnectionClosedTcs), + preferLocal: false); + + await waitForConnectionClosedTcs.Task; } } catch (Exception e) @@ -123,7 +161,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal public override void Abort(ConnectionAbortedException abortReason) { _abortReason = abortReason; - + // Cancel WriteOutputAsync loop after setting _abortReason. Output.CancelPendingRead(); @@ -131,11 +169,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal Thread.Post(s => s.Dispose(), _socket); } - // Only called after connection middleware is complete which means the ConnectionClosed token has fired. - public void Dispose() + public override async ValueTask DisposeAsync() { + Transport.Input.Complete(); + Transport.Output.Complete(); + + if (_processingTask != null) + { + await _processingTask; + } + _connectionClosedTokenSource.Dispose(); - _connectionClosingCts.Dispose(); } // Called on Libuv thread diff --git a/src/Servers/Kestrel/Transport.Libuv/src/Internal/LibuvTransport.cs b/src/Servers/Kestrel/Transport.Libuv/src/Internal/LibuvConnectionListener.cs similarity index 51% rename from src/Servers/Kestrel/Transport.Libuv/src/Internal/LibuvTransport.cs rename to src/Servers/Kestrel/Transport.Libuv/src/Internal/LibuvConnectionListener.cs index 4bbeae88f1..d1b2e9a8e7 100644 --- a/src/Servers/Kestrel/Transport.Libuv/src/Internal/LibuvTransport.cs +++ b/src/Servers/Kestrel/Transport.Libuv/src/Internal/LibuvConnectionListener.cs @@ -4,32 +4,34 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net; +using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; -using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; using Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal.Networking; -using Microsoft.Extensions.Logging; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal { - internal class LibuvTransport : ITransport + internal class LibuvConnectionListener : IConnectionListener { - private readonly IEndPointInformation _endPointInformation; + private readonly List _listeners = new List(); + private IAsyncEnumerator _acceptEnumerator; + private bool _stopped; + private bool _disposed; - private readonly List _listeners = new List(); - - public LibuvTransport(LibuvTransportContext context, IEndPointInformation endPointInformation) - : this(new LibuvFunctions(), context, endPointInformation) + public LibuvConnectionListener(LibuvTransportContext context, EndPoint endPoint) + : this(new LibuvFunctions(), context, endPoint) { } // For testing - public LibuvTransport(LibuvFunctions uv, LibuvTransportContext context, IEndPointInformation endPointInformation) + public LibuvConnectionListener(LibuvFunctions uv, LibuvTransportContext context, EndPoint endPoint) { Libuv = uv; TransportContext = context; - _endPointInformation = endPointInformation; + EndPoint = endPoint; } public LibuvFunctions Libuv { get; } @@ -40,7 +42,64 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal public ILibuvTrace Log => TransportContext.Log; public LibuvTransportOptions TransportOptions => TransportContext.Options; - public async Task StopAsync() + public EndPoint EndPoint { get; set; } + + public async ValueTask AcceptAsync(CancellationToken cancellationToken = default) + { + if (_disposed) + { + throw new ObjectDisposedException(GetType().FullName); + } + + if (await _acceptEnumerator.MoveNextAsync()) + { + return _acceptEnumerator.Current; + } + + // null means we're done... + return null; + } + + public async ValueTask UnbindAsync(CancellationToken cancellationToken = default) + { + if (_stopped) + { + return; + } + + _stopped = true; + + var disposeTasks = _listeners.Select(listener => ((IAsyncDisposable)listener).DisposeAsync()).ToArray(); + + if (!await WaitAsync(Task.WhenAll(disposeTasks), TimeSpan.FromSeconds(5)).ConfigureAwait(false)) + { + Log.LogError(0, null, "Disposing listeners failed"); + } + } + + + public async ValueTask DisposeAsync() + { + if (_disposed) + { + return; + } + + _disposed = true; + + await UnbindAsync().ConfigureAwait(false); + + foreach (var listener in _listeners) + { + await listener.AbortQueuedConnectionAsync().ConfigureAwait(false); + } + + _listeners.Clear(); + + await StopThreadsAsync().ConfigureAwait(false); + } + + internal async Task StopThreadsAsync() { try { @@ -68,13 +127,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal #endif } - public async Task BindAsync() + internal async Task BindAsync() { // TODO: Move thread management to LibuvTransportFactory // TODO: Split endpoint management from thread management for (var index = 0; index < TransportOptions.ThreadCount; index++) { - Threads.Add(new LibuvThread(this)); + Threads.Add(new LibuvThread(Libuv, TransportContext)); } foreach (var thread in Threads) @@ -88,7 +147,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal { var listener = new Listener(TransportContext); _listeners.Add(listener); - await listener.StartAsync(_endPointInformation, Threads[0]).ConfigureAwait(false); + await listener.StartAsync(EndPoint, Threads[0]).ConfigureAwait(false); + EndPoint = listener.EndPoint; } else { @@ -97,15 +157,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal var listenerPrimary = new ListenerPrimary(TransportContext); _listeners.Add(listenerPrimary); - await listenerPrimary.StartAsync(pipeName, pipeMessage, _endPointInformation, Threads[0]).ConfigureAwait(false); + await listenerPrimary.StartAsync(pipeName, pipeMessage, EndPoint, Threads[0]).ConfigureAwait(false); + EndPoint = listenerPrimary.EndPoint; foreach (var thread in Threads.Skip(1)) { var listenerSecondary = new ListenerSecondary(TransportContext); _listeners.Add(listenerSecondary); - await listenerSecondary.StartAsync(pipeName, pipeMessage, _endPointInformation, thread).ConfigureAwait(false); + await listenerSecondary.StartAsync(pipeName, pipeMessage, EndPoint, thread).ConfigureAwait(false); } } + _acceptEnumerator = AcceptConnections(); } catch (UvException ex) when (ex.StatusCode == LibuvConstants.EADDRINUSE) { @@ -119,16 +181,45 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal } } - public async Task UnbindAsync() + private async IAsyncEnumerator AcceptConnections() { - var disposeTasks = _listeners.Select(listener => listener.DisposeAsync()).ToArray(); + var slots = new Task<(LibuvConnection, int)>[_listeners.Count]; + // This is the task we'll put in the slot when each listening completes. It'll prevent + // us from having to shrink the array. We'll just loop while there are active slots. + var incompleteTask = new TaskCompletionSource<(LibuvConnection, int)>().Task; - if (!await WaitAsync(Task.WhenAll(disposeTasks), TimeSpan.FromSeconds(5)).ConfigureAwait(false)) + var remainingSlots = slots.Length; + + // Issue parallel accepts on all listeners + for (int i = 0; i < remainingSlots; i++) { - Log.LogError(0, null, "Disposing listeners failed"); + slots[i] = AcceptAsync(_listeners[i], i); } - _listeners.Clear(); + while (remainingSlots > 0) + { + // Calling GetAwaiter().GetResult() is safe because we know the task is completed + (var connection, var slot) = (await Task.WhenAny(slots)).GetAwaiter().GetResult(); + + // If the connection is null then the listener was closed + if (connection == null) + { + remainingSlots--; + slots[slot] = incompleteTask; + } + else + { + // Fill that slot with another accept and yield the connection + slots[slot] = AcceptAsync(_listeners[slot], slot); + + yield return connection; + } + } + + static async Task<(LibuvConnection, int)> AcceptAsync(ListenerContext listener, int slot) + { + return (await listener.AcceptAsync(), slot); + } } private static async Task WaitAsync(Task task, TimeSpan timeout) diff --git a/src/Servers/Kestrel/Transport.Libuv/src/Internal/LibuvThread.cs b/src/Servers/Kestrel/Transport.Libuv/src/Internal/LibuvThread.cs index 9d50eb5f67..de212ca89d 100644 --- a/src/Servers/Kestrel/Transport.Libuv/src/Internal/LibuvThread.cs +++ b/src/Servers/Kestrel/Transport.Libuv/src/Internal/LibuvThread.cs @@ -20,9 +20,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal // maximum times the work queues swapped and are processed in a single pass // as completing a task may immediately have write data to put on the network // otherwise it needs to wait till the next pass of the libuv loop - private readonly int _maxLoops = 8; + private readonly int _maxLoops; - private readonly LibuvTransport _transport; + private readonly LibuvFunctions _libuv; private readonly IHostApplicationLifetime _appLifetime; private readonly Thread _thread; private readonly TaskCompletionSource _threadTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -40,13 +40,19 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal private Exception _closeError; private readonly ILibuvTrace _log; - public LibuvThread(LibuvTransport transport) + public LibuvThread(LibuvFunctions libuv, LibuvTransportContext libuvTransportContext, int maxLoops = 8) + : this(libuv, libuvTransportContext.AppLifetime, libuvTransportContext.Options.MemoryPoolFactory(), libuvTransportContext.Log, maxLoops) { - _transport = transport; - _appLifetime = transport.AppLifetime; - _log = transport.Log; + } + + public LibuvThread(LibuvFunctions libuv, IHostApplicationLifetime appLifetime, MemoryPool pool, ILibuvTrace log, int maxLoops = 8) + { + _libuv = libuv; + _appLifetime = appLifetime; + _log = log; _loop = new UvLoopHandle(_log); _post = new UvAsyncHandle(_log); + _maxLoops = maxLoops; _thread = new Thread(ThreadStart); #if !INNER_LOOP @@ -60,17 +66,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal #endif QueueCloseHandle = PostCloseHandle; QueueCloseAsyncHandle = EnqueueCloseHandle; - MemoryPool = transport.TransportOptions.MemoryPoolFactory(); + MemoryPool = pool; WriteReqPool = new WriteReqPool(this, _log); } - // For testing - public LibuvThread(LibuvTransport transport, int maxLoops) - : this(transport) - { - _maxLoops = maxLoops; - } - public UvLoopHandle Loop { get { return _loop; } } public MemoryPool MemoryPool { get; } @@ -113,13 +112,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal Post(t => t.AllowStop()); if (!await WaitAsync(_threadTcs.Task, stepTimeout).ConfigureAwait(false)) { + _log.LogCritical($"{nameof(LibuvThread)}.{nameof(StopAsync)} failed to terminate libuv thread, {nameof(AllowStop)}"); + Post(t => t.OnStopRude()); if (!await WaitAsync(_threadTcs.Task, stepTimeout).ConfigureAwait(false)) { + _log.LogCritical($"{nameof(LibuvThread)}.{nameof(StopAsync)} failed to terminate libuv thread, {nameof(OnStopRude)}."); + Post(t => t.OnStopImmediate()); if (!await WaitAsync(_threadTcs.Task, stepTimeout).ConfigureAwait(false)) { - _log.LogCritical($"{nameof(LibuvThread)}.{nameof(StopAsync)} failed to terminate libuv thread."); + _log.LogCritical($"{nameof(LibuvThread)}.{nameof(StopAsync)} failed to terminate libuv thread, {nameof(OnStopImmediate)}."); } } } @@ -253,7 +256,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal private void Walk(LibuvFunctions.uv_walk_cb callback, IntPtr arg) { - _transport.Libuv.walk( + _libuv.walk( _loop, callback, arg @@ -282,7 +285,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal var tcs = (TaskCompletionSource)parameter; try { - _loop.Init(_transport.Libuv); + _loop.Init(_libuv); _post.Init(_loop, OnPost, EnqueueCloseHandle); _initCompleted = true; tcs.SetResult(0); diff --git a/src/Servers/Kestrel/Transport.Libuv/src/Internal/LibuvTransportContext.cs b/src/Servers/Kestrel/Transport.Libuv/src/Internal/LibuvTransportContext.cs index 7f9e1d62f9..973a0fec06 100644 --- a/src/Servers/Kestrel/Transport.Libuv/src/Internal/LibuvTransportContext.cs +++ b/src/Servers/Kestrel/Transport.Libuv/src/Internal/LibuvTransportContext.cs @@ -1,7 +1,6 @@ // 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.Server.Kestrel.Transport.Abstractions.Internal; using Microsoft.Extensions.Hosting; namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal @@ -13,7 +12,5 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal public IHostApplicationLifetime AppLifetime { get; set; } public ILibuvTrace Log { get; set; } - - public IConnectionDispatcher ConnectionDispatcher { get; set; } } } diff --git a/src/Servers/Kestrel/Transport.Libuv/src/Internal/LibuvTransportFactory.cs b/src/Servers/Kestrel/Transport.Libuv/src/Internal/LibuvTransportFactory.cs index 485582355d..689c473195 100644 --- a/src/Servers/Kestrel/Transport.Libuv/src/Internal/LibuvTransportFactory.cs +++ b/src/Servers/Kestrel/Transport.Libuv/src/Internal/LibuvTransportFactory.cs @@ -2,14 +2,17 @@ // 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.Transport.Abstractions.Internal; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Connections; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal { - internal class LibuvTransportFactory : ITransportFactory + internal class LibuvTransportFactory : IConnectionListenerFactory { private readonly LibuvTransportContext _baseTransportContext; @@ -31,7 +34,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal throw new ArgumentNullException(nameof(loggerFactory)); } - var logger = loggerFactory.CreateLogger("Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv"); + var logger = loggerFactory.CreateLogger("Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv"); var trace = new LibuvTrace(logger); var threadCount = options.Value.ThreadCount; @@ -61,17 +64,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal }; } - public ITransport Create(IEndPointInformation endPointInformation, IConnectionDispatcher dispatcher) + public async ValueTask BindAsync(EndPoint endpoint, CancellationToken cancellationToken = default) { var transportContext = new LibuvTransportContext { Options = _baseTransportContext.Options, AppLifetime = _baseTransportContext.AppLifetime, - Log = _baseTransportContext.Log, - ConnectionDispatcher = dispatcher + Log = _baseTransportContext.Log }; - return new LibuvTransport(transportContext, endPointInformation); + var transport = new LibuvConnectionListener(transportContext, endpoint); + await transport.BindAsync(); + return transport; } } } diff --git a/src/Servers/Kestrel/Transport.Libuv/src/Internal/Listener.cs b/src/Servers/Kestrel/Transport.Libuv/src/Internal/Listener.cs index 010a84e006..9dc11a31a4 100644 --- a/src/Servers/Kestrel/Transport.Libuv/src/Internal/Listener.cs +++ b/src/Servers/Kestrel/Transport.Libuv/src/Internal/Listener.cs @@ -2,8 +2,10 @@ // 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.Net.Sockets; using System.Threading.Tasks; -using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; +using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal.Networking; using Microsoft.Extensions.Logging; @@ -14,6 +16,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal /// internal class Listener : ListenerContext, IAsyncDisposable { + // REVIEW: This needs to be bounded and we need a strategy for what to do when the queue is full private bool _closed; public Listener(LibuvTransportContext transportContext) : base(transportContext) @@ -25,10 +28,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal public ILibuvTrace Log => TransportContext.Log; public Task StartAsync( - IEndPointInformation endPointInformation, + EndPoint endPoint, LibuvThread thread) { - EndPointInformation = endPointInformation; + EndPoint = endPoint; Thread = thread; return Thread.PostAsync(listener => @@ -43,13 +46,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal /// private UvStreamHandle CreateListenSocket() { - switch (EndPointInformation.Type) + switch (EndPoint) { - case ListenType.IPEndPoint: + case IPEndPoint _: return ListenTcp(useFileHandle: false); - case ListenType.SocketPath: + case UnixDomainSocketEndPoint _: return ListenPipe(useFileHandle: false); - case ListenType.FileHandle: + case FileHandleEndPoint _: return ListenHandle(); default: throw new NotSupportedException(); @@ -63,18 +66,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal try { socket.Init(Thread.Loop, Thread.QueueCloseHandle); - socket.NoDelay(EndPointInformation.NoDelay); + socket.NoDelay(TransportContext.Options.NoDelay); if (!useFileHandle) { - socket.Bind(EndPointInformation.IPEndPoint); + socket.Bind((IPEndPoint)EndPoint); // If requested port was "0", replace with assigned dynamic port. - EndPointInformation.IPEndPoint = socket.GetSockIPEndPoint(); + EndPoint = socket.GetSockIPEndPoint(); } else { - socket.Open((IntPtr)EndPointInformation.FileHandle); + socket.Open((IntPtr)((FileHandleEndPoint)EndPoint).FileHandle); } } catch @@ -96,11 +99,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal if (!useFileHandle) { - pipe.Bind(EndPointInformation.SocketPath); + // UnixDomainSocketEndPoint.ToString() returns the path + pipe.Bind(EndPoint.ToString()); } else { - pipe.Open((IntPtr)EndPointInformation.FileHandle); + pipe.Open((IntPtr)((FileHandleEndPoint)EndPoint).FileHandle); } } catch @@ -114,7 +118,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal private UvStreamHandle ListenHandle() { - switch (EndPointInformation.HandleType) + var handleEndPoint = (FileHandleEndPoint)EndPoint; + + switch (handleEndPoint.FileHandleType) { case FileHandleType.Auto: break; @@ -130,7 +136,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal try { handle = ListenTcp(useFileHandle: true); - EndPointInformation.HandleType = FileHandleType.Tcp; + EndPoint = new FileHandleEndPoint(handleEndPoint.FileHandle, FileHandleType.Tcp); return handle; } catch (UvException exception) when (exception.StatusCode == LibuvConstants.ENOTSUP) @@ -139,7 +145,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal } handle = ListenPipe(useFileHandle: true); - EndPointInformation.HandleType = FileHandleType.Pipe; + EndPoint = new FileHandleEndPoint(handleEndPoint.FileHandle, FileHandleType.Pipe); return handle; } @@ -186,9 +192,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal protected virtual void DispatchConnection(UvStreamHandle socket) { - // REVIEW: This task should be tracked by the server for graceful shutdown - // Today it's handled specifically for http but not for arbitrary middleware - _ = HandleConnectionAsync(socket); + HandleConnection(socket); } public virtual async Task DisposeAsync() @@ -205,6 +209,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal listener._closed = true; + listener.StopAcceptingConnections(); + }, this).ConfigureAwait(false); } diff --git a/src/Servers/Kestrel/Transport.Libuv/src/Internal/ListenerContext.cs b/src/Servers/Kestrel/Transport.Libuv/src/Internal/ListenerContext.cs index e153565af8..528ec5ca3b 100644 --- a/src/Servers/Kestrel/Transport.Libuv/src/Internal/ListenerContext.cs +++ b/src/Servers/Kestrel/Transport.Libuv/src/Internal/ListenerContext.cs @@ -2,9 +2,14 @@ // 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.IO.Pipelines; using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Channels; using System.Threading.Tasks; -using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; +using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal.Networking; using Microsoft.Extensions.Logging; @@ -12,6 +17,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal { internal class ListenerContext { + // Single reader, single writer queue since all writes happen from the uv thread and reads happen sequentially + private readonly Channel _acceptQueue = Channel.CreateUnbounded(new UnboundedChannelOptions + { + SingleReader = true, + SingleWriter = true + }); + public ListenerContext(LibuvTransportContext transportContext) { TransportContext = transportContext; @@ -19,29 +31,62 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal public LibuvTransportContext TransportContext { get; set; } - public IEndPointInformation EndPointInformation { get; set; } + public EndPoint EndPoint { get; set; } public LibuvThread Thread { get; set; } + public PipeOptions InputOptions { get; set; } + + public PipeOptions OutputOptions { get; set; } + + public async ValueTask AcceptAsync(CancellationToken cancellationToken = default) + { + while (await _acceptQueue.Reader.WaitToReadAsync()) + { + while (_acceptQueue.Reader.TryRead(out var connection)) + { + return connection; + } + } + + return null; + } + + /// + /// Aborts all unaccepted connections in the queue + /// + /// + public async Task AbortQueuedConnectionAsync() + { + while (await _acceptQueue.Reader.WaitToReadAsync()) + { + while (_acceptQueue.Reader.TryRead(out var connection)) + { + // REVIEW: Pass an abort reason? + connection.Abort(); + } + } + } + /// /// Creates a socket which can be used to accept an incoming connection. /// protected UvStreamHandle CreateAcceptSocket() { - switch (EndPointInformation.Type) + switch (EndPoint) { - case ListenType.IPEndPoint: + case IPEndPoint _: return AcceptTcp(); - case ListenType.SocketPath: + case UnixDomainSocketEndPoint _: return AcceptPipe(); - case ListenType.FileHandle: + case FileHandleEndPoint _: return AcceptHandle(); default: throw new InvalidOperationException(); } } - protected async Task HandleConnectionAsync(UvStreamHandle socket) + protected internal void HandleConnection(UvStreamHandle socket) { try { @@ -63,18 +108,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal } } - var connection = new LibuvConnection(socket, TransportContext.Log, Thread, remoteEndPoint, localEndPoint); - var middlewareTask = TransportContext.ConnectionDispatcher.OnConnection(connection); - var transportTask = connection.Start(); + var options = TransportContext.Options; + var connection = new LibuvConnection(socket, TransportContext.Log, Thread, remoteEndPoint, localEndPoint, InputOptions, OutputOptions, options.MaxReadBufferSize, options.MaxWriteBufferSize); + connection.Start(); - await transportTask; - await middlewareTask; - - connection.Dispose(); + bool accepted = _acceptQueue.Writer.TryWrite(connection); + Debug.Assert(accepted, "The connection was not written to the channel!"); } catch (Exception ex) { - TransportContext.Log.LogCritical(ex, $"Unexpected exception in {nameof(ListenerContext)}.{nameof(HandleConnectionAsync)}."); + TransportContext.Log.LogCritical(ex, $"Unexpected exception in {nameof(ListenerContext)}.{nameof(HandleConnection)}."); } } @@ -85,7 +128,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal try { socket.Init(Thread.Loop, Thread.QueueCloseHandle); - socket.NoDelay(EndPointInformation.NoDelay); + socket.NoDelay(TransportContext.Options.NoDelay); } catch { @@ -113,9 +156,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal return pipe; } + protected void StopAcceptingConnections() + { + _acceptQueue.Writer.TryComplete(); + } + private UvStreamHandle AcceptHandle() { - switch (EndPointInformation.HandleType) + var fileHandleEndPoint = (FileHandleEndPoint)EndPoint; + + switch (fileHandleEndPoint.FileHandleType) { case FileHandleType.Auto: throw new InvalidOperationException("Cannot accept on a non-specific file handle, listen should be performed first."); diff --git a/src/Servers/Kestrel/Transport.Libuv/src/Internal/ListenerPrimary.cs b/src/Servers/Kestrel/Transport.Libuv/src/Internal/ListenerPrimary.cs index 4ce0afab29..acbc356294 100644 --- a/src/Servers/Kestrel/Transport.Libuv/src/Internal/ListenerPrimary.cs +++ b/src/Servers/Kestrel/Transport.Libuv/src/Internal/ListenerPrimary.cs @@ -4,9 +4,9 @@ using System; using System.Collections.Generic; using System.IO; +using System.Net; using System.Runtime.InteropServices; using System.Threading.Tasks; -using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; using Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal.Networking; using Microsoft.Extensions.Logging; @@ -46,7 +46,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal public async Task StartAsync( string pipeName, byte[] pipeMessage, - IEndPointInformation endPointInformation, + EndPoint endPoint, LibuvThread thread) { _pipeName = pipeName; @@ -59,7 +59,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal Marshal.StructureToPtr(fileCompletionInfo, _fileCompletionInfoPtr, false); } - await StartAsync(endPointInformation, thread).ConfigureAwait(false); + await StartAsync(endPoint, thread).ConfigureAwait(false); await Thread.PostAsync(listener => listener.PostCallback(), this).ConfigureAwait(false); } diff --git a/src/Servers/Kestrel/Transport.Libuv/src/Internal/ListenerSecondary.cs b/src/Servers/Kestrel/Transport.Libuv/src/Internal/ListenerSecondary.cs index 6597df28d7..477d391f88 100644 --- a/src/Servers/Kestrel/Transport.Libuv/src/Internal/ListenerSecondary.cs +++ b/src/Servers/Kestrel/Transport.Libuv/src/Internal/ListenerSecondary.cs @@ -2,10 +2,10 @@ // 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.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; -using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; using Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal.Networking; using Microsoft.Extensions.Logging; @@ -35,14 +35,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal public Task StartAsync( string pipeName, byte[] pipeMessage, - IEndPointInformation endPointInformation, + EndPoint endPoint, LibuvThread thread) { _pipeName = pipeName; _pipeMessage = pipeMessage; _buf = thread.Loop.Libuv.buf_init(_ptr, 4); - EndPointInformation = endPointInformation; + EndPoint = endPoint; Thread = thread; DispatchPipe = new UvPipeHandle(Log); @@ -152,9 +152,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal { DispatchPipe.Accept(acceptSocket); - // REVIEW: This task should be tracked by the server for graceful shutdown - // Today it's handled specifically for http but not for arbitrary middleware - _ = HandleConnectionAsync(acceptSocket); + HandleConnection(acceptSocket); } catch (UvException ex) when (LibuvConstants.IsConnectionReset(ex.StatusCode)) { @@ -192,11 +190,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal listener._closed = true; + listener.StopAcceptingConnections(); + }, this).ConfigureAwait(false); } else { FreeBuffer(); + + StopAcceptingConnections(); } } } diff --git a/src/Servers/Kestrel/Transport.Libuv/src/LibuvTransportOptions.cs b/src/Servers/Kestrel/Transport.Libuv/src/LibuvTransportOptions.cs index a040dea87b..9110b39587 100644 --- a/src/Servers/Kestrel/Transport.Libuv/src/LibuvTransportOptions.cs +++ b/src/Servers/Kestrel/Transport.Libuv/src/LibuvTransportOptions.cs @@ -1,9 +1,9 @@ -// 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 Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; +using System.IO.Pipelines; namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv { @@ -20,7 +20,19 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv /// public int ThreadCount { get; set; } = ProcessorThreadCount; - internal Func> MemoryPoolFactory { get; set; } = () => KestrelMemoryPool.Create(); + /// + /// Set to false to enable Nagle's algorithm for all connections. + /// + /// + /// Defaults to true. + /// + public bool NoDelay { get; set; } = true; + + public long? MaxReadBufferSize { get; set; } = PipeOptions.Default.PauseWriterThreshold; + + public long? MaxWriteBufferSize { get; set; } = PipeOptions.Default.PauseWriterThreshold; + + internal Func> MemoryPoolFactory { get; set; } = System.Buffers.MemoryPoolFactory.Create; private static int ProcessorThreadCount { diff --git a/src/Servers/Kestrel/Transport.Libuv/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.csproj b/src/Servers/Kestrel/Transport.Libuv/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.csproj index 2b52fe7f46..d6bd6d4e45 100644 --- a/src/Servers/Kestrel/Transport.Libuv/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.csproj +++ b/src/Servers/Kestrel/Transport.Libuv/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.csproj @@ -1,4 +1,4 @@ - + Libuv transport for the ASP.NET Core Kestrel cross-platform web server. @@ -10,12 +10,18 @@ true + + + + + - + + diff --git a/src/Servers/Kestrel/Transport.Libuv/src/WebHostBuilderLibuvExtensions.cs b/src/Servers/Kestrel/Transport.Libuv/src/WebHostBuilderLibuvExtensions.cs index 386d0b6679..8f0986d334 100644 --- a/src/Servers/Kestrel/Transport.Libuv/src/WebHostBuilderLibuvExtensions.cs +++ b/src/Servers/Kestrel/Transport.Libuv/src/WebHostBuilderLibuvExtensions.cs @@ -1,8 +1,8 @@ -// 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 Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; +using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv; using Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal; using Microsoft.Extensions.DependencyInjection; @@ -24,7 +24,7 @@ namespace Microsoft.AspNetCore.Hosting { return hostBuilder.ConfigureServices(services => { - services.AddSingleton(); + services.AddSingleton(); }); } diff --git a/src/Servers/Kestrel/Transport.Libuv/test/LibuvConnectionTests.cs b/src/Servers/Kestrel/Transport.Libuv/test/LibuvConnectionTests.cs index c284ec1584..d1ff9b380c 100644 --- a/src/Servers/Kestrel/Transport.Libuv/test/LibuvConnectionTests.cs +++ b/src/Servers/Kestrel/Transport.Libuv/test/LibuvConnectionTests.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; @@ -19,39 +19,33 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests [Fact] public async Task DoesNotEndConnectionOnZeroRead() { - var mockConnectionDispatcher = new MockConnectionDispatcher(); var mockLibuv = new MockLibuv(); - var transportContext = new TestLibuvTransportContext { ConnectionDispatcher = mockConnectionDispatcher }; - var transport = new LibuvTransport(mockLibuv, transportContext, null); - var thread = new LibuvThread(transport); - Task connectionTask = null; + var transportContext = new TestLibuvTransportContext(); + var thread = new LibuvThread(mockLibuv, transportContext); + var listenerContext = new ListenerContext(transportContext) + { + Thread = thread + }; + try { await thread.StartAsync(); await thread.PostAsync(_ => - { - var listenerContext = new ListenerContext(transportContext) - { - Thread = thread - }; + { var socket = new MockSocket(mockLibuv, Thread.CurrentThread.ManagedThreadId, transportContext.Log); - var connection = new LibuvConnection(socket, listenerContext.TransportContext.Log, thread, null, null); - listenerContext.TransportContext.ConnectionDispatcher.OnConnection(connection); - connectionTask = connection.Start(); + listenerContext.HandleConnection(socket); mockLibuv.AllocCallback(socket.InternalGetHandle(), 2048, out var ignored); mockLibuv.ReadCallback(socket.InternalGetHandle(), 0, ref ignored); }, (object)null); - var readAwaitable = mockConnectionDispatcher.Input.Reader.ReadAsync(); + await using var connection = await listenerContext.AcceptAsync(); + + var readAwaitable = connection.Transport.Input.ReadAsync(); Assert.False(readAwaitable.IsCompleted); } finally { - mockConnectionDispatcher.Input.Reader.Complete(); - mockConnectionDispatcher.Output.Writer.Complete(); - await connectionTask; - await thread.StopAsync(TimeSpan.FromSeconds(5)); } } @@ -59,25 +53,27 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests [Fact] public async Task ConnectionDoesNotResumeAfterSocketCloseIfBackpressureIsApplied() { - var mockConnectionDispatcher = new MockConnectionDispatcher(); var mockLibuv = new MockLibuv(); - var transportContext = new TestLibuvTransportContext { ConnectionDispatcher = mockConnectionDispatcher }; - var transport = new LibuvTransport(mockLibuv, transportContext, null); - var thread = new LibuvThread(transport); - mockConnectionDispatcher.InputOptions = pool => - new PipeOptions( - pool: pool, + var transportContext = new TestLibuvTransportContext(); + var thread = new LibuvThread(mockLibuv, transportContext); + var listenerContext = new ListenerContext(transportContext) + { + Thread = thread, + InputOptions = new PipeOptions( + pool: thread.MemoryPool, pauseWriterThreshold: 3, readerScheduler: PipeScheduler.Inline, writerScheduler: PipeScheduler.Inline, - useSynchronizationContext: false); + useSynchronizationContext: false), - // We don't set the output writer scheduler here since we want to run the callback inline + // We don't set the output writer scheduler here since we want to run the callback inline + OutputOptions = new PipeOptions( + pool: thread.MemoryPool, + readerScheduler: thread, + writerScheduler: PipeScheduler.Inline, + useSynchronizationContext: false) + }; - mockConnectionDispatcher.OutputOptions = pool => new PipeOptions(pool: pool, readerScheduler: thread, writerScheduler: PipeScheduler.Inline, useSynchronizationContext: false); - - - Task connectionTask = null; try { await thread.StartAsync(); @@ -85,28 +81,22 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests // Write enough to make sure back pressure will be applied await thread.PostAsync(_ => { - var listenerContext = new ListenerContext(transportContext) - { - Thread = thread - }; var socket = new MockSocket(mockLibuv, Thread.CurrentThread.ManagedThreadId, transportContext.Log); - var connection = new LibuvConnection(socket, listenerContext.TransportContext.Log, thread, null, null); - listenerContext.TransportContext.ConnectionDispatcher.OnConnection(connection); - connectionTask = connection.Start(); + listenerContext.HandleConnection(socket); mockLibuv.AllocCallback(socket.InternalGetHandle(), 2048, out var ignored); mockLibuv.ReadCallback(socket.InternalGetHandle(), 5, ref ignored); }, null); + var connection = await listenerContext.AcceptAsync(); + // Now assert that we removed the callback from libuv to stop reading Assert.Null(mockLibuv.AllocCallback); Assert.Null(mockLibuv.ReadCallback); // Now complete the output writer so that the connection closes - mockConnectionDispatcher.Output.Writer.Complete(); - - await connectionTask.DefaultTimeout(); + await connection.DisposeAsync(); // Assert that we don't try to start reading Assert.Null(mockLibuv.AllocCallback); @@ -114,9 +104,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests } finally { - mockConnectionDispatcher.Input.Reader.Complete(); - mockConnectionDispatcher.Output.Writer.Complete(); - await thread.StopAsync(TimeSpan.FromSeconds(5)); } } @@ -124,29 +111,32 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests [Fact] public async Task ConnectionDoesNotResumeAfterReadCallbackScheduledAndSocketCloseIfBackpressureIsApplied() { - var mockConnectionDispatcher = new MockConnectionDispatcher(); var mockLibuv = new MockLibuv(); - var transportContext = new TestLibuvTransportContext { ConnectionDispatcher = mockConnectionDispatcher }; - var transport = new LibuvTransport(mockLibuv, transportContext, null); - var thread = new LibuvThread(transport); + var transportContext = new TestLibuvTransportContext(); + var thread = new LibuvThread(mockLibuv, transportContext); var mockScheduler = new Mock(); Action backPressure = null; mockScheduler.Setup(m => m.Schedule(It.IsAny>(), It.IsAny())).Callback, object>((a, o) => { backPressure = () => a(o); }); - mockConnectionDispatcher.InputOptions = pool => - new PipeOptions( - pool: pool, + var listenerContext = new ListenerContext(transportContext) + { + Thread = thread, + InputOptions = new PipeOptions( + pool: thread.MemoryPool, pauseWriterThreshold: 3, resumeWriterThreshold: 3, writerScheduler: mockScheduler.Object, readerScheduler: PipeScheduler.Inline, - useSynchronizationContext: false); + useSynchronizationContext: false), + OutputOptions = new PipeOptions( + pool: thread.MemoryPool, + readerScheduler: thread, + writerScheduler: PipeScheduler.Inline, + useSynchronizationContext: false) + }; - mockConnectionDispatcher.OutputOptions = pool => new PipeOptions(pool: pool, readerScheduler: thread, writerScheduler: PipeScheduler.Inline, useSynchronizationContext: false); - - Task connectionTask = null; try { await thread.StartAsync(); @@ -154,32 +144,28 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests // Write enough to make sure back pressure will be applied await thread.PostAsync(_ => { - var listenerContext = new ListenerContext(transportContext) - { - Thread = thread - }; var socket = new MockSocket(mockLibuv, Thread.CurrentThread.ManagedThreadId, transportContext.Log); - var connection = new LibuvConnection(socket, listenerContext.TransportContext.Log, thread, null, null); - listenerContext.TransportContext.ConnectionDispatcher.OnConnection(connection); - connectionTask = connection.Start(); - + listenerContext.HandleConnection(socket); + mockLibuv.AllocCallback(socket.InternalGetHandle(), 2048, out var ignored); mockLibuv.ReadCallback(socket.InternalGetHandle(), 5, ref ignored); }, null); + var connection = await listenerContext.AcceptAsync(); + // Now assert that we removed the callback from libuv to stop reading Assert.Null(mockLibuv.AllocCallback); Assert.Null(mockLibuv.ReadCallback); // Now release backpressure by reading the input - var result = await mockConnectionDispatcher.Input.Reader.ReadAsync(); + var result = await connection.Transport.Input.ReadAsync(); // Calling advance will call into our custom scheduler that captures the back pressure // callback - mockConnectionDispatcher.Input.Reader.AdvanceTo(result.Buffer.End); + connection.Transport.Input.AdvanceTo(result.Buffer.End); // Cancel the current pending flush - mockConnectionDispatcher.Input.Writer.CancelPendingFlush(); + connection.Application.Output.CancelPendingFlush(); // Now release the back pressure await thread.PostAsync(a => a(), backPressure); @@ -189,19 +175,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests Assert.Null(mockLibuv.ReadCallback); // Now complete the output writer and wait for the connection to close - mockConnectionDispatcher.Output.Writer.Complete(); - - await connectionTask.DefaultTimeout(); - + await connection.DisposeAsync(); + // Assert that we don't try to start reading Assert.Null(mockLibuv.AllocCallback); Assert.Null(mockLibuv.ReadCallback); } finally { - mockConnectionDispatcher.Input.Reader.Complete(); - mockConnectionDispatcher.Output.Writer.Complete(); - await thread.StopAsync(TimeSpan.FromSeconds(5)); } } @@ -209,42 +190,35 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests [Fact] public async Task DoesNotThrowIfOnReadCallbackCalledWithEOFButAllocCallbackNotCalled() { - var mockConnectionDispatcher = new MockConnectionDispatcher(); var mockLibuv = new MockLibuv(); - var transportContext = new TestLibuvTransportContext { ConnectionDispatcher = mockConnectionDispatcher }; - var transport = new LibuvTransport(mockLibuv, transportContext, null); - var thread = new LibuvThread(transport); + var transportContext = new TestLibuvTransportContext(); + var thread = new LibuvThread(mockLibuv, transportContext); + var listenerContext = new ListenerContext(transportContext) + { + Thread = thread + }; - Task connectionTask = null; try { await thread.StartAsync(); await thread.PostAsync(_ => { - var listenerContext = new ListenerContext(transportContext) - { - Thread = thread - }; var socket = new MockSocket(mockLibuv, Thread.CurrentThread.ManagedThreadId, transportContext.Log); - var connection = new LibuvConnection(socket, listenerContext.TransportContext.Log, thread, null, null); - listenerContext.TransportContext.ConnectionDispatcher.OnConnection(connection); - connectionTask = connection.Start(); - + listenerContext.HandleConnection(socket); + var ignored = new LibuvFunctions.uv_buf_t(); mockLibuv.ReadCallback(socket.InternalGetHandle(), TestConstants.EOF, ref ignored); }, (object)null); - var readAwaitable = await mockConnectionDispatcher.Input.Reader.ReadAsync(); + await using var connection = await listenerContext.AcceptAsync(); + + var readAwaitable = await connection.Transport.Input.ReadAsync(); Assert.True(readAwaitable.IsCompleted); } finally { - mockConnectionDispatcher.Input.Reader.Complete(); - mockConnectionDispatcher.Output.Writer.Complete(); - await connectionTask; - await thread.StopAsync(TimeSpan.FromSeconds(5)); } } } -} \ No newline at end of file +} diff --git a/src/Servers/Kestrel/Transport.Libuv/test/LibuvOutputConsumerTests.cs b/src/Servers/Kestrel/Transport.Libuv/test/LibuvOutputConsumerTests.cs index d72e6485b5..f0a9769357 100644 --- a/src/Servers/Kestrel/Transport.Libuv/test/LibuvOutputConsumerTests.cs +++ b/src/Servers/Kestrel/Transport.Libuv/test/LibuvOutputConsumerTests.cs @@ -14,7 +14,6 @@ using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; -using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; using Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal; using Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal.Networking; using Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests.TestHelpers; @@ -42,11 +41,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests public LibuvOutputConsumerTests() { - _memoryPool = KestrelMemoryPool.Create(); + _memoryPool = MemoryPoolFactory.Create(); _mockLibuv = new MockLibuv(); - var libuvTransport = new LibuvTransport(_mockLibuv, new TestLibuvTransportContext(), new ListenOptions((ulong)0)); - _libuvThread = new LibuvThread(libuvTransport, maxLoops: 1); + var context = new TestLibuvTransportContext(); + _libuvThread = new LibuvThread(_mockLibuv, context, maxLoops: 1); _libuvThread.StartAsync().Wait(); } @@ -769,5 +768,43 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests outputReader.Complete(ex); } } + + // Work around the internal type conflict (multiple assemblies have internalized this type and that fails with IVT) + private class DuplexPipe : IDuplexPipe + { + public DuplexPipe(PipeReader reader, PipeWriter writer) + { + Input = reader; + Output = writer; + } + + public PipeReader Input { get; } + + public PipeWriter Output { get; } + + public static DuplexPipePair CreateConnectionPair(PipeOptions inputOptions, PipeOptions outputOptions) + { + var input = new Pipe(inputOptions); + var output = new Pipe(outputOptions); + + var transportToApplication = new DuplexPipe(output.Reader, input.Writer); + var applicationToTransport = new DuplexPipe(input.Reader, output.Writer); + + return new DuplexPipePair(applicationToTransport, transportToApplication); + } + + // This class exists to work around issues with value tuple on .NET Framework + public readonly struct DuplexPipePair + { + public IDuplexPipe Transport { get; } + public IDuplexPipe Application { get; } + + public DuplexPipePair(IDuplexPipe transport, IDuplexPipe application) + { + Transport = transport; + Application = application; + } + } + } } } diff --git a/src/Servers/Kestrel/Transport.Libuv/test/LibuvThreadTests.cs b/src/Servers/Kestrel/Transport.Libuv/test/LibuvThreadTests.cs index 69daacab35..7bfd5280ef 100644 --- a/src/Servers/Kestrel/Transport.Libuv/test/LibuvThreadTests.cs +++ b/src/Servers/Kestrel/Transport.Libuv/test/LibuvThreadTests.cs @@ -14,11 +14,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests [Fact] public async Task LibuvThreadDoesNotThrowIfPostingWorkAfterDispose() { - var mockConnectionDispatcher = new MockConnectionDispatcher(); var mockLibuv = new MockLibuv(); - var transportContext = new TestLibuvTransportContext { ConnectionDispatcher = mockConnectionDispatcher }; - var transport = new LibuvTransport(mockLibuv, transportContext, null); - var thread = new LibuvThread(transport); + var transportContext = new TestLibuvTransportContext(); + var thread = new LibuvThread(mockLibuv, transportContext); var ranOne = false; var ranTwo = false; var ranThree = false; diff --git a/src/Servers/Kestrel/Transport.Libuv/test/LibuvTransportTests.cs b/src/Servers/Kestrel/Transport.Libuv/test/LibuvTransportTests.cs index abdd8e1c7a..d18af3aa39 100644 --- a/src/Servers/Kestrel/Transport.Libuv/test/LibuvTransportTests.cs +++ b/src/Servers/Kestrel/Transport.Libuv/test/LibuvTransportTests.cs @@ -1,18 +1,18 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; +using System.Buffers; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; using System.Net.Sockets; using System.Text; -using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; -using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal; using Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests.TestHelpers; using Microsoft.AspNetCore.Testing; @@ -23,71 +23,154 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests { public class LibuvTransportTests { - public static TheoryData ConnectionAdapterData => new TheoryData - { - new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)), - new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)) - { - ConnectionAdapters = { new PassThroughConnectionAdapter() } - } - }; - public static IEnumerable OneToTen => Enumerable.Range(1, 10).Select(i => new object[] { i }); [Fact] public async Task TransportCanBindAndStop() { var transportContext = new TestLibuvTransportContext(); - var transport = new LibuvTransport(transportContext, - new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0))); + var transport = new LibuvConnectionListener(transportContext, new IPEndPoint(IPAddress.Loopback, 0)); // The transport can no longer start threads without binding to an endpoint. await transport.BindAsync(); - await transport.StopAsync(); + await transport.DisposeAsync(); } [Fact] public async Task TransportCanBindUnbindAndStop() { var transportContext = new TestLibuvTransportContext(); - var transport = new LibuvTransport(transportContext, new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0))); + var transport = new LibuvConnectionListener(transportContext, new IPEndPoint(IPAddress.Loopback, 0)); await transport.BindAsync(); await transport.UnbindAsync(); - await transport.StopAsync(); + await transport.DisposeAsync(); } - [Theory] - [MemberData(nameof(ConnectionAdapterData))] - public async Task ConnectionCanReadAndWrite(ListenOptions listenOptions) + [Fact] + public async Task ConnectionCanReadAndWrite() { - var serviceContext = new TestServiceContext(); - listenOptions.UseHttpServer(listenOptions.ConnectionAdapters, serviceContext, new DummyApplication(TestApp.EchoApp), HttpProtocols.Http1); - - var transportContext = new TestLibuvTransportContext - { - ConnectionDispatcher = new ConnectionDispatcher(serviceContext, listenOptions.Build()) - }; - - var transport = new LibuvTransport(transportContext, listenOptions); + var transportContext = new TestLibuvTransportContext(); + await using var transport = new LibuvConnectionListener(transportContext, new IPEndPoint(IPAddress.Loopback, 0)); await transport.BindAsync(); + var endpoint = (IPEndPoint)transport.EndPoint; - using (var socket = TestConnection.CreateConnectedLoopbackSocket(listenOptions.IPEndPoint.Port)) + async Task EchoServerAsync() { - var data = "Hello World"; - socket.Send(Encoding.ASCII.GetBytes($"POST / HTTP/1.0\r\nContent-Length: 11\r\n\r\n{data}")); + while (true) + { + await using var connection = await transport.AcceptAsync(); + + if (connection == null) + { + break; + } + + while (true) + { + var result = await connection.Transport.Input.ReadAsync(); + + if (result.IsCompleted) + { + break; + } + await connection.Transport.Output.WriteAsync(result.Buffer.ToArray()); + + connection.Transport.Input.AdvanceTo(result.Buffer.End); + } + } + } + + var serverTask = EchoServerAsync(); + + using (var socket = TestConnection.CreateConnectedLoopbackSocket(endpoint.Port)) + { + var data = Encoding.ASCII.GetBytes("Hello World"); + await socket.SendAsync(data, SocketFlags.None); + var buffer = new byte[data.Length]; var read = 0; while (read < data.Length) { - read += socket.Receive(buffer, read, buffer.Length - read, SocketFlags.None); + read += await socket.ReceiveAsync(buffer.AsMemory(read, buffer.Length - read), SocketFlags.None); + } + + Assert.Equal(data, buffer); + } + + await transport.UnbindAsync(); + + await serverTask.DefaultTimeout(); + } + + [Fact] + public async Task UnacceptedConnectionsAreAborted() + { + var transportContext = new TestLibuvTransportContext(); + var transport = new LibuvConnectionListener(transportContext, new IPEndPoint(IPAddress.Loopback, 0)); + + await transport.BindAsync(); + var endpoint = (IPEndPoint)transport.EndPoint; + + async Task ConnectAsync() + { + using (var socket = TestConnection.CreateConnectedLoopbackSocket(endpoint.Port)) + { + try + { + var read = await socket.ReceiveAsync(new byte[10], SocketFlags.None); + Assert.Equal(0, read); + } + catch (SocketException) + { + // The connection can be reset sometimes + } } } - Assert.True(await serviceContext.ConnectionManager.CloseAllConnectionsAsync(new CancellationTokenSource(TestConstants.DefaultTimeout).Token)); + var connectTask = ConnectAsync(); + await transport.UnbindAsync(); - await transport.StopAsync(); + await transport.DisposeAsync(); + + // The connection was accepted because libuv eagerly accepts connections + // they sit in a queue in each listener, we want to make sure that resources + // are cleaned up if they are never accepted by the caller + + await connectTask.DefaultTimeout(); + } + + [Fact] + public async Task CallingAcceptAfterDisposeAsyncThrows() + { + var transportContext = new TestLibuvTransportContext(); + var transport = new LibuvConnectionListener(transportContext, new IPEndPoint(IPAddress.Loopback, 0)); + + await transport.BindAsync(); + var endpoint = (IPEndPoint)transport.EndPoint; + + await transport.UnbindAsync(); + await transport.DisposeAsync(); + + await Assert.ThrowsAsync(() => transport.AcceptAsync().AsTask()); + } + + [Fact] + public async Task CallingDisposeAsyncWillYieldPendingAccepts() + { + var transportContext = new TestLibuvTransportContext(); + await using var transport = new LibuvConnectionListener(transportContext, new IPEndPoint(IPAddress.Loopback, 0)); + + await transport.BindAsync(); + + var acceptTask = transport.AcceptAsync(); + + await transport.UnbindAsync(); + + var connection = await acceptTask.DefaultTimeout(); + + Assert.Null(connection); } [ConditionalTheory] @@ -106,13 +189,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests var transportContext = new TestLibuvTransportContext { - ConnectionDispatcher = new ConnectionDispatcher(serviceContext, listenOptions.Build()), Options = new LibuvTransportOptions { ThreadCount = threadCount } }; - var transport = new LibuvTransport(transportContext, listenOptions); - + await using var transport = new LibuvConnectionListener(transportContext, listenOptions.EndPoint); await transport.BindAsync(); + listenOptions.EndPoint = transport.EndPoint; + + var dispatcher = new ConnectionDispatcher(serviceContext, listenOptions.Build()); + var acceptTask = dispatcher.StartAcceptingConnections(transport); using (var client = new HttpClient()) { @@ -132,12 +217,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests await transport.UnbindAsync(); - if (!await serviceContext.ConnectionManager.CloseAllConnectionsAsync(default).ConfigureAwait(false)) - { - await serviceContext.ConnectionManager.AbortAllConnectionsAsync().ConfigureAwait(false); - } + await acceptTask; - await transport.StopAsync(); + if (!await serviceContext.ConnectionManager.CloseAllConnectionsAsync(default)) + { + await serviceContext.ConnectionManager.AbortAllConnectionsAsync(); + } } } } diff --git a/src/Servers/Kestrel/Transport.Libuv/test/ListenerPrimaryTests.cs b/src/Servers/Kestrel/Transport.Libuv/test/ListenerPrimaryTests.cs index b1a338bd56..b3ed1b94fe 100644 --- a/src/Servers/Kestrel/Transport.Libuv/test/ListenerPrimaryTests.cs +++ b/src/Servers/Kestrel/Transport.Libuv/test/ListenerPrimaryTests.cs @@ -2,16 +2,10 @@ // 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.IO; using System.Linq; using System.Net; using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Connections; -using Microsoft.AspNetCore.Server.Kestrel.Core; -using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; -using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; using Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal; using Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal.Networking; using Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests.TestHelpers; @@ -28,42 +22,39 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests { var libuv = new LibuvFunctions(); - var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)); + var endpoint = new IPEndPoint(IPAddress.Loopback, 0); - var serviceContextPrimary = new TestServiceContext(); var transportContextPrimary = new TestLibuvTransportContext(); - var builderPrimary = new ConnectionBuilder(); - builderPrimary.UseHttpServer(serviceContextPrimary, new DummyApplication(c => c.Response.WriteAsync("Primary")), HttpProtocols.Http1); - transportContextPrimary.ConnectionDispatcher = new ConnectionDispatcher(serviceContextPrimary, builderPrimary.Build()); - - var serviceContextSecondary = new TestServiceContext(); - var builderSecondary = new ConnectionBuilder(); - builderSecondary.UseHttpServer(serviceContextSecondary, new DummyApplication(c => c.Response.WriteAsync("Secondary")), HttpProtocols.Http1); var transportContextSecondary = new TestLibuvTransportContext(); - transportContextSecondary.ConnectionDispatcher = new ConnectionDispatcher(serviceContextSecondary, builderSecondary.Build()); - - var libuvTransport = new LibuvTransport(libuv, transportContextPrimary, listenOptions); var pipeName = (libuv.IsWindows ? @"\\.\pipe\kestrel_" : "/tmp/kestrel_") + Guid.NewGuid().ToString("n"); var pipeMessage = Guid.NewGuid().ToByteArray(); // Start primary listener - var libuvThreadPrimary = new LibuvThread(libuvTransport); + var libuvThreadPrimary = new LibuvThread(libuv, transportContextPrimary); await libuvThreadPrimary.StartAsync(); var listenerPrimary = new ListenerPrimary(transportContextPrimary); - await listenerPrimary.StartAsync(pipeName, pipeMessage, listenOptions, libuvThreadPrimary); - var address = GetUri(listenOptions); + await listenerPrimary.StartAsync(pipeName, pipeMessage, endpoint, libuvThreadPrimary); + var address = GetUri(listenerPrimary.EndPoint); - // Until a secondary listener is added, TCP connections get dispatched directly - Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address)); - Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address)); + var acceptTask = listenerPrimary.AcceptAsync().AsTask(); + using (var socket = await HttpClientSlim.GetSocket(address)) + { + await (await acceptTask.DefaultTimeout()).DisposeAsync(); + } + + acceptTask = listenerPrimary.AcceptAsync().AsTask(); + using (var socket = await HttpClientSlim.GetSocket(address)) + { + await (await acceptTask.DefaultTimeout()).DisposeAsync(); + } var listenerCount = listenerPrimary.UvPipeCount; // Add secondary listener - var libuvThreadSecondary = new LibuvThread(libuvTransport); + var libuvThreadSecondary = new LibuvThread(libuv, transportContextSecondary); await libuvThreadSecondary.StartAsync(); var listenerSecondary = new ListenerSecondary(transportContextSecondary); - await listenerSecondary.StartAsync(pipeName, pipeMessage, listenOptions, libuvThreadSecondary); + await listenerSecondary.StartAsync(pipeName, pipeMessage, endpoint, libuvThreadSecondary); var maxWait = Task.Delay(TestConstants.DefaultTimeout); // wait for ListenerPrimary.ReadCallback to add the secondary pipe @@ -77,14 +68,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests } // Once a secondary listener is added, TCP connections start getting dispatched to it - await AssertResponseEventually(address, "Secondary", allowed: new[] { "Primary" }); + // This returns the incomplete primary task after the secondary listener got the last + // connection + var primary = await WaitForSecondaryListener(address, listenerPrimary, listenerSecondary); // TCP connections will still get round-robined to the primary listener - Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address)); - Assert.Equal("Secondary", await HttpClientSlim.GetStringAsync(address)); - Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address)); + ListenerContext currentListener = listenerSecondary; + Task expected = primary; + + await AssertRoundRobin(address, listenerPrimary, listenerSecondary, currentListener, expected); await listenerSecondary.DisposeAsync(); + await libuvThreadSecondary.StopAsync(TimeSpan.FromSeconds(5)); await listenerPrimary.DisposeAsync(); @@ -96,48 +91,36 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests public async Task NonListenerPipeConnectionsAreLoggedAndIgnored() { var libuv = new LibuvFunctions(); - var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)); + var endpoint = new IPEndPoint(IPAddress.Loopback, 0); var logger = new TestApplicationErrorLogger(); - var serviceContextPrimary = new TestServiceContext(); - var builderPrimary = new ConnectionBuilder(); - builderPrimary.UseHttpServer(serviceContextPrimary, new DummyApplication(c => c.Response.WriteAsync("Primary")), HttpProtocols.Http1); var transportContextPrimary = new TestLibuvTransportContext { Log = new LibuvTrace(logger) }; - transportContextPrimary.ConnectionDispatcher = new ConnectionDispatcher(serviceContextPrimary, builderPrimary.Build()); - - var serviceContextSecondary = new TestServiceContext - { - DateHeaderValueManager = serviceContextPrimary.DateHeaderValueManager, - ServerOptions = serviceContextPrimary.ServerOptions, - Scheduler = serviceContextPrimary.Scheduler, - HttpParser = serviceContextPrimary.HttpParser, - }; - var builderSecondary = new ConnectionBuilder(); - builderSecondary.UseHttpServer(serviceContextSecondary, new DummyApplication(c => c.Response.WriteAsync("Secondary")), HttpProtocols.Http1); var transportContextSecondary = new TestLibuvTransportContext(); - transportContextSecondary.ConnectionDispatcher = new ConnectionDispatcher(serviceContextSecondary, builderSecondary.Build()); - - var libuvTransport = new LibuvTransport(libuv, transportContextPrimary, listenOptions); var pipeName = (libuv.IsWindows ? @"\\.\pipe\kestrel_" : "/tmp/kestrel_") + Guid.NewGuid().ToString("n"); var pipeMessage = Guid.NewGuid().ToByteArray(); // Start primary listener - var libuvThreadPrimary = new LibuvThread(libuvTransport); + var libuvThreadPrimary = new LibuvThread(libuv, transportContextPrimary); await libuvThreadPrimary.StartAsync(); var listenerPrimary = new ListenerPrimary(transportContextPrimary); - await listenerPrimary.StartAsync(pipeName, pipeMessage, listenOptions, libuvThreadPrimary); - var address = GetUri(listenOptions); + await listenerPrimary.StartAsync(pipeName, pipeMessage, endpoint, libuvThreadPrimary); + var address = GetUri(listenerPrimary.EndPoint); // Add secondary listener - var libuvThreadSecondary = new LibuvThread(libuvTransport); + var libuvThreadSecondary = new LibuvThread(libuv, transportContextSecondary); await libuvThreadSecondary.StartAsync(); var listenerSecondary = new ListenerSecondary(transportContextSecondary); - await listenerSecondary.StartAsync(pipeName, pipeMessage, listenOptions, libuvThreadSecondary); + await listenerSecondary.StartAsync(pipeName, pipeMessage, endpoint, libuvThreadSecondary); // TCP Connections get round-robined - await AssertResponseEventually(address, "Secondary", allowed: new[] { "Primary" }); - Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address)); + var primary = await WaitForSecondaryListener(address, listenerPrimary, listenerSecondary); + + // Make sure the pending accept get yields + using (var socket = await HttpClientSlim.GetSocket(address)) + { + await (await primary.DefaultTimeout()).DisposeAsync(); + } // Create a pipe connection and keep it open without sending any data var connectTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -173,9 +156,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests await connectTcs.Task; // TCP connections will still get round-robined between only the two listeners - Assert.Equal("Secondary", await HttpClientSlim.GetStringAsync(address)); - Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address)); - Assert.Equal("Secondary", await HttpClientSlim.GetStringAsync(address)); + await AssertRoundRobin(address, listenerPrimary, listenerSecondary, listenerPrimary); await libuvThreadPrimary.PostAsync(_ => pipe.Dispose(), (object)null); @@ -186,9 +167,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests } // Same for after the non-listener pipe connection is closed - Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address)); - Assert.Equal("Secondary", await HttpClientSlim.GetStringAsync(address)); - Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address)); + await AssertRoundRobin(address, listenerPrimary, listenerSecondary, listenerPrimary); await listenerSecondary.DisposeAsync(); await libuvThreadSecondary.StopAsync(TimeSpan.FromSeconds(5)); @@ -207,45 +186,28 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests public async Task PipeConnectionsWithWrongMessageAreLoggedAndIgnored() { var libuv = new LibuvFunctions(); - var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)); + var endpoint = new IPEndPoint(IPAddress.Loopback, 0); var logger = new TestApplicationErrorLogger(); - var serviceContextPrimary = new TestServiceContext(); - var builderPrimary = new ConnectionBuilder(); - builderPrimary.UseHttpServer(serviceContextPrimary, new DummyApplication(c => c.Response.WriteAsync("Primary")), HttpProtocols.Http1); var transportContextPrimary = new TestLibuvTransportContext { Log = new LibuvTrace(logger) }; - transportContextPrimary.ConnectionDispatcher = new ConnectionDispatcher(serviceContextPrimary, builderPrimary.Build()); - - var serviceContextSecondary = new TestServiceContext - { - DateHeaderValueManager = serviceContextPrimary.DateHeaderValueManager, - ServerOptions = serviceContextPrimary.ServerOptions, - Scheduler = serviceContextPrimary.Scheduler, - HttpParser = serviceContextPrimary.HttpParser, - }; - var builderSecondary = new ConnectionBuilder(); - builderSecondary.UseHttpServer(serviceContextSecondary, new DummyApplication(c => c.Response.WriteAsync("Secondary")), HttpProtocols.Http1); var transportContextSecondary = new TestLibuvTransportContext(); - transportContextSecondary.ConnectionDispatcher = new ConnectionDispatcher(serviceContextSecondary, builderSecondary.Build()); - - var libuvTransport = new LibuvTransport(libuv, transportContextPrimary, listenOptions); var pipeName = (libuv.IsWindows ? @"\\.\pipe\kestrel_" : "/tmp/kestrel_") + Guid.NewGuid().ToString("n"); var pipeMessage = Guid.NewGuid().ToByteArray(); // Start primary listener - var libuvThreadPrimary = new LibuvThread(libuvTransport); + var libuvThreadPrimary = new LibuvThread(libuv, transportContextPrimary); await libuvThreadPrimary.StartAsync(); var listenerPrimary = new ListenerPrimary(transportContextPrimary); - await listenerPrimary.StartAsync(pipeName, pipeMessage, listenOptions, libuvThreadPrimary); - var address = GetUri(listenOptions); + await listenerPrimary.StartAsync(pipeName, pipeMessage, endpoint, libuvThreadPrimary); + var address = GetUri(listenerPrimary.EndPoint); // Add secondary listener with wrong pipe message - var libuvThreadSecondary = new LibuvThread(libuvTransport); + var libuvThreadSecondary = new LibuvThread(libuv, transportContextSecondary); await libuvThreadSecondary.StartAsync(); var listenerSecondary = new ListenerSecondary(transportContextSecondary); - await listenerSecondary.StartAsync(pipeName, Guid.NewGuid().ToByteArray(), listenOptions, libuvThreadSecondary); + await listenerSecondary.StartAsync(pipeName, Guid.NewGuid().ToByteArray(), endpoint, libuvThreadSecondary); // Wait up to 10 seconds for error to be logged for (var i = 0; i < 10 && logger.TotalErrorsLogged == 0; i++) @@ -253,10 +215,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests await Task.Delay(100); } - // TCP Connections don't get round-robined - Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address)); - Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address)); - Assert.Equal("Primary", await HttpClientSlim.GetStringAsync(address)); + // TCP Connections don't get round-robined. This should time out if the request goes to the secondary listener + for (int i = 0; i < 3; i++) + { + using var socket = await HttpClientSlim.GetSocket(address); + + await using var connection = await listenerPrimary.AcceptAsync().AsTask().DefaultTimeout(); + } await listenerSecondary.DisposeAsync(); await libuvThreadSecondary.StopAsync(TimeSpan.FromSeconds(5)); @@ -270,73 +235,73 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests Assert.Contains("Bad data", errorMessage.Exception.ToString()); } - private static async Task AssertResponseEventually( - Uri address, - string expected, - string[] allowed = null, - int maxRetries = 100, - int retryDelay = 100) + + private static async Task AssertRoundRobin(Uri address, ListenerPrimary listenerPrimary, ListenerSecondary listenerSecondary, ListenerContext currentListener, Task expected = null, int connections = 4) { - for (var i = 0; i < maxRetries; i++) + for (int i = 0; i < connections; i++) { - var response = await HttpClientSlim.GetStringAsync(address); - if (response == expected) + if (currentListener == listenerPrimary) { - return; + expected ??= listenerSecondary.AcceptAsync().AsTask(); + currentListener = listenerSecondary; + } + else + { + expected ??= listenerPrimary.AcceptAsync().AsTask(); + currentListener = listenerPrimary; } - if (allowed != null) + using var socket = await HttpClientSlim.GetSocket(address); + + await using var connection = await expected.DefaultTimeout(); + + expected = null; + } + } + + private static async Task> WaitForSecondaryListener(Uri address, ListenerContext listenerPrimary, ListenerContext listenerSecondary) + { + int maxRetries = 100; + int retryDelay = 100; + + Task primary = null; + Task secondary = null; + + for (var i = 0; i < maxRetries; i++) + { + primary ??= listenerPrimary.AcceptAsync().AsTask(); + secondary ??= listenerSecondary.AcceptAsync().AsTask(); + + using var _ = await HttpClientSlim.GetSocket(address); + + var task = await Task.WhenAny(primary, secondary); + + if (task == secondary) { - Assert.Contains(response, allowed); + // Dispose this connection now that we know the seconary listener is working + await (await secondary).DisposeAsync(); + + // Return the primary task (it should be incomplete), we do this so that we can + return primary; + } + else + { + // Dispose the connection + await (await primary).DisposeAsync(); + + primary = null; } await Task.Delay(retryDelay); } - Assert.True(false, $"'{address}' failed to respond with '{expected}' in {maxRetries} retries."); + Assert.True(false, $"'{address}' failed to get queued connection in secondary listener in {maxRetries} retries."); + return null; } - private static Uri GetUri(ListenOptions options) + private static Uri GetUri(EndPoint endpoint) { - if (options.Type != ListenType.IPEndPoint) - { - throw new InvalidOperationException($"Could not determine a proper URI for options with Type {options.Type}"); - } - - var scheme = options.ConnectionAdapters.Any(f => f.IsHttps) - ? "https" - : "http"; - - return new Uri($"{scheme}://{options.IPEndPoint}"); - } - - private class ConnectionBuilder : IConnectionBuilder - { - private readonly List> _components = new List>(); - - public IServiceProvider ApplicationServices { get; set; } - - public IConnectionBuilder Use(Func middleware) - { - _components.Add(middleware); - return this; - } - - public ConnectionDelegate Build() - { - ConnectionDelegate app = context => - { - return Task.CompletedTask; - }; - - for (int i = _components.Count - 1; i >= 0; i--) - { - var component = _components[i]; - app = component(app); - } - - return app; - } + return new Uri($"http://{endpoint}"); } } } diff --git a/src/Servers/Kestrel/Transport.Libuv/test/TestHelpers/MockConnectionDispatcher.cs b/src/Servers/Kestrel/Transport.Libuv/test/TestHelpers/MockConnectionDispatcher.cs deleted file mode 100644 index 01e0c049d0..0000000000 --- a/src/Servers/Kestrel/Transport.Libuv/test/TestHelpers/MockConnectionDispatcher.cs +++ /dev/null @@ -1,31 +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.Buffers; -using System.IO.Pipelines; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; - -namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests.TestHelpers -{ - public class MockConnectionDispatcher : IConnectionDispatcher - { - public Func, PipeOptions> InputOptions { get; set; } = pool => new PipeOptions(pool, readerScheduler: PipeScheduler.Inline, writerScheduler: PipeScheduler.Inline, useSynchronizationContext: false); - public Func, PipeOptions> OutputOptions { get; set; } = pool => new PipeOptions(pool, readerScheduler: PipeScheduler.Inline, writerScheduler: PipeScheduler.Inline, useSynchronizationContext: false); - - public Task OnConnection(TransportConnection connection) - { - Input = new Pipe(InputOptions(connection.MemoryPool)); - Output = new Pipe(OutputOptions(connection.MemoryPool)); - - connection.Transport = new DuplexPipe(Input.Reader, Output.Writer); - connection.Application = new DuplexPipe(Output.Reader, Input.Writer); - - return Task.CompletedTask; - } - - public Pipe Input { get; private set; } - public Pipe Output { get; private set; } - } -} diff --git a/src/Servers/Kestrel/Transport.Libuv/test/TestHelpers/TestLibuvTransportContext.cs b/src/Servers/Kestrel/Transport.Libuv/test/TestHelpers/TestLibuvTransportContext.cs index 155c31b2eb..1bccf57d32 100644 --- a/src/Servers/Kestrel/Transport.Libuv/test/TestHelpers/TestLibuvTransportContext.cs +++ b/src/Servers/Kestrel/Transport.Libuv/test/TestHelpers/TestLibuvTransportContext.cs @@ -13,7 +13,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests.TestHelpers var logger = new TestApplicationErrorLogger(); AppLifetime = new LifetimeNotImplemented(); - ConnectionDispatcher = new MockConnectionDispatcher(); Log = new LibuvTrace(logger); Options = new LibuvTransportOptions { ThreadCount = 1 }; } diff --git a/src/Servers/Kestrel/Transport.Sockets/ref/Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.csproj b/src/Servers/Kestrel/Transport.Sockets/ref/Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.csproj index d41703c771..d5f1350fec 100644 --- a/src/Servers/Kestrel/Transport.Sockets/ref/Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.csproj +++ b/src/Servers/Kestrel/Transport.Sockets/ref/Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.csproj @@ -6,7 +6,7 @@ - + diff --git a/src/Servers/Kestrel/Transport.Sockets/ref/Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.netcoreapp3.0.cs b/src/Servers/Kestrel/Transport.Sockets/ref/Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.netcoreapp3.0.cs index 2b9b9b1b87..34c7f982bb 100644 --- a/src/Servers/Kestrel/Transport.Sockets/ref/Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.netcoreapp3.0.cs +++ b/src/Servers/Kestrel/Transport.Sockets/ref/Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.netcoreapp3.0.cs @@ -11,15 +11,18 @@ namespace Microsoft.AspNetCore.Hosting } namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets { - public sealed partial class SocketTransportFactory : Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal.ITransportFactory + public sealed partial class SocketTransportFactory : Microsoft.AspNetCore.Connections.IConnectionListenerFactory { - public SocketTransportFactory(Microsoft.Extensions.Options.IOptions options, Microsoft.Extensions.Hosting.IHostApplicationLifetime applicationLifetime, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory) { } - public Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal.ITransport Create(Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal.IEndPointInformation endPointInformation, Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal.IConnectionDispatcher dispatcher) { throw null; } + public SocketTransportFactory(Microsoft.Extensions.Options.IOptions options, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory) { } + public System.Threading.Tasks.ValueTask BindAsync(System.Net.EndPoint endpoint, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } } public partial class SocketTransportOptions { public SocketTransportOptions() { } public int IOQueueCount { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + public long? MaxReadBufferSize { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + public long? MaxWriteBufferSize { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + public bool NoDelay { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } } } namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal diff --git a/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketConnection.cs b/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketConnection.cs index bfb467f94a..1c867f3199 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketConnection.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketConnection.cs @@ -11,19 +11,17 @@ using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; -using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal { - internal sealed class SocketConnection : TransportConnection, IDisposable + internal sealed class SocketConnection : TransportConnection { - private static readonly int MinAllocBufferSize = KestrelMemoryPool.MinimumSegmentSize / 2; + private static readonly int MinAllocBufferSize = SlabMemoryPool.BlockSize / 2; private static readonly bool IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); private static readonly bool IsMacOS = RuntimeInformation.IsOSPlatform(OSPlatform.OSX); private readonly Socket _socket; - private readonly PipeScheduler _scheduler; private readonly ISocketsTrace _trace; private readonly SocketReceiver _receiver; private readonly SocketSender _sender; @@ -32,8 +30,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal private readonly object _shutdownLock = new object(); private volatile bool _socketDisposed; private volatile Exception _shutdownReason; + private Task _processingTask; - internal SocketConnection(Socket socket, MemoryPool memoryPool, PipeScheduler scheduler, ISocketsTrace trace) + internal SocketConnection(Socket socket, + MemoryPool memoryPool, + PipeScheduler scheduler, + ISocketsTrace trace, + long? maxReadBufferSize = null, + long? maxWriteBufferSize = null) { Debug.Assert(socket != null); Debug.Assert(memoryPool != null); @@ -41,35 +45,46 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal _socket = socket; MemoryPool = memoryPool; - _scheduler = scheduler; - Logger = trace; _trace = trace; - var localEndPoint = (IPEndPoint)_socket.LocalEndPoint; - var remoteEndPoint = (IPEndPoint)_socket.RemoteEndPoint; - - LocalAddress = localEndPoint.Address; - LocalPort = localEndPoint.Port; - - RemoteAddress = remoteEndPoint.Address; - RemotePort = remoteEndPoint.Port; + LocalEndPoint = _socket.LocalEndPoint; + RemoteEndPoint = _socket.RemoteEndPoint; ConnectionClosed = _connectionClosedTokenSource.Token; // On *nix platforms, Sockets already dispatches to the ThreadPool. // Yes, the IOQueues are still used for the PipeSchedulers. This is intentional. // https://github.com/aspnet/KestrelHttpServer/issues/2573 - var awaiterScheduler = IsWindows ? _scheduler : PipeScheduler.Inline; + var awaiterScheduler = IsWindows ? scheduler : PipeScheduler.Inline; _receiver = new SocketReceiver(_socket, awaiterScheduler); _sender = new SocketSender(_socket, awaiterScheduler); + + maxReadBufferSize ??= 0; + maxWriteBufferSize ??= 0; + + var inputOptions = new PipeOptions(MemoryPool, PipeScheduler.ThreadPool, scheduler, maxReadBufferSize.Value, maxReadBufferSize.Value / 2, useSynchronizationContext: false); + var outputOptions = new PipeOptions(MemoryPool, scheduler, PipeScheduler.ThreadPool, maxWriteBufferSize.Value, maxWriteBufferSize.Value / 2, useSynchronizationContext: false); + + var pair = DuplexPipe.CreateConnectionPair(inputOptions, outputOptions); + + // Set the transport and connection id + Transport = pair.Transport; + Application = pair.Application; } - public override MemoryPool MemoryPool { get; } - public override PipeScheduler InputWriterScheduler => _scheduler; - public override PipeScheduler OutputReaderScheduler => _scheduler; + public PipeWriter Input => Application.Output; - public async Task StartAsync() + public PipeReader Output => Application.Input; + + public override MemoryPool MemoryPool { get; } + + public void Start() + { + _processingTask = StartAsync(); + } + + private async Task StartAsync() { try { @@ -83,7 +98,22 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal _receiver.Dispose(); _sender.Dispose(); - ThreadPool.UnsafeQueueUserWorkItem(state => ((SocketConnection)state).CancelConnectionClosedToken(), this); + + // Fire the connection closed token and wait for it to complete + var waitForConnectionClosedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + ThreadPool.UnsafeQueueUserWorkItem(state => + { + (var connection, var tcs) = state; + + connection.CancelConnectionClosedToken(); + + tcs.TrySetResult(null); + }, + (this, waitForConnectionClosedTcs), + preferLocal: false); + + await waitForConnectionClosedTcs.Task; } catch (Exception ex) { @@ -101,10 +131,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal } // Only called after connection middleware is complete which means the ConnectionClosed token has fired. - public void Dispose() + public override async ValueTask DisposeAsync() { + Transport.Input.Complete(); + Transport.Output.Complete(); + + if (_processingTask != null) + { + await _processingTask; + } + _connectionClosedTokenSource.Dispose(); - _connectionClosingCts.Dispose(); } private async Task DoReceive() @@ -211,7 +248,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal } catch (SocketException ex) when (IsConnectionResetError(ex.SocketErrorCode)) { - shutdownReason = new ConnectionResetException(ex.Message, ex);; + shutdownReason = new ConnectionResetException(ex.Message, ex); _trace.ConnectionReset(ConnectionId); } catch (Exception ex) diff --git a/src/Servers/Kestrel/Transport.Sockets/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.csproj b/src/Servers/Kestrel/Transport.Sockets/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.csproj index e5ffbb53b6..337685e9b7 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.csproj +++ b/src/Servers/Kestrel/Transport.Sockets/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.csproj @@ -10,9 +10,14 @@ CS1591;$(NoWarn) + + + + + - + diff --git a/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionListener.cs b/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionListener.cs new file mode 100644 index 0000000000..b51e22e61e --- /dev/null +++ b/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionListener.cs @@ -0,0 +1,139 @@ +// 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.Diagnostics; +using System.IO.Pipelines; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal; + +namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets +{ + internal sealed class SocketConnectionListener : IConnectionListener + { + private readonly MemoryPool _memoryPool; + private readonly int _numSchedulers; + private readonly PipeScheduler[] _schedulers; + private readonly ISocketsTrace _trace; + private Socket _listenSocket; + private int _schedulerIndex; + private readonly SocketTransportOptions _options; + + public EndPoint EndPoint { get; private set; } + + internal SocketConnectionListener( + EndPoint endpoint, + SocketTransportOptions options, + ISocketsTrace trace) + { + Debug.Assert(endpoint != null); + Debug.Assert(endpoint is IPEndPoint); + Debug.Assert(trace != null); + + EndPoint = endpoint; + _trace = trace; + _options = options; + _memoryPool = _options.MemoryPoolFactory(); + var ioQueueCount = options.IOQueueCount; + + if (ioQueueCount > 0) + { + _numSchedulers = ioQueueCount; + _schedulers = new IOQueue[_numSchedulers]; + + for (var i = 0; i < _numSchedulers; i++) + { + _schedulers[i] = new IOQueue(); + } + } + else + { + var directScheduler = new PipeScheduler[] { PipeScheduler.ThreadPool }; + _numSchedulers = directScheduler.Length; + _schedulers = directScheduler; + } + } + + internal void Bind() + { + if (_listenSocket != null) + { + throw new InvalidOperationException(SocketsStrings.TransportAlreadyBound); + } + + // TODO: Add support for UnixDomainSocket + + var listenSocket = new Socket(EndPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); + + // Kestrel expects IPv6Any to bind to both IPv6 and IPv4 + if (EndPoint is IPEndPoint ip && ip.Address == IPAddress.IPv6Any) + { + listenSocket.DualMode = true; + } + + try + { + listenSocket.Bind(EndPoint); + } + catch (SocketException e) when (e.SocketErrorCode == SocketError.AddressAlreadyInUse) + { + throw new AddressInUseException(e.Message, e); + } + + EndPoint = listenSocket.LocalEndPoint; + + listenSocket.Listen(512); + + _listenSocket = listenSocket; + } + + public async ValueTask AcceptAsync(CancellationToken cancellationToken = default) + { + while (true) + { + try + { + var acceptSocket = await _listenSocket.AcceptAsync(); + acceptSocket.NoDelay = _options.NoDelay; + + var connection = new SocketConnection(acceptSocket, _memoryPool, _schedulers[_schedulerIndex], _trace, _options.MaxReadBufferSize, _options.MaxWriteBufferSize); + + connection.Start(); + + _schedulerIndex = (_schedulerIndex + 1) % _numSchedulers; + + return connection; + } + catch (ObjectDisposedException) + { + // A call was made to UnbindAsync/DisposeAsync just return null which signals we're done + return null; + } + catch (SocketException) + { + // The connection got reset while it was in the backlog, so we try again. + _trace.ConnectionReset(connectionId: "(null)"); + } + } + } + + public ValueTask UnbindAsync(CancellationToken cancellationToken = default) + { + _listenSocket?.Dispose(); + return default; + } + + public ValueTask DisposeAsync() + { + _listenSocket?.Dispose(); + // Dispose the memory pool + _memoryPool.Dispose(); + return default; + } + } +} diff --git a/src/Servers/Kestrel/Transport.Sockets/src/SocketTransport.cs b/src/Servers/Kestrel/Transport.Sockets/src/SocketTransport.cs deleted file mode 100644 index a1b01d45bb..0000000000 --- a/src/Servers/Kestrel/Transport.Sockets/src/SocketTransport.cs +++ /dev/null @@ -1,205 +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.Buffers; -using System.Diagnostics; -using System.IO.Pipelines; -using System.Net; -using System.Net.Sockets; -using System.Runtime.ExceptionServices; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Connections; -using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; -using Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Hosting; - -namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets -{ - internal sealed class SocketTransport : ITransport - { - private readonly MemoryPool _memoryPool; - private readonly IEndPointInformation _endPointInformation; - private readonly IConnectionDispatcher _dispatcher; - private readonly IHostApplicationLifetime _appLifetime; - private readonly int _numSchedulers; - private readonly PipeScheduler[] _schedulers; - private readonly ISocketsTrace _trace; - private Socket _listenSocket; - private Task _listenTask; - private Exception _listenException; - private volatile bool _unbinding; - - internal SocketTransport( - IEndPointInformation endPointInformation, - IConnectionDispatcher dispatcher, - IHostApplicationLifetime applicationLifetime, - int ioQueueCount, - ISocketsTrace trace, - MemoryPool memoryPool) - { - Debug.Assert(endPointInformation != null); - Debug.Assert(endPointInformation.Type == ListenType.IPEndPoint); - Debug.Assert(dispatcher != null); - Debug.Assert(applicationLifetime != null); - Debug.Assert(trace != null); - - _endPointInformation = endPointInformation; - _dispatcher = dispatcher; - _appLifetime = applicationLifetime; - _trace = trace; - _memoryPool = memoryPool; - - if (ioQueueCount > 0) - { - _numSchedulers = ioQueueCount; - _schedulers = new IOQueue[_numSchedulers]; - - for (var i = 0; i < _numSchedulers; i++) - { - _schedulers[i] = new IOQueue(); - } - } - else - { - var directScheduler = new PipeScheduler[] { PipeScheduler.ThreadPool }; - _numSchedulers = directScheduler.Length; - _schedulers = directScheduler; - } - } - - public Task BindAsync() - { - if (_listenSocket != null) - { - throw new InvalidOperationException(SocketsStrings.TransportAlreadyBound); - } - - IPEndPoint endPoint = _endPointInformation.IPEndPoint; - - var listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); - - // Kestrel expects IPv6Any to bind to both IPv6 and IPv4 - if (endPoint.Address == IPAddress.IPv6Any) - { - listenSocket.DualMode = true; - } - - try - { - listenSocket.Bind(endPoint); - } - catch (SocketException e) when (e.SocketErrorCode == SocketError.AddressAlreadyInUse) - { - throw new AddressInUseException(e.Message, e); - } - - // If requested port was "0", replace with assigned dynamic port. - if (_endPointInformation.IPEndPoint.Port == 0) - { - _endPointInformation.IPEndPoint = (IPEndPoint)listenSocket.LocalEndPoint; - } - - listenSocket.Listen(512); - - _listenSocket = listenSocket; - - _listenTask = Task.Run(() => RunAcceptLoopAsync()); - - return Task.CompletedTask; - } - - public async Task UnbindAsync() - { - if (_listenSocket != null) - { - _unbinding = true; - _listenSocket.Dispose(); - - Debug.Assert(_listenTask != null); - await _listenTask.ConfigureAwait(false); - - _unbinding = false; - _listenSocket = null; - _listenTask = null; - - if (_listenException != null) - { - var exInfo = ExceptionDispatchInfo.Capture(_listenException); - _listenException = null; - exInfo.Throw(); - } - } - } - - public Task StopAsync() - { - _memoryPool.Dispose(); - return Task.CompletedTask; - } - - private async Task RunAcceptLoopAsync() - { - try - { - while (true) - { - for (var schedulerIndex = 0; schedulerIndex < _numSchedulers; schedulerIndex++) - { - try - { - var acceptSocket = await _listenSocket.AcceptAsync(); - acceptSocket.NoDelay = _endPointInformation.NoDelay; - - var connection = new SocketConnection(acceptSocket, _memoryPool, _schedulers[schedulerIndex], _trace); - - // REVIEW: This task should be tracked by the server for graceful shutdown - // Today it's handled specifically for http but not for arbitrary middleware - _ = HandleConnectionAsync(connection); - } - catch (SocketException) when (!_unbinding) - { - _trace.ConnectionReset(connectionId: "(null)"); - } - } - } - } - catch (Exception ex) - { - if (_unbinding) - { - // Means we must be unbinding. Eat the exception. - } - else - { - _trace.LogCritical(ex, $"Unexpected exception in {nameof(SocketTransport)}.{nameof(RunAcceptLoopAsync)}."); - _listenException = ex; - - // Request shutdown so we can rethrow this exception - // in Stop which should be observable. - _appLifetime.StopApplication(); - } - } - } - - private async Task HandleConnectionAsync(SocketConnection connection) - { - try - { - var middlewareTask = _dispatcher.OnConnection(connection); - var transportTask = connection.StartAsync(); - - await transportTask; - await middlewareTask; - - connection.Dispose(); - } - catch (Exception ex) - { - _trace.LogCritical(ex, $"Unexpected exception in {nameof(SocketTransport)}.{nameof(HandleConnectionAsync)}."); - } - } - } -} diff --git a/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportFactory.cs b/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportFactory.cs index f6ef805fbb..1988897855 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportFactory.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportFactory.cs @@ -2,66 +2,46 @@ // 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.Transport.Abstractions.Internal; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal; -using Microsoft.Extensions.Options; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets { -#pragma warning disable PUB0001 // Pubternal type in public API - public sealed class SocketTransportFactory : ITransportFactory -#pragma warning restore PUB0001 // Pubternal type in public API + public sealed class SocketTransportFactory : IConnectionListenerFactory { private readonly SocketTransportOptions _options; - private readonly IHostApplicationLifetime _appLifetime; private readonly SocketsTrace _trace; public SocketTransportFactory( IOptions options, - IHostApplicationLifetime applicationLifetime, ILoggerFactory loggerFactory) { if (options == null) { throw new ArgumentNullException(nameof(options)); } - if (applicationLifetime == null) - { - throw new ArgumentNullException(nameof(applicationLifetime)); - } + if (loggerFactory == null) { throw new ArgumentNullException(nameof(loggerFactory)); } _options = options.Value; - _appLifetime = applicationLifetime; - var logger = loggerFactory.CreateLogger("Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets"); + var logger = loggerFactory.CreateLogger("Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets"); _trace = new SocketsTrace(logger); } -#pragma warning disable PUB0001 // Pubternal type in public API - public ITransport Create(IEndPointInformation endPointInformation, IConnectionDispatcher dispatcher) -#pragma warning restore PUB0001 // Pubternal type in public API + public ValueTask BindAsync(EndPoint endpoint, CancellationToken cancellationToken = default) { - if (endPointInformation == null) - { - throw new ArgumentNullException(nameof(endPointInformation)); - } - - if (endPointInformation.Type != ListenType.IPEndPoint) - { - throw new ArgumentException(SocketsStrings.OnlyIPEndPointsSupported, nameof(endPointInformation)); - } - - if (dispatcher == null) - { - throw new ArgumentNullException(nameof(dispatcher)); - } - - return new SocketTransport(endPointInformation, dispatcher, _appLifetime, _options.IOQueueCount, _trace, _options.MemoryPoolFactory()); + var transport = new SocketConnectionListener(endpoint, _options, _trace); + transport.Bind(); + return new ValueTask(transport); } } } diff --git a/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs b/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs index 2dad914423..2ec6c52fd0 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs @@ -1,9 +1,9 @@ -// 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 Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; +using System.IO.Pipelines; namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets { @@ -17,6 +17,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets /// public int IOQueueCount { get; set; } = Math.Min(Environment.ProcessorCount, 16); - internal Func> MemoryPoolFactory { get; set; } = () => KestrelMemoryPool.Create(); + /// + /// Set to false to enable Nagle's algorithm for all connections. + /// + /// + /// Defaults to true. + /// + public bool NoDelay { get; set; } = true; + + public long? MaxReadBufferSize { get; set; } = PipeOptions.Default.PauseWriterThreshold; + + public long? MaxWriteBufferSize { get; set; } = PipeOptions.Default.PauseWriterThreshold; + + internal Func> MemoryPoolFactory { get; set; } = System.Buffers.MemoryPoolFactory.Create; } } diff --git a/src/Servers/Kestrel/Transport.Sockets/src/WebHostBuilderSocketExtensions.cs b/src/Servers/Kestrel/Transport.Sockets/src/WebHostBuilderSocketExtensions.cs index 95d27e46db..d073f91aa4 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/WebHostBuilderSocketExtensions.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/WebHostBuilderSocketExtensions.cs @@ -1,8 +1,8 @@ -// 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 Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; +using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets; using Microsoft.Extensions.DependencyInjection; @@ -23,7 +23,7 @@ namespace Microsoft.AspNetCore.Hosting { return hostBuilder.ConfigureServices(services => { - services.AddSingleton(); + services.AddSingleton(); }); } diff --git a/src/Servers/Kestrel/perf/Kestrel.Performance/ChunkWriterBenchmark.cs b/src/Servers/Kestrel/perf/Kestrel.Performance/ChunkWriterBenchmark.cs index 3b24e49cab..c327d8d51f 100644 --- a/src/Servers/Kestrel/perf/Kestrel.Performance/ChunkWriterBenchmark.cs +++ b/src/Servers/Kestrel/perf/Kestrel.Performance/ChunkWriterBenchmark.cs @@ -6,7 +6,6 @@ using System.IO.Pipelines; using System.Threading.Tasks; using BenchmarkDotNet.Attributes; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; -using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; namespace Microsoft.AspNetCore.Server.Kestrel.Performance { @@ -21,7 +20,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance [GlobalSetup] public void Setup() { - _memoryPool = KestrelMemoryPool.Create(); + _memoryPool = MemoryPoolFactory.Create(); var pipe = new Pipe(new PipeOptions(_memoryPool)); _reader = pipe.Reader; _writer = pipe.Writer; diff --git a/src/Servers/Kestrel/perf/Kestrel.Performance/Http1ConnectionBenchmark.cs b/src/Servers/Kestrel/perf/Kestrel.Performance/Http1ConnectionBenchmark.cs index c404a16c27..d7bdd0a709 100644 --- a/src/Servers/Kestrel/perf/Kestrel.Performance/Http1ConnectionBenchmark.cs +++ b/src/Servers/Kestrel/perf/Kestrel.Performance/Http1ConnectionBenchmark.cs @@ -10,7 +10,6 @@ using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; -using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; namespace Microsoft.AspNetCore.Server.Kestrel.Performance { @@ -27,7 +26,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance [GlobalSetup] public void Setup() { - var memoryPool = KestrelMemoryPool.Create(); + var memoryPool = MemoryPoolFactory.Create(); var options = new PipeOptions(memoryPool, readerScheduler: PipeScheduler.Inline, writerScheduler: PipeScheduler.Inline, useSynchronizationContext: false); var pair = DuplexPipe.CreateConnectionPair(options, options); diff --git a/src/Servers/Kestrel/perf/Kestrel.Performance/Http1ConnectionParsingOverheadBenchmark.cs b/src/Servers/Kestrel/perf/Kestrel.Performance/Http1ConnectionParsingOverheadBenchmark.cs index 84ac57e630..8ffe0a0059 100644 --- a/src/Servers/Kestrel/perf/Kestrel.Performance/Http1ConnectionParsingOverheadBenchmark.cs +++ b/src/Servers/Kestrel/perf/Kestrel.Performance/Http1ConnectionParsingOverheadBenchmark.cs @@ -9,7 +9,6 @@ using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; -using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; namespace Microsoft.AspNetCore.Server.Kestrel.Performance { @@ -23,7 +22,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance [IterationSetup] public void Setup() { - var memoryPool = KestrelMemoryPool.Create(); + var memoryPool = MemoryPoolFactory.Create(); var options = new PipeOptions(memoryPool, readerScheduler: PipeScheduler.Inline, writerScheduler: PipeScheduler.Inline, useSynchronizationContext: false); var pair = DuplexPipe.CreateConnectionPair(options, options); diff --git a/src/Servers/Kestrel/perf/Kestrel.Performance/Http1ReadingBenchmark.cs b/src/Servers/Kestrel/perf/Kestrel.Performance/Http1ReadingBenchmark.cs index 122c5b694c..2faca73d6a 100644 --- a/src/Servers/Kestrel/perf/Kestrel.Performance/Http1ReadingBenchmark.cs +++ b/src/Servers/Kestrel/perf/Kestrel.Performance/Http1ReadingBenchmark.cs @@ -13,7 +13,6 @@ using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; -using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; using Microsoft.AspNetCore.Testing; namespace Microsoft.AspNetCore.Server.Kestrel.Performance @@ -35,7 +34,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance [GlobalSetup] public void GlobalSetup() { - _memoryPool = KestrelMemoryPool.Create(); + _memoryPool = MemoryPoolFactory.Create(); _http1Connection = MakeHttp1Connection(); } diff --git a/src/Servers/Kestrel/perf/Kestrel.Performance/Http1WritingBenchmark.cs b/src/Servers/Kestrel/perf/Kestrel.Performance/Http1WritingBenchmark.cs index d0c1cf3370..94dc4e66c5 100644 --- a/src/Servers/Kestrel/perf/Kestrel.Performance/Http1WritingBenchmark.cs +++ b/src/Servers/Kestrel/perf/Kestrel.Performance/Http1WritingBenchmark.cs @@ -13,7 +13,6 @@ using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; -using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; using Microsoft.AspNetCore.Testing; namespace Microsoft.AspNetCore.Server.Kestrel.Performance @@ -35,7 +34,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance [GlobalSetup] public void GlobalSetup() { - _memoryPool = KestrelMemoryPool.Create(); + _memoryPool = MemoryPoolFactory.Create(); _http1Connection = MakeHttp1Connection(); } diff --git a/src/Servers/Kestrel/perf/Kestrel.Performance/HttpProtocolFeatureCollection.cs b/src/Servers/Kestrel/perf/Kestrel.Performance/HttpProtocolFeatureCollection.cs index 3f8bae879f..7f948c5a4b 100644 --- a/src/Servers/Kestrel/perf/Kestrel.Performance/HttpProtocolFeatureCollection.cs +++ b/src/Servers/Kestrel/perf/Kestrel.Performance/HttpProtocolFeatureCollection.cs @@ -1,7 +1,8 @@ -// 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.IO.Pipelines; using System.Runtime.CompilerServices; using System.Threading; @@ -11,7 +12,6 @@ using Microsoft.AspNetCore.Http.Features; 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.Transport.Abstractions.Internal; namespace Microsoft.AspNetCore.Server.Kestrel.Performance { @@ -78,7 +78,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance public HttpProtocolFeatureCollection() { - var memoryPool = KestrelMemoryPool.Create(); + var memoryPool = MemoryPoolFactory.Create(); var options = new PipeOptions(memoryPool, readerScheduler: PipeScheduler.Inline, writerScheduler: PipeScheduler.Inline, useSynchronizationContext: false); var pair = DuplexPipe.CreateConnectionPair(options, options); diff --git a/src/Servers/Kestrel/perf/Kestrel.Performance/InMemoryTransportBenchmark.cs b/src/Servers/Kestrel/perf/Kestrel.Performance/InMemoryTransportBenchmark.cs index d5421ab448..b3e4e13eff 100644 --- a/src/Servers/Kestrel/perf/Kestrel.Performance/InMemoryTransportBenchmark.cs +++ b/src/Servers/Kestrel/perf/Kestrel.Performance/InMemoryTransportBenchmark.cs @@ -6,13 +6,15 @@ using System.Buffers; using System.Collections.Generic; using System.IO.Pipelines; using System.Linq; +using System.Net; using System.Text; +using System.Threading; using System.Threading.Tasks; 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; using Microsoft.Extensions.DependencyInjection; namespace Microsoft.AspNetCore.Server.Kestrel.Performance @@ -45,7 +47,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance .UseKestrel() // Bind to a single non-HTTPS endpoint .UseUrls("http://127.0.0.1:5000") - .ConfigureServices(services => services.AddSingleton(transportFactory)) + .ConfigureServices(services => services.AddSingleton(transportFactory)) .Configure(app => app.UseMiddleware()) .Build(); @@ -94,21 +96,21 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance await _connection.ReadResponseAsync(_plaintextPipelinedExpectedResponse.Length); } - internal class InMemoryTransportFactory : ITransportFactory + internal class InMemoryTransportFactory : IConnectionListenerFactory { private readonly int _connectionsPerEndPoint; - private readonly Dictionary> _connections = - new Dictionary>(); + private readonly Dictionary> _connections = + new Dictionary>(); - public IReadOnlyDictionary> Connections => _connections; + public IReadOnlyDictionary> Connections => _connections; public InMemoryTransportFactory(int connectionsPerEndPoint) { _connectionsPerEndPoint = connectionsPerEndPoint; } - public ITransport Create(IEndPointInformation endPointInformation, IConnectionDispatcher handler) + public ValueTask BindAsync(EndPoint endpoint, CancellationToken cancellationToken = default) { var connections = new InMemoryConnection[_connectionsPerEndPoint]; for (var i = 0; i < _connectionsPerEndPoint; i++) @@ -116,46 +118,66 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance connections[i] = new InMemoryConnection(); } - _connections.Add(endPointInformation, connections); + _connections.Add(endpoint, connections); - return new InMemoryTransport(handler, connections); + return new ValueTask(new InMemoryTransport(endpoint, connections)); } } - public class InMemoryTransport : ITransport + public class InMemoryTransport : IConnectionListener { - private readonly IConnectionDispatcher _dispatcher; private readonly IReadOnlyList _connections; + private readonly TaskCompletionSource _tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + private int _acceptedConnections; - public InMemoryTransport(IConnectionDispatcher dispatcher, IReadOnlyList connections) + public InMemoryTransport(EndPoint endpoint, IReadOnlyList connections) { - _dispatcher = dispatcher; + EndPoint = endpoint; _connections = connections; } - public Task BindAsync() + public EndPoint EndPoint { get; } + + public ValueTask AcceptAsync(CancellationToken cancellationToken = default) { - foreach (var connection in _connections) + if (_acceptedConnections < _connections.Count) { - _dispatcher.OnConnection(connection); + return new ValueTask(_connections[_acceptedConnections++]); } - - return Task.CompletedTask; + return new ValueTask(_tcs.Task); } - public Task StopAsync() + public ValueTask DisposeAsync() { - return Task.CompletedTask; + _tcs.TrySetResult(null); + return default; } - public Task UnbindAsync() + public ValueTask UnbindAsync(CancellationToken cancellationToken = default) { - return Task.CompletedTask; + _tcs.TrySetResult(null); + return default; } } public class InMemoryConnection : TransportConnection { + public InMemoryConnection() + { + var inputOptions = new PipeOptions(useSynchronizationContext: false); + var outputOptions = new PipeOptions(useSynchronizationContext: false); + + var pair = DuplexPipe.CreateConnectionPair(inputOptions, outputOptions); + + // Set the transport and connection id + Transport = pair.Transport; + Application = pair.Application; + } + + public PipeWriter Input => Application.Output; + + public PipeReader Output => Application.Input; + public ValueTask SendRequestAsync(byte[] request) { return Input.WriteAsync(request); diff --git a/src/Servers/Kestrel/perf/Kestrel.Performance/Microsoft.AspNetCore.Server.Kestrel.Performance.csproj b/src/Servers/Kestrel/perf/Kestrel.Performance/Microsoft.AspNetCore.Server.Kestrel.Performance.csproj index e801048b67..93d77cab5f 100644 --- a/src/Servers/Kestrel/perf/Kestrel.Performance/Microsoft.AspNetCore.Server.Kestrel.Performance.csproj +++ b/src/Servers/Kestrel/perf/Kestrel.Performance/Microsoft.AspNetCore.Server.Kestrel.Performance.csproj @@ -10,6 +10,7 @@ + diff --git a/src/Servers/Kestrel/perf/Kestrel.Performance/PipeThroughputBenchmark.cs b/src/Servers/Kestrel/perf/Kestrel.Performance/PipeThroughputBenchmark.cs index b37656faec..03b3eb2f1d 100644 --- a/src/Servers/Kestrel/perf/Kestrel.Performance/PipeThroughputBenchmark.cs +++ b/src/Servers/Kestrel/perf/Kestrel.Performance/PipeThroughputBenchmark.cs @@ -1,11 +1,10 @@ -// 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.Buffers; using System.IO.Pipelines; using System.Threading.Tasks; using BenchmarkDotNet.Attributes; -using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; namespace Microsoft.AspNetCore.Server.Kestrel.Performance { @@ -20,7 +19,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance [IterationSetup] public void Setup() { - _memoryPool = KestrelMemoryPool.Create(); + _memoryPool = MemoryPoolFactory.Create(); _pipe = new Pipe(new PipeOptions(_memoryPool)); } diff --git a/src/Servers/Kestrel/perf/Kestrel.Performance/RequestParsingBenchmark.cs b/src/Servers/Kestrel/perf/Kestrel.Performance/RequestParsingBenchmark.cs index da9f0f7faf..a6cba51178 100644 --- a/src/Servers/Kestrel/perf/Kestrel.Performance/RequestParsingBenchmark.cs +++ b/src/Servers/Kestrel/perf/Kestrel.Performance/RequestParsingBenchmark.cs @@ -9,7 +9,6 @@ using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; -using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; namespace Microsoft.AspNetCore.Server.Kestrel.Performance { @@ -24,7 +23,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance [IterationSetup] public void Setup() { - _memoryPool = KestrelMemoryPool.Create(); + _memoryPool = MemoryPoolFactory.Create(); var options = new PipeOptions(_memoryPool, readerScheduler: PipeScheduler.Inline, writerScheduler: PipeScheduler.Inline, useSynchronizationContext: false); var pair = DuplexPipe.CreateConnectionPair(options, options); diff --git a/src/Servers/Kestrel/perf/Kestrel.Performance/ResponseHeaderCollectionBenchmark.cs b/src/Servers/Kestrel/perf/Kestrel.Performance/ResponseHeaderCollectionBenchmark.cs index 530a0ec968..417c5a8bbe 100644 --- a/src/Servers/Kestrel/perf/Kestrel.Performance/ResponseHeaderCollectionBenchmark.cs +++ b/src/Servers/Kestrel/perf/Kestrel.Performance/ResponseHeaderCollectionBenchmark.cs @@ -2,6 +2,7 @@ // 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.IO.Pipelines; using System.Runtime.CompilerServices; using System.Text; @@ -12,7 +13,6 @@ using Microsoft.AspNetCore.Http.Internal; 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.Transport.Abstractions.Internal; using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.Server.Kestrel.Performance @@ -172,7 +172,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance [IterationSetup] public void Setup() { - var memoryPool = KestrelMemoryPool.Create(); + var memoryPool = MemoryPoolFactory.Create(); var options = new PipeOptions(memoryPool, readerScheduler: PipeScheduler.Inline, writerScheduler: PipeScheduler.Inline, useSynchronizationContext: false); var pair = DuplexPipe.CreateConnectionPair(options, options); diff --git a/src/Servers/Kestrel/samples/Http2SampleApp/Program.cs b/src/Servers/Kestrel/samples/Http2SampleApp/Program.cs index f1fc2223cf..794f011ecc 100644 --- a/src/Servers/Kestrel/samples/Http2SampleApp/Program.cs +++ b/src/Servers/Kestrel/samples/Http2SampleApp/Program.cs @@ -7,7 +7,6 @@ using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core.Adapter.Internal; -using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; @@ -29,9 +28,6 @@ namespace Http2SampleApp { var basePort = context.Configuration.GetValue("BASE_PORT") ?? 5000; - // Run callbacks on the transport thread - options.ApplicationSchedulingMode = SchedulingMode.Inline; - // Http/1.1 endpoint for comparison options.Listen(IPAddress.Any, basePort, listenOptions => { diff --git a/src/Servers/Kestrel/samples/PlaintextApp/PlaintextApp.csproj b/src/Servers/Kestrel/samples/PlaintextApp/PlaintextApp.csproj index 86fd68771b..8b48f958a9 100644 --- a/src/Servers/Kestrel/samples/PlaintextApp/PlaintextApp.csproj +++ b/src/Servers/Kestrel/samples/PlaintextApp/PlaintextApp.csproj @@ -1,4 +1,4 @@ - + netcoreapp3.0 @@ -8,6 +8,7 @@ + diff --git a/src/Servers/Kestrel/samples/PlaintextApp/Startup.cs b/src/Servers/Kestrel/samples/PlaintextApp/Startup.cs index 98dc353f23..044e8b5dfe 100644 --- a/src/Servers/Kestrel/samples/PlaintextApp/Startup.cs +++ b/src/Servers/Kestrel/samples/PlaintextApp/Startup.cs @@ -8,6 +8,7 @@ using System.Runtime.CompilerServices; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Hosting; namespace PlaintextApp @@ -31,7 +32,7 @@ namespace PlaintextApp }); } - public static Task Main(string[] args) + public static async Task Main(string[] args) { var host = new WebHostBuilder() .UseKestrel(options => @@ -42,7 +43,7 @@ namespace PlaintextApp .UseStartup() .Build(); - return host.RunAsync(); + await host.RunAsync(); } } diff --git a/src/Servers/Kestrel/samples/SampleApp/Startup.cs b/src/Servers/Kestrel/samples/SampleApp/Startup.cs index 71f218d816..82f505c510 100644 --- a/src/Servers/Kestrel/samples/SampleApp/Startup.cs +++ b/src/Servers/Kestrel/samples/SampleApp/Startup.cs @@ -14,7 +14,6 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Https.Internal; -using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -85,19 +84,11 @@ namespace SampleApp var basePort = context.Configuration.GetValue("BASE_PORT") ?? 5000; - options.ConfigureEndpointDefaults(opt => - { - opt.NoDelay = true; - }); - options.ConfigureHttpsDefaults(httpsOptions => { httpsOptions.SslProtocols = SslProtocols.Tls12; }); - // Run callbacks on the transport thread - options.ApplicationSchedulingMode = SchedulingMode.Inline; - options.Listen(IPAddress.Loopback, basePort, listenOptions => { // Uncomment the following to enable Nagle's algorithm for this endpoint. @@ -148,7 +139,7 @@ namespace SampleApp .Configure(context.Configuration.GetSection("Kestrel")) .Endpoint("NamedEndpoint", opt => { - opt.ListenOptions.NoDelay = true; + }) .Endpoint("NamedHttpsEndpoint", opt => { diff --git a/src/Servers/Kestrel/samples/SystemdTestApp/Startup.cs b/src/Servers/Kestrel/samples/SystemdTestApp/Startup.cs index e91322a827..56e6f3f980 100644 --- a/src/Servers/Kestrel/samples/SystemdTestApp/Startup.cs +++ b/src/Servers/Kestrel/samples/SystemdTestApp/Startup.cs @@ -9,7 +9,6 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; @@ -51,9 +50,6 @@ namespace SystemdTestApp { var basePort = context.Configuration.GetValue("BASE_PORT") ?? 5000; - // Run callbacks on the transport thread - options.ApplicationSchedulingMode = SchedulingMode.Inline; - options.Listen(IPAddress.Loopback, basePort, listenOptions => { // Uncomment the following to enable Nagle's algorithm for this endpoint. @@ -89,4 +85,4 @@ namespace SystemdTestApp return hostBuilder.Build().RunAsync(); } } -} \ No newline at end of file +} diff --git a/src/Servers/Kestrel/shared/test/TaskTimeoutExtensions.cs b/src/Servers/Kestrel/shared/test/TaskTimeoutExtensions.cs index 8e83a7a70e..68010b090a 100644 --- a/src/Servers/Kestrel/shared/test/TaskTimeoutExtensions.cs +++ b/src/Servers/Kestrel/shared/test/TaskTimeoutExtensions.cs @@ -7,6 +7,16 @@ namespace System.Threading.Tasks { public static class TaskTimeoutExtensions { + public static Task DefaultTimeout(this ValueTask task) + { + return task.AsTask().TimeoutAfter(TestConstants.DefaultTimeout); + } + + public static Task DefaultTimeout(this ValueTask task) + { + return task.AsTask().TimeoutAfter(TestConstants.DefaultTimeout); + } + public static Task DefaultTimeout(this Task task) { return task.TimeoutAfter(TestConstants.DefaultTimeout); diff --git a/src/Servers/Kestrel/shared/test/TestServiceContext.cs b/src/Servers/Kestrel/shared/test/TestServiceContext.cs index 7b6abf939d..e5332963aa 100644 --- a/src/Servers/Kestrel/shared/test/TestServiceContext.cs +++ b/src/Servers/Kestrel/shared/test/TestServiceContext.cs @@ -8,7 +8,6 @@ using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; -using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Testing @@ -75,7 +74,7 @@ namespace Microsoft.AspNetCore.Testing public MockSystemClock MockSystemClock { get; set; } - public Func> MemoryPoolFactory { get; set; } = KestrelMemoryPool.Create; + public Func> MemoryPoolFactory { get; set; } = System.Buffers.MemoryPoolFactory.Create; public int ExpectedConnectionMiddlewareCount { get; set; } diff --git a/src/Servers/Kestrel/shared/test/TransportTestHelpers/DiagnosticMemoryPoolFactory.cs b/src/Servers/Kestrel/shared/test/TransportTestHelpers/DiagnosticMemoryPoolFactory.cs index d3a33bf2cf..26c8746e39 100644 --- a/src/Servers/Kestrel/shared/test/TransportTestHelpers/DiagnosticMemoryPoolFactory.cs +++ b/src/Servers/Kestrel/shared/test/TransportTestHelpers/DiagnosticMemoryPoolFactory.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; @@ -6,7 +6,6 @@ using System.Buffers; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests { @@ -29,7 +28,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests { lock (_pools) { - var pool = new DiagnosticMemoryPool(KestrelMemoryPool.CreateSlabMemoryPool(), _allowLateReturn, _rentTracking); + var pool = new DiagnosticMemoryPool(new SlabMemoryPool(), _allowLateReturn, _rentTracking); _pools.Add(pool); return pool; } @@ -43,4 +42,4 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests } } } -} \ No newline at end of file +} diff --git a/src/Servers/Kestrel/shared/test/TransportTestHelpers/TestServer.cs b/src/Servers/Kestrel/shared/test/TransportTestHelpers/TestServer.cs index bbb6d6fe49..1382a70175 100644 --- a/src/Servers/Kestrel/shared/test/TransportTestHelpers/TestServer.cs +++ b/src/Servers/Kestrel/shared/test/TransportTestHelpers/TestServer.cs @@ -8,11 +8,11 @@ using System.Reflection; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Server.Kestrel.Core; -using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -61,7 +61,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests _app = app; Context = context; - _host = TransportSelector.GetWebHostBuilder(context.MemoryPoolFactory) + _host = TransportSelector.GetWebHostBuilder(context.MemoryPoolFactory, context.ServerOptions.Limits.MaxRequestBufferSize) .UseKestrel(options => { configureKestrel(options); @@ -86,7 +86,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests Assert.All(context.ServerOptions.ListenOptions, lo => Assert.Equal(context.ExpectedConnectionMiddlewareCount, lo._middleware.Count)); - return new KestrelServer(sp.GetRequiredService(), context); + return new KestrelServer(sp.GetRequiredService(), context); }); configureServices(services); }) diff --git a/src/Servers/Kestrel/test/FunctionalTests/MaxRequestBufferSizeTests.cs b/src/Servers/Kestrel/test/FunctionalTests/MaxRequestBufferSizeTests.cs index 540690317b..039b148db0 100644 --- a/src/Servers/Kestrel/test/FunctionalTests/MaxRequestBufferSizeTests.cs +++ b/src/Servers/Kestrel/test/FunctionalTests/MaxRequestBufferSizeTests.cs @@ -289,7 +289,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests TaskCompletionSource clientFinishedSendingRequestBody, Func> memoryPoolFactory = null) { - var host = TransportSelector.GetWebHostBuilder(memoryPoolFactory) + var host = TransportSelector.GetWebHostBuilder(memoryPoolFactory, maxRequestBufferSize) .ConfigureServices(AddTestLogging) .UseKestrel(options => { diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/ChunkedRequestTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/ChunkedRequestTests.cs index 2f3f01a941..12ac5791ff 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/ChunkedRequestTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/ChunkedRequestTests.cs @@ -127,7 +127,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests [Fact] public async Task Http10KeepAliveTransferEncoding() { - var testContext = new TestServiceContext(); + var testContext = new TestServiceContext(LoggerFactory); await using (var server = new TestServer(AppChunked, testContext)) { diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs index 6751565db1..424307ac26 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs @@ -23,7 +23,6 @@ using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.FlowControl; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; -using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; @@ -115,7 +114,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests protected static readonly byte[] _noData = new byte[0]; protected static readonly byte[] _maxData = Encoding.ASCII.GetBytes(new string('a', Http2PeerSettings.MinAllowedMaxFrameSize)); - private readonly MemoryPool _memoryPool = KestrelMemoryPool.Create(); + private readonly MemoryPool _memoryPool = MemoryPoolFactory.Create(); internal readonly Http2PeerSettings _clientSettings = new Http2PeerSettings(); internal readonly HPackEncoder _hpackEncoder = new HPackEncoder(); @@ -429,8 +428,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests // Always dispatch test code back to the ThreadPool. This prevents deadlocks caused by continuing // Http2Connection.ProcessRequestsAsync() loop with writer locks acquired. Run product code inline to make // it easier to verify request frames are processed correctly immediately after sending the them. - var inputPipeOptions = ConnectionDispatcher.GetInputPipeOptions(_serviceContext, _memoryPool, PipeScheduler.ThreadPool); - var outputPipeOptions = ConnectionDispatcher.GetOutputPipeOptions(_serviceContext, _memoryPool, PipeScheduler.ThreadPool); + var inputPipeOptions = GetInputPipeOptions(_serviceContext, _memoryPool, PipeScheduler.ThreadPool); + var outputPipeOptions = GetOutputPipeOptions(_serviceContext, _memoryPool, PipeScheduler.ThreadPool); _pair = DuplexPipe.CreateConnectionPair(inputPipeOptions, outputPipeOptions); @@ -1248,6 +1247,41 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests _timeoutControl.Tick(clock.UtcNow); } + private static PipeOptions GetInputPipeOptions(ServiceContext serviceContext, MemoryPool memoryPool, PipeScheduler writerScheduler) => new PipeOptions + ( + pool: memoryPool, + readerScheduler: serviceContext.Scheduler, + writerScheduler: writerScheduler, + pauseWriterThreshold: serviceContext.ServerOptions.Limits.MaxRequestBufferSize ?? 0, + resumeWriterThreshold: serviceContext.ServerOptions.Limits.MaxRequestBufferSize ?? 0, + useSynchronizationContext: false, + minimumSegmentSize: memoryPool.GetMinimumSegmentSize() + ); + + private static PipeOptions GetOutputPipeOptions(ServiceContext serviceContext, MemoryPool memoryPool, PipeScheduler readerScheduler) => new PipeOptions + ( + pool: memoryPool, + readerScheduler: readerScheduler, + writerScheduler: serviceContext.Scheduler, + pauseWriterThreshold: GetOutputResponseBufferSize(serviceContext), + resumeWriterThreshold: GetOutputResponseBufferSize(serviceContext), + useSynchronizationContext: false, + minimumSegmentSize: memoryPool.GetMinimumSegmentSize() + ); + + private static long GetOutputResponseBufferSize(ServiceContext serviceContext) + { + var bufferSize = serviceContext.ServerOptions.Limits.MaxResponseBufferSize; + if (bufferSize == 0) + { + // 0 = no buffering so we need to configure the pipe so the writer waits on the reader directly + return 1; + } + + // null means that we have no back pressure + return bufferSize ?? 0; + } + internal class Http2FrameWithPayload : Http2Frame { public Http2FrameWithPayload() : base() diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/InMemory.FunctionalTests.csproj b/src/Servers/Kestrel/test/InMemory.FunctionalTests/InMemory.FunctionalTests.csproj index c7b344c2c8..c1c9107598 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/InMemory.FunctionalTests.csproj +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/InMemory.FunctionalTests.csproj @@ -4,6 +4,7 @@ netcoreapp3.0 true InMemory.FunctionalTests + true @@ -11,9 +12,11 @@ + + diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/KeepAliveTimeoutTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/KeepAliveTimeoutTests.cs index 422520f5b8..34f3932129 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/KeepAliveTimeoutTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/KeepAliveTimeoutTests.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.IO.Pipelines; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -33,6 +34,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests { using (var connection = server.CreateConnection()) { + await connection.TransportConnection.WaitForReadTask; + await connection.Send( "GET / HTTP/1.1", "Host:", @@ -59,6 +62,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests { using (var connection = server.CreateConnection()) { + await connection.TransportConnection.WaitForReadTask; + for (var i = 0; i < 10; i++) { await connection.Send( @@ -86,6 +91,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests { using (var connection = server.CreateConnection()) { + await connection.TransportConnection.WaitForReadTask; + await connection.Send( "POST /consume HTTP/1.1", "Host:", @@ -126,6 +133,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests { using (var connection = server.CreateConnection()) { + await connection.TransportConnection.WaitForReadTask; + await connection.Send( "GET /longrunning HTTP/1.1", "Host:", @@ -164,6 +173,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests { using (var connection = server.CreateConnection()) { + await connection.TransportConnection.WaitForReadTask; + // Min amount of time between requests that triggers a keep-alive timeout. testContext.MockSystemClock.UtcNow += _keepAliveTimeout + Heartbeat.Interval + TimeSpan.FromTicks(1); heartbeatManager.OnHeartbeat(testContext.SystemClock.UtcNow); @@ -184,6 +195,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests { using (var connection = server.CreateConnection()) { + await connection.TransportConnection.WaitForReadTask; + await connection.Send( "GET /upgrade HTTP/1.1", "Host:", @@ -212,6 +225,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests private TestServer CreateServer(TestServiceContext context, CancellationToken longRunningCt = default, CancellationToken upgradeCt = default) { + // Ensure request headers timeout is started as soon as the tests send requests. + context.Scheduler = PipeScheduler.Inline; context.ServerOptions.AddServerHeader = false; context.ServerOptions.Limits.KeepAliveTimeout = _keepAliveTimeout; context.ServerOptions.Limits.MinRequestBodyDataRate = null; diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestHeadersTimeoutTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestHeadersTimeoutTests.cs index 65448bd6d2..e217ece3f0 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestHeadersTimeoutTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestHeadersTimeoutTests.cs @@ -32,6 +32,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests { using (var connection = server.CreateConnection()) { + await connection.TransportConnection.WaitForReadTask; + await connection.Send( "GET / HTTP/1.1", headers); @@ -55,6 +57,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests { using (var connection = server.CreateConnection()) { + await connection.TransportConnection.WaitForReadTask; + await connection.Send( "POST / HTTP/1.1", "Host:", @@ -86,6 +90,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests { using (var connection = server.CreateConnection()) { + await connection.TransportConnection.WaitForReadTask; + await connection.Send(requestLine); // Min amount of time between requests that triggers a request headers timeout. @@ -110,6 +116,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests { using (var connection = server.CreateConnection()) { + await connection.TransportConnection.WaitForReadTask; + foreach (var ch in "POST / HTTP/1.1\r\nHost:\r\n\r\n") { await connection.Send(ch.ToString()); diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/TestTransport/InMemoryTransportConnection.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/TestTransport/InMemoryTransportConnection.cs index 95c1907c0a..cc91efd9b2 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/TestTransport/InMemoryTransportConnection.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/TestTransport/InMemoryTransportConnection.cs @@ -6,34 +6,45 @@ using System.Buffers; using System.IO.Pipelines; using System.Net; using System.Threading; +using System.Threading.Tasks; +using System.Threading.Tasks.Sources; using Microsoft.AspNetCore.Connections; -using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport { - internal class InMemoryTransportConnection : TransportConnection, IDisposable + internal class InMemoryTransportConnection : TransportConnection { private readonly CancellationTokenSource _connectionClosedTokenSource = new CancellationTokenSource(); private readonly ILogger _logger; private bool _isClosed; + private readonly TaskCompletionSource _waitForCloseTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - public InMemoryTransportConnection(MemoryPool memoryPool, ILogger logger) + public InMemoryTransportConnection(MemoryPool memoryPool, ILogger logger, PipeScheduler scheduler = null) { MemoryPool = memoryPool; _logger = logger; - LocalAddress = IPAddress.Loopback; - RemoteAddress = IPAddress.Loopback; + LocalEndPoint = new IPEndPoint(IPAddress.Loopback, 0); + RemoteEndPoint = new IPEndPoint(IPAddress.Loopback, 0); + + var pair = DuplexPipe.CreateConnectionPair(new PipeOptions(memoryPool, readerScheduler: scheduler, useSynchronizationContext: false), new PipeOptions(memoryPool, writerScheduler: scheduler, useSynchronizationContext: false)); + Application = pair.Application; + var wrapper = new ObservableDuplexPipe(pair.Transport); + Transport = wrapper; + WaitForReadTask = wrapper.WaitForReadTask; ConnectionClosed = _connectionClosedTokenSource.Token; } - public override MemoryPool MemoryPool { get; } + public PipeWriter Input => Application.Output; - public override PipeScheduler InputWriterScheduler => PipeScheduler.ThreadPool; - public override PipeScheduler OutputReaderScheduler => PipeScheduler.ThreadPool; + public PipeReader Output => Application.Input; + + public Task WaitForReadTask { get; } + + public override MemoryPool MemoryPool { get; } public ConnectionAbortedException AbortReason { get; private set; } @@ -57,14 +68,141 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTrans ThreadPool.UnsafeQueueUserWorkItem(state => { - var self = (InMemoryTransportConnection)state; - self._connectionClosedTokenSource.Cancel(); - }, this); + state._connectionClosedTokenSource.Cancel(); + + state._waitForCloseTcs.TrySetResult(null); + }, + this, + preferLocal: false); } - public void Dispose() + public override async ValueTask DisposeAsync() { + Transport.Input.Complete(); + Transport.Output.Complete(); + + await _waitForCloseTcs.Task; + _connectionClosedTokenSource.Dispose(); } + + // This piece of code allows us to wait until the PipeReader has been awaited on. + // We need to wrap lots of layers (including the ValueTask) to gain visiblity into when + // the machinery for the await happens + private class ObservableDuplexPipe : IDuplexPipe + { + private readonly ObservablePipeReader _reader; + + public ObservableDuplexPipe(IDuplexPipe duplexPipe) + { + _reader = new ObservablePipeReader(duplexPipe.Input); + + Input = _reader; + Output = duplexPipe.Output; + + } + + public Task WaitForReadTask => _reader.WaitForReadTask; + + public PipeReader Input { get; } + + public PipeWriter Output { get; } + + private class ObservablePipeReader : PipeReader + { + private readonly PipeReader _reader; + private readonly TaskCompletionSource _tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + public Task WaitForReadTask => _tcs.Task; + + public ObservablePipeReader(PipeReader reader) + { + _reader = reader; + } + + public override void AdvanceTo(SequencePosition consumed) + { + _reader.AdvanceTo(consumed); + } + + public override void AdvanceTo(SequencePosition consumed, SequencePosition examined) + { + _reader.AdvanceTo(consumed, examined); + } + + public override void CancelPendingRead() + { + _reader.CancelPendingRead(); + } + + public override void Complete(Exception exception = null) + { + _reader.Complete(exception); + } + + public override void OnWriterCompleted(Action callback, object state) + { + _reader.OnWriterCompleted(callback, state); + } + + public override ValueTask ReadAsync(CancellationToken cancellationToken = default) + { + var task = _reader.ReadAsync(cancellationToken); + + if (_tcs.Task.IsCompleted) + { + return task; + } + + return new ValueTask(new ObservableValueTask(task, _tcs), 0); + } + + public override bool TryRead(out ReadResult result) + { + return _reader.TryRead(out result); + } + + private class ObservableValueTask : IValueTaskSource + { + private readonly ValueTask _task; + private readonly TaskCompletionSource _tcs; + + public ObservableValueTask(ValueTask task, TaskCompletionSource tcs) + { + _task = task; + _tcs = tcs; + } + + public T GetResult(short token) + { + return _task.GetAwaiter().GetResult(); + } + + public ValueTaskSourceStatus GetStatus(short token) + { + if (_task.IsCanceled) + { + return ValueTaskSourceStatus.Canceled; + } + if (_task.IsFaulted) + { + return ValueTaskSourceStatus.Faulted; + } + if (_task.IsCompleted) + { + return ValueTaskSourceStatus.Succeeded; + } + return ValueTaskSourceStatus.Pending; + } + + public void OnCompleted(Action continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags) + { + _task.GetAwaiter().UnsafeOnCompleted(() => continuation(state)); + + _tcs.TrySetResult(null); + } + } + } + } } } diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/TestTransport/InMemoryTransportFactory.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/TestTransport/InMemoryTransportFactory.cs index eeb271a706..6c5ff14882 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/TestTransport/InMemoryTransportFactory.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/TestTransport/InMemoryTransportFactory.cs @@ -2,43 +2,56 @@ // 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; +using System.Threading.Channels; using System.Threading.Tasks; -using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; +using Microsoft.AspNetCore.Connections; namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport { - internal class InMemoryTransportFactory : ITransportFactory + internal class InMemoryTransportFactory : IConnectionListenerFactory, IConnectionListener { - public ITransport Create(IEndPointInformation endPointInformation, IConnectionDispatcher dispatcher) + private Channel _acceptQueue = Channel.CreateUnbounded(); + + public EndPoint EndPoint { get; set; } + + public void AddConnection(ConnectionContext connection) { - if (ConnectionDispatcher != null) - { - throw new InvalidOperationException("InMemoryTransportFactory doesn't support creating multiple endpoints"); - } - - ConnectionDispatcher = dispatcher; - - return new NoopTransport(); + _acceptQueue.Writer.TryWrite(connection); } - public IConnectionDispatcher ConnectionDispatcher { get; private set; } - - private class NoopTransport : ITransport + public async ValueTask AcceptAsync(CancellationToken cancellationToken = default) { - public Task BindAsync() + if (await _acceptQueue.Reader.WaitToReadAsync(cancellationToken)) { - return Task.CompletedTask; + while (_acceptQueue.Reader.TryRead(out var item)) + { + return item; + } } - public Task StopAsync() - { - return Task.CompletedTask; - } + return null; - public Task UnbindAsync() - { - return Task.CompletedTask; - } + } + + public ValueTask BindAsync(EndPoint endpoint, CancellationToken cancellationToken = default) + { + EndPoint = endpoint; + + return new ValueTask(this); + } + + public ValueTask DisposeAsync() + { + return UnbindAsync(default); + } + + public ValueTask UnbindAsync(CancellationToken cancellationToken = default) + { + _acceptQueue.Writer.TryComplete(); + + return default; } } } diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/TestTransport/TestServer.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/TestTransport/TestServer.cs index 9f3828f07b..c7bab027d8 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/TestTransport/TestServer.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/TestTransport/TestServer.cs @@ -102,8 +102,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTrans public InMemoryConnection CreateConnection() { - var transportConnection = new InMemoryTransportConnection(_memoryPool, Context.Log); - _ = HandleConnection(transportConnection); + var transportConnection = new InMemoryTransportConnection(_memoryPool, Context.Log, Context.Scheduler); + _transportFactory.AddConnection(transportConnection); return new InMemoryConnection(transportConnection); } @@ -128,36 +128,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTrans return services.BuildServiceProvider(); } - private async Task HandleConnection(InMemoryTransportConnection transportConnection) - { - try - { - var middlewareTask = _transportFactory.ConnectionDispatcher.OnConnection(transportConnection); - var transportTask = CancellationTokenAsTask(transportConnection.ConnectionClosed); - - await transportTask; - await middlewareTask; - - transportConnection.Dispose(); - } - catch (Exception ex) - { - Debug.Assert(false, $"Unexpected exception: {ex}."); - } - } - - private static Task CancellationTokenAsTask(CancellationToken token) - { - if (token.IsCancellationRequested) - { - return Task.CompletedTask; - } - - var tcs = new TaskCompletionSource(); - token.Register(() => tcs.SetResult(null)); - return tcs.Task; - } - public async ValueTask DisposeAsync() { // The concrete WebHost implements IAsyncDisposable diff --git a/src/Servers/Kestrel/test/Libuv.FunctionalTests/TransportSelector.cs b/src/Servers/Kestrel/test/Libuv.FunctionalTests/TransportSelector.cs index ca209ba6e7..4bcae2a6cf 100644 --- a/src/Servers/Kestrel/test/Libuv.FunctionalTests/TransportSelector.cs +++ b/src/Servers/Kestrel/test/Libuv.FunctionalTests/TransportSelector.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,9 +9,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests { public static class TransportSelector { - public static IWebHostBuilder GetWebHostBuilder(Func> memoryPoolFactory = null) + public static IWebHostBuilder GetWebHostBuilder(Func> memoryPoolFactory = null, + long? maxReadBufferSize = null) { - return new WebHostBuilder().UseLibuv(options => { options.MemoryPoolFactory = memoryPoolFactory ?? options.MemoryPoolFactory; }); + return new WebHostBuilder().UseLibuv(options => + { + options.MemoryPoolFactory = memoryPoolFactory ?? options.MemoryPoolFactory; + options.MaxReadBufferSize = maxReadBufferSize; + }); } } } diff --git a/src/Servers/Kestrel/test/Sockets.FunctionalTests/TransportSelector.cs b/src/Servers/Kestrel/test/Sockets.FunctionalTests/TransportSelector.cs index 3e3cfe3f6c..6d2461866e 100644 --- a/src/Servers/Kestrel/test/Sockets.FunctionalTests/TransportSelector.cs +++ b/src/Servers/Kestrel/test/Sockets.FunctionalTests/TransportSelector.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,9 +9,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests { public static class TransportSelector { - public static IWebHostBuilder GetWebHostBuilder(Func> memoryPoolFactory = null) + public static IWebHostBuilder GetWebHostBuilder(Func> memoryPoolFactory = null, + long? maxReadBufferSize = null) { - return new WebHostBuilder().UseSockets(options => { options.MemoryPoolFactory = memoryPoolFactory ?? options.MemoryPoolFactory; }); + return new WebHostBuilder().UseSockets(options => + { + options.MemoryPoolFactory = memoryPoolFactory ?? options.MemoryPoolFactory; + options.MaxReadBufferSize = maxReadBufferSize; + }); } } } diff --git a/src/Servers/Kestrel/tools/CodeGenerator/CodeGenerator.csproj b/src/Servers/Kestrel/tools/CodeGenerator/CodeGenerator.csproj index afb106a5cf..90f9bb3627 100644 --- a/src/Servers/Kestrel/tools/CodeGenerator/CodeGenerator.csproj +++ b/src/Servers/Kestrel/tools/CodeGenerator/CodeGenerator.csproj @@ -18,7 +18,7 @@ $(MSBuildThisFileDirectory)..\..\ - Core/src/Internal/Http/HttpHeaders.Generated.cs Core/src/Internal/Http/HttpProtocol.Generated.cs Core/src/Internal/Infrastructure/HttpUtilities.Generated.cs Transport.Abstractions/src/Internal/TransportConnection.Generated.cs + Core/src/Internal/Http/HttpHeaders.Generated.cs Core/src/Internal/Http/HttpProtocol.Generated.cs Core/src/Internal/Infrastructure/HttpUtilities.Generated.cs ../Connections.Abstractions/src/TransportConnection.Generated.cs diff --git a/src/Servers/Kestrel/tools/CodeGenerator/TransportConnectionFeatureCollection.cs b/src/Servers/Kestrel/tools/CodeGenerator/TransportConnectionFeatureCollection.cs index 93d0339a53..a9eabbe474 100644 --- a/src/Servers/Kestrel/tools/CodeGenerator/TransportConnectionFeatureCollection.cs +++ b/src/Servers/Kestrel/tools/CodeGenerator/TransportConnectionFeatureCollection.cs @@ -11,17 +11,11 @@ namespace CodeGenerator // See also: src/Kestrel.Transport.Abstractions/Internal/TransportConnection.FeatureCollection.cs var features = new[] { - "IHttpConnectionFeature", "IConnectionIdFeature", "IConnectionTransportFeature", "IConnectionItemsFeature", "IMemoryPoolFeature", - "IApplicationTransportFeature", - "ITransportSchedulerFeature", - "IConnectionLifetimeFeature", - "IConnectionHeartbeatFeature", - "IConnectionLifetimeNotificationFeature", - "IConnectionCompleteFeature" + "IConnectionLifetimeFeature" }; var usings = $@" @@ -29,7 +23,7 @@ using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Http.Features;"; return FeatureCollectionGenerator.GenerateFile( - namespaceName: "Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal", + namespaceName: "Microsoft.AspNetCore.Connections", className: "TransportConnection", allFeatures: features, implementedFeatures: features, diff --git a/src/Servers/Kestrel/Transport.Abstractions/src/Internal/KestrelMemoryPool.cs b/src/Shared/Buffers.MemoryPool/MemoryPoolFactory.cs similarity index 69% rename from src/Servers/Kestrel/Transport.Abstractions/src/Internal/KestrelMemoryPool.cs rename to src/Shared/Buffers.MemoryPool/MemoryPoolFactory.cs index b3577f2c0a..c7ee26ca25 100644 --- a/src/Servers/Kestrel/Transport.Abstractions/src/Internal/KestrelMemoryPool.cs +++ b/src/Shared/Buffers.MemoryPool/MemoryPoolFactory.cs @@ -1,12 +1,9 @@ // 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; - -namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal +namespace System.Buffers { - public static class KestrelMemoryPool + internal static class MemoryPoolFactory { public static MemoryPool Create() { @@ -21,7 +18,5 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal { return new SlabMemoryPool(); } - - public static readonly int MinimumSegmentSize = 4096; } } diff --git a/src/Shared/Buffers.MemoryPool/SlabMemoryPool.cs b/src/Shared/Buffers.MemoryPool/SlabMemoryPool.cs index 0359c72193..e808624010 100644 --- a/src/Shared/Buffers.MemoryPool/SlabMemoryPool.cs +++ b/src/Shared/Buffers.MemoryPool/SlabMemoryPool.cs @@ -30,6 +30,11 @@ namespace System.Buffers /// public override int MaxBufferSize { get; } = _blockSize; + /// + /// The size of a block. 4096 is chosen because most operating systems use 4k pages. + /// + public static int BlockSize => _blockSize; + /// /// 4096 * 32 gives you a slabLength of 128k contiguous bytes allocated per slab /// diff --git a/src/SignalR/SignalR.sln b/src/SignalR/SignalR.sln index bfe46375f3..8a76052bfd 100644 --- a/src/SignalR/SignalR.sln +++ b/src/SignalR/SignalR.sln @@ -147,6 +147,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.HttpOv EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.SignalR.Protocols.Json", "common\Protocols.Json\src\Microsoft.AspNetCore.SignalR.Protocols.Json.csproj", "{BB52C0FB-19FD-485A-9EBD-3FC173ECAEA0}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Metadata", "..\Http\Metadata\src\Microsoft.AspNetCore.Metadata.csproj", "{2E107FBB-2387-4A9F-A2CA-EFDF2E4DD64D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -405,6 +407,10 @@ Global {BB52C0FB-19FD-485A-9EBD-3FC173ECAEA0}.Debug|Any CPU.Build.0 = Debug|Any CPU {BB52C0FB-19FD-485A-9EBD-3FC173ECAEA0}.Release|Any CPU.ActiveCfg = Release|Any CPU {BB52C0FB-19FD-485A-9EBD-3FC173ECAEA0}.Release|Any CPU.Build.0 = Release|Any CPU + {2E107FBB-2387-4A9F-A2CA-EFDF2E4DD64D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2E107FBB-2387-4A9F-A2CA-EFDF2E4DD64D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2E107FBB-2387-4A9F-A2CA-EFDF2E4DD64D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2E107FBB-2387-4A9F-A2CA-EFDF2E4DD64D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -475,6 +481,7 @@ Global {762A7DD1-E45E-4EA3-8109-521E844AE613} = {1C8016A8-F362-45C7-9EA9-A1CCE7918F2F} {FD3A8F8D-2967-4635-86FC-CC49BAF651C1} = {EDE8E45E-A5D0-4F0E-B72C-7CC14146C60A} {BB52C0FB-19FD-485A-9EBD-3FC173ECAEA0} = {9FCD621E-E710-4991-B45C-1BABC977BEEC} + {2E107FBB-2387-4A9F-A2CA-EFDF2E4DD64D} = {EDE8E45E-A5D0-4F0E-B72C-7CC14146C60A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7945A4E4-ACDB-4F6E-95CA-6AC6E7C2CD59} diff --git a/src/SignalR/clients/csharp/Client/src/HttpConnectionFactory.cs b/src/SignalR/clients/csharp/Client/src/HttpConnectionFactory.cs index 99da56fbb4..3ecf4dacd0 100644 --- a/src/SignalR/clients/csharp/Client/src/HttpConnectionFactory.cs +++ b/src/SignalR/clients/csharp/Client/src/HttpConnectionFactory.cs @@ -60,7 +60,7 @@ namespace Microsoft.AspNetCore.SignalR.Client /// public Task DisposeAsync(ConnectionContext connection) { - return ((HttpConnection)connection).DisposeAsync(); + return connection.DisposeAsync().AsTask(); } } -} \ No newline at end of file +} diff --git a/src/SignalR/clients/csharp/Client/test/FunctionalTests/HubConnectionTests.cs b/src/SignalR/clients/csharp/Client/test/FunctionalTests/HubConnectionTests.cs index 5a8d26f060..d723973629 100644 --- a/src/SignalR/clients/csharp/Client/test/FunctionalTests/HubConnectionTests.cs +++ b/src/SignalR/clients/csharp/Client/test/FunctionalTests/HubConnectionTests.cs @@ -59,7 +59,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests var delegateConnectionFactory = new DelegateConnectionFactory( GetHttpConnectionFactory(url, loggerFactory, path, transportType ?? HttpTransportType.LongPolling | HttpTransportType.WebSockets | HttpTransportType.ServerSentEvents), - connection => ((HttpConnection)connection).DisposeAsync()); + connection => ((HttpConnection)connection).DisposeAsync().AsTask()); hubConnectionBuilder.Services.AddSingleton(delegateConnectionFactory); return hubConnectionBuilder.Build(); diff --git a/src/SignalR/clients/csharp/Client/test/UnitTests/HubConnectionTests.ConnectionLifecycle.cs b/src/SignalR/clients/csharp/Client/test/UnitTests/HubConnectionTests.ConnectionLifecycle.cs index 647aa433ca..7e29231574 100644 --- a/src/SignalR/clients/csharp/Client/test/UnitTests/HubConnectionTests.ConnectionLifecycle.cs +++ b/src/SignalR/clients/csharp/Client/test/UnitTests/HubConnectionTests.ConnectionLifecycle.cs @@ -86,7 +86,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests Task DisposeAsync(ConnectionContext connection) { - return ((TestConnection)connection).DisposeAsync(); + return connection.DisposeAsync().AsTask(); } var builder = new HubConnectionBuilder(); @@ -122,7 +122,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests return new TestConnection(onDispose: createCount == 1 ? onDisposeForFirstConnection : null).StartAsync(format); } - Task DisposeAsync(ConnectionContext connection) => ((TestConnection)connection).DisposeAsync(); + Task DisposeAsync(ConnectionContext connection) => connection.DisposeAsync().AsTask(); var builder = new HubConnectionBuilder(); var delegateConnectionFactory = new DelegateConnectionFactory(ConnectionFactory, DisposeAsync); @@ -601,7 +601,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests var delegateConnectionFactory = new DelegateConnectionFactory( format => innerConnection.StartAsync(format), - connection => ((TestConnection)connection).DisposeAsync()); + connection => connection.DisposeAsync().AsTask()); builder.Services.AddSingleton(delegateConnectionFactory); var hubConnection = builder.Build(); diff --git a/src/SignalR/clients/csharp/Client/test/UnitTests/HubConnectionTests.Helpers.cs b/src/SignalR/clients/csharp/Client/test/UnitTests/HubConnectionTests.Helpers.cs index 0ec53e6479..96c18db8d0 100644 --- a/src/SignalR/clients/csharp/Client/test/UnitTests/HubConnectionTests.Helpers.cs +++ b/src/SignalR/clients/csharp/Client/test/UnitTests/HubConnectionTests.Helpers.cs @@ -16,7 +16,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests var delegateConnectionFactory = new DelegateConnectionFactory( connection.StartAsync, - c => ((TestConnection)c).DisposeAsync()); + c => c.DisposeAsync().AsTask()); builder.Services.AddSingleton(delegateConnectionFactory); diff --git a/src/SignalR/clients/csharp/Client/test/UnitTests/HubConnectionTests.cs b/src/SignalR/clients/csharp/Client/test/UnitTests/HubConnectionTests.cs index 40cca161c0..74a2da0c7f 100644 --- a/src/SignalR/clients/csharp/Client/test/UnitTests/HubConnectionTests.cs +++ b/src/SignalR/clients/csharp/Client/test/UnitTests/HubConnectionTests.cs @@ -63,7 +63,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests var delegateConnectionFactory = new DelegateConnectionFactory( format => new TestConnection().StartAsync(format), - connection => ((TestConnection)connection).DisposeAsync()); + connection => ((TestConnection)connection).DisposeAsync().AsTask()); builder.Services.AddSingleton(delegateConnectionFactory); var hubConnection = builder.Build(); diff --git a/src/SignalR/clients/csharp/Client/test/UnitTests/TestConnection.cs b/src/SignalR/clients/csharp/Client/test/UnitTests/TestConnection.cs index dc9e1b9d56..cdb61db868 100644 --- a/src/SignalR/clients/csharp/Client/test/UnitTests/TestConnection.cs +++ b/src/SignalR/clients/csharp/Client/test/UnitTests/TestConnection.cs @@ -60,7 +60,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests null); } - public Task DisposeAsync() => DisposeCoreAsync(); + public override ValueTask DisposeAsync() => DisposeCoreAsync(); public async Task StartAsync(TransferFormat transferFormat = TransferFormat.Binary) { @@ -195,7 +195,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests Application.Output.Complete(ex); } - private async Task DisposeCoreAsync(Exception ex = null) + private async ValueTask DisposeCoreAsync(Exception ex = null) { Interlocked.Increment(ref _disposeCount); _disposed.TrySetResult(null); diff --git a/src/SignalR/clients/csharp/Http.Connections.Client/ref/Microsoft.AspNetCore.Http.Connections.Client.netcoreapp3.0.cs b/src/SignalR/clients/csharp/Http.Connections.Client/ref/Microsoft.AspNetCore.Http.Connections.Client.netcoreapp3.0.cs index dd78ecdf90..3c74a6e8c1 100644 --- a/src/SignalR/clients/csharp/Http.Connections.Client/ref/Microsoft.AspNetCore.Http.Connections.Client.netcoreapp3.0.cs +++ b/src/SignalR/clients/csharp/Http.Connections.Client/ref/Microsoft.AspNetCore.Http.Connections.Client.netcoreapp3.0.cs @@ -15,7 +15,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Client bool Microsoft.AspNetCore.Connections.Features.IConnectionInherentKeepAliveFeature.HasInherentKeepAlive { get { throw null; } } public override System.IO.Pipelines.IDuplexPipe Transport { get { throw null; } set { } } [System.Diagnostics.DebuggerStepThroughAttribute] - public System.Threading.Tasks.Task DisposeAsync() { throw null; } + public override System.Threading.Tasks.ValueTask DisposeAsync() { throw null; } [System.Diagnostics.DebuggerStepThroughAttribute] public System.Threading.Tasks.Task StartAsync(Microsoft.AspNetCore.Connections.TransferFormat transferFormat, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public System.Threading.Tasks.Task StartAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } diff --git a/src/SignalR/clients/csharp/Http.Connections.Client/ref/Microsoft.AspNetCore.Http.Connections.Client.netstandard2.0.cs b/src/SignalR/clients/csharp/Http.Connections.Client/ref/Microsoft.AspNetCore.Http.Connections.Client.netstandard2.0.cs index dd78ecdf90..3c74a6e8c1 100644 --- a/src/SignalR/clients/csharp/Http.Connections.Client/ref/Microsoft.AspNetCore.Http.Connections.Client.netstandard2.0.cs +++ b/src/SignalR/clients/csharp/Http.Connections.Client/ref/Microsoft.AspNetCore.Http.Connections.Client.netstandard2.0.cs @@ -15,7 +15,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Client bool Microsoft.AspNetCore.Connections.Features.IConnectionInherentKeepAliveFeature.HasInherentKeepAlive { get { throw null; } } public override System.IO.Pipelines.IDuplexPipe Transport { get { throw null; } set { } } [System.Diagnostics.DebuggerStepThroughAttribute] - public System.Threading.Tasks.Task DisposeAsync() { throw null; } + public override System.Threading.Tasks.ValueTask DisposeAsync() { throw null; } [System.Diagnostics.DebuggerStepThroughAttribute] public System.Threading.Tasks.Task StartAsync(Microsoft.AspNetCore.Connections.TransferFormat transferFormat, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public System.Threading.Tasks.Task StartAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } diff --git a/src/SignalR/clients/csharp/Http.Connections.Client/src/HttpConnection.cs b/src/SignalR/clients/csharp/Http.Connections.Client/src/HttpConnection.cs index fdbecdef64..4953e82c92 100644 --- a/src/SignalR/clients/csharp/Http.Connections.Client/src/HttpConnection.cs +++ b/src/SignalR/clients/csharp/Http.Connections.Client/src/HttpConnection.cs @@ -234,7 +234,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Client /// A connection cannot be restarted after it has stopped. To restart a connection /// a new instance should be created using the same options. /// - public async Task DisposeAsync() + public override async ValueTask DisposeAsync() { using (_logger.BeginScope(_logScope)) { diff --git a/src/SignalR/common/testassets/Tests.Utils/TaskExtensions.cs b/src/SignalR/common/testassets/Tests.Utils/TaskExtensions.cs index 5aa1675012..2e0a5215c3 100644 --- a/src/SignalR/common/testassets/Tests.Utils/TaskExtensions.cs +++ b/src/SignalR/common/testassets/Tests.Utils/TaskExtensions.cs @@ -17,6 +17,16 @@ namespace System.Threading.Tasks { private const int DefaultTimeout = 30 * 1000; + public static Task OrTimeout(this ValueTask task, int milliseconds = DefaultTimeout, [CallerMemberName] string memberName = null, [CallerFilePath] string filePath = null, [CallerLineNumber] int? lineNumber = null) + { + return OrTimeout(task, new TimeSpan(0, 0, 0, 0, milliseconds), memberName, filePath, lineNumber); + } + + public static Task OrTimeout(this ValueTask task, TimeSpan timeout, [CallerMemberName] string memberName = null, [CallerFilePath] string filePath = null, [CallerLineNumber] int? lineNumber = null) + { + return task.AsTask().TimeoutAfter(timeout, filePath, lineNumber ?? 0); + } + public static Task OrTimeout(this Task task, int milliseconds = DefaultTimeout, [CallerMemberName] string memberName = null, [CallerFilePath] string filePath = null, [CallerLineNumber] int? lineNumber = null) { return OrTimeout(task, new TimeSpan(0, 0, 0, 0, milliseconds), memberName, filePath, lineNumber); diff --git a/src/SignalR/samples/ClientSample/Tcp/TcpConnection.cs b/src/SignalR/samples/ClientSample/Tcp/TcpConnection.cs index 1fbb213b9b..d2802b2e0c 100644 --- a/src/SignalR/samples/ClientSample/Tcp/TcpConnection.cs +++ b/src/SignalR/samples/ClientSample/Tcp/TcpConnection.cs @@ -43,14 +43,14 @@ namespace ClientSample // We claim to have inherent keep-alive so the client doesn't kill the connection when it hasn't seen ping frames. public bool HasInherentKeepAlive { get; } = true; - public Task DisposeAsync() + public override ValueTask DisposeAsync() { Transport?.Output.Complete(); Transport?.Input.Complete(); _socket?.Dispose(); - return Task.CompletedTask; + return default; } public async Task StartAsync() diff --git a/src/SignalR/samples/ClientSample/Tcp/TcpHubConnectionBuilderExtensions.cs b/src/SignalR/samples/ClientSample/Tcp/TcpHubConnectionBuilderExtensions.cs index 763725cb69..a52b18600e 100644 --- a/src/SignalR/samples/ClientSample/Tcp/TcpHubConnectionBuilderExtensions.cs +++ b/src/SignalR/samples/ClientSample/Tcp/TcpHubConnectionBuilderExtensions.cs @@ -54,7 +54,7 @@ namespace Microsoft.AspNetCore.SignalR.Client public Task DisposeAsync(ConnectionContext connection) { - return ((TcpConnection)connection).DisposeAsync(); + return connection.DisposeAsync().AsTask(); } } }