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 1189ad64cf5c..bb446f937d4b 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 @@ public static partial class ConnectionBuilderExtensions 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 @@ public partial class ConnectionResetException : System.IO.IOException 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 @@ public partial interface IConnectionBuilder 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 @@ public partial interface IConnectionCompleteFeature { 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 a709a5f89195..cecf3513c793 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 @@ public abstract class ConnectionContext 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 @@ public virtual void Abort(ConnectionAbortedException abortReason) } 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 f0d64d158724..dff0384f6018 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 fab7c929e24f..81a56478ba79 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 @@ public class DefaultConnectionContext : ConnectionContext, IConnectionItemsFeature, IConnectionTransportFeature, IConnectionUserFeature, - IConnectionLifetimeFeature + IConnectionLifetimeFeature, + IConnectionEndPointFeature { private CancellationTokenSource _connectionClosedTokenSource = new CancellationTokenSource(); @@ -42,6 +45,7 @@ public DefaultConnectionContext(string id) Features.Set(this); Features.Set(this); Features.Set(this); + Features.Set(this); } public DefaultConnectionContext(string id, IDuplexPipe transport, IDuplexPipe application) @@ -63,7 +67,9 @@ public DefaultConnectionContext(string id, IDuplexPipe transport, IDuplexPipe ap 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 @@ public void Dispose() { _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 000000000000..7c44146ede79 --- /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 3efb362fc79d..7698693a54e0 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 @@ public interface IConnectionUserFeature { 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 000000000000..41f4d50812f1 --- /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 bb70e4ec344a..f16935e044f9 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 482574829287..5fe3ec25a0fe 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 000000000000..c9d0564447ce --- /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 000000000000..b28724e1dd33 --- /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 000000000000..fc5443ecfc64 --- /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 b5d0122ffb03..eb6f2ba253e7 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 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 @@ public partial class TransportConnection : IFeatureCollection 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 @@ object IFeatureCollection.this[Type key] get { object feature = null; - if (key == IHttpConnectionFeatureType) - { - feature = _currentIHttpConnectionFeature; - } - else if (key == IConnectionIdFeatureType) + if (key == IConnectionIdFeatureType) { feature = _currentIConnectionIdFeature; } @@ -128,30 +106,10 @@ object IFeatureCollection.this[Type key] { 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 @@ object IFeatureCollection.this[Type key] { _featureRevision++; - if (key == IHttpConnectionFeatureType) - { - _currentIHttpConnectionFeature = value; - } - else if (key == IConnectionIdFeatureType) + if (key == IConnectionIdFeatureType) { _currentIConnectionIdFeature = value; } @@ -184,30 +138,10 @@ object IFeatureCollection.this[Type key] { _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 @@ object IFeatureCollection.this[Type key] 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 @@ TFeature IFeatureCollection.Get() { 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 @@ TFeature IFeatureCollection.Get() 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 @@ void IFeatureCollection.Set(TFeature feature) { _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 @@ void IFeatureCollection.Set(TFeature feature) 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 @@ private IEnumerable> FastEnumerable() { 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 000000000000..4cef0363c988 --- /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 a7e5f1b75a17..367e32ffec62 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 1c23c2dc9874..8891d7feb782 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 @@ public enum HttpProtocols } 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 @@ public partial class KestrelServerOptions 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 @@ public void ListenLocalhost(int port, 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 8d9a35b45639..8043ca28fe56 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.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 @@ private async Task ReadInputAsync(Stream stream) 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 e2319b4977c7..555dc4af1df9 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 @@ internal override async Task BindAsync(AddressBindContext context) 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 0c0cca15732d..4e354d811358 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 @@ public ConnectionDispatcher(ServiceContext serviceContext, ConnectionDelegate co 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); + ThreadPool.UnsafeQueueUserWorkItem(StartAcceptingConnectionsCore, listener, preferLocal: false); + return _acceptLoopTcs.Task; + } - var pair = DuplexPipe.CreateConnectionPair(inputOptions, outputOptions); + private void StartAcceptingConnectionsCore(IConnectionListener listener) + { + // REVIEW: Multiple accept loops in parallel? + _ = AcceptConnectionsAsync(); - // Set the transport and connection id - connection.ConnectionId = CorrelationIdGenerator.GetNextId(); - connection.Transport = pair.Transport; + async Task AcceptConnectionsAsync() + { + try + { + while (true) + { + var connection = await listener.AcceptAsync(); - // This *must* be set before returning from OnConnection - connection.Application = pair.Application; + if (connection == null) + { + // We're done listening + break; + } - return Execute(new KestrelConnection(connection)); + _ = 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); + } + } } - private async Task Execute(KestrelConnection connection) + internal async Task Execute(KestrelConnection connection) { var id = Interlocked.Increment(ref _lastConnectionId); var connectionContext = connection.TransportConnection; @@ -69,25 +88,19 @@ private async Task Execute(KestrelConnection connection) { 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 @@ private IDisposable BeginConnectionScope(ConnectionContext connectionContext) 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 7c1327cd73ae..db9e78aa2f8d 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.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 @@ private Pipe CreateRequestBodyPipe(Http1Connection context) 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 11fcadc07413..27b385888cb1 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.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 @@ Stream IHttpRequestFeature.Body { 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 @@ Stream IHttpResponseFeature.Body 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 fec48783efce..4f481850d703 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.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 @@ private static Pipe CreateDataPipe(MemoryPool pool) 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 5a27a5641f75..d9407a831895 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.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 @@ private Pipe CreateRequestBodyPipe(uint windowSize) 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 2bb90b1ae0ad..2d7b3c76337b 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.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 @@ public HttpConnection(HttpConnectionContext context) 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 @@ public HttpConnection(HttpConnectionContext context) 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 @@ public async Task ProcessRequestsAsync(IHttpApplication http 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 76b2f3c8e0ce..efae39ce71bb 100644 --- a/src/Servers/Kestrel/Core/src/Internal/HttpConnectionMiddleware.cs +++ b/src/Servers/Kestrel/Core/src/Internal/HttpConnectionMiddleware.cs @@ -47,20 +47,8 @@ public Task OnConnectionAsync(ConnectionContext connectionContext) 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 cb402facea81..05bb0f0726a5 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 @@ public void AddConnection(long id, KestrelConnection connection) 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 @@ public void Walk(Action callback) } } + 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 5877cbcf5572..000000000000 --- 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 8fcd25fe9830..18635e1f27b0 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 @@ public void OnHeartbeat(DateTimeOffset now) 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 a253aa5c328d..ccb51230c54c 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) + { + 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; + } + + private ILogger Logger { get; } + + public ConnectionContext TransportConnection { get; set; } + public CancellationToken ConnectionClosedRequested { get; set; } + public Task ExecutionTask => _completionTcs.Task; + + public void TickHeartbeat() + { + lock (_heartbeatLock) + { + if (_heartbeatHandlers == null) + { + return; + } + + foreach (var (handler, state) in _heartbeatHandlers) + { + handler(state); + } + } + } + + public void OnHeartbeat(Action action, object state) { - TransportConnection = transportConnection; - ExecutionTask = _executionTcs.Task; + lock (_heartbeatLock) + { + if (_heartbeatHandlers == null) + { + _heartbeatHandlers = new List<(Action handler, object state)>(); + } + + _heartbeatHandlers.Add((action, state)); + } } - public TransportConnection TransportConnection { get; } + 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."); + } - public Task ExecutionTask { get; } + _completed = true; + var onCompleted = _onCompleted; - internal void Complete() => _executionTcs.TrySetResult(null); + 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 ae6a03915b9c..fdabf48247f6 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 @@ private KestrelEventSource() // - 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 @@ private void ConnectionStart(string connectionId, } [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 000000000000..d3b1f30a0263 --- /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 51b444ada232..0481df8fad71 100644 --- a/src/Servers/Kestrel/Core/src/KestrelServer.cs +++ b/src/Servers/Kestrel/Core/src/KestrelServer.cs @@ -6,13 +6,13 @@ 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 @@ private static ServiceContext CreateServiceContext(IOptions 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 @@ public async Task StartAsync(IHttpApplication application, C 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 @@ async Task OnBind(ListenOptions endpoint) } 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 @@ public async Task StopAsync(CancellationToken cancellationToken) 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 @@ public async Task StopAsync(CancellationToken cancellationToken) 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 bbe5d30b9a34..01a94fb0e702 100644 --- a/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs +++ b/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs @@ -10,7 +10,6 @@ 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 @@ public class KestrelServerOptions /// 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 b3e4ec21ced0..3a850c25f9f4 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 @@ internal ListenOptions(ulong fileHandle) 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 @@ public FileHandleType HandleType /// 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 @@ internal virtual string GetDisplayName() ? "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 80e008c78be8..c7e5f47cadb5 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 @@ internal ListenOptions Clone(IPAddress address) { 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 a528049cf616..816133461348 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 74b067cae3a2..76d517220710 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 @@ public void DoesNotCreateIPEndPointOnInvalidIPAddress(string address) [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 @@ public void ParseAddressDefaultsToAnyIPOnInvalidIPAddress(string host) [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 @@ public void ParseAddressUnixPipe() [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 1d1c3b46e3bc..5302cccba714 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 @@ public void OnConnectionCreatesLogScopeWithConnectionId() 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 @@ public void OnConnectionCreatesLogScopeWithConnectionId() } [Fact] - public async Task OnConnectionCompletesTransportPipesAfterReturning() + public async Task StartAcceptingConnectionsAsyncLogsIfAcceptAsyncThrows() { var serviceContext = new TestServiceContext(); - var dispatcher = new ConnectionDispatcher(serviceContext, _ => Task.CompletedTask); + var logger = ((TestKestrelTrace)serviceContext.Log).Logger; + logger.ThrowOnCriticalErrors = false; - 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; + var dispatcher = new ConnectionDispatcher(serviceContext, _ => Task.CompletedTask); - await dispatcher.OnConnection(connection); + await dispatcher.StartAcceptingConnections(new ThrowingListener()); - 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 @@ public async Task OnConnectionFiresOnCompleted() 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 @@ public async Task OnConnectionOnCompletedExceptionCaught() 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 494e4531e6c6..042a136b3e5f 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.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 @@ public class Http1ConnectionTests : IDisposable 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 809c719e5bb3..b7c7df6a1d9d 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 @@ private void UnrootedConnectionsGetRemovedFromHeartbeatInnerScope( 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 4aad2e3561fd..6a4e1c235221 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.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 @@ public class HttpResponseHeadersTests [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 02e52db7f0a5..c8243b6025ad 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 @@ public void ConfigureEndpointDefaultsAppliesToNewEndpoints() 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 180c7e9bf8ca..8af4397809be 100644 --- a/src/Servers/Kestrel/Core/test/KestrelServerTests.cs +++ b/src/Servers/Kestrel/Core/test/KestrelServerTests.cs @@ -7,11 +7,11 @@ 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 @@ public void LoggerCategoryNameIsKestrelServerNamespace() 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 @@ public async Task StopAsyncCallsCompleteWhenFirstCallCompletes() 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 @@ public async Task StopAsyncCallsCompleteWhenFirstCallCompletes() 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 @@ public async Task StopAsyncCallsCompleteWhenFirstCallCompletes() 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 @@ public async Task StopAsyncCallsCompleteWithThrownException() 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 () => + var mockTransport = new Mock(); + var mockTransportFactory = new Mock(); + mockTransportFactory + .Setup(transportFactory => transportFactory.BindAsync(It.IsAny(), It.IsAny())) + .Returns((e, token) => { - await unbind.WaitAsync(); - throw unbindException; + 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); }); - mockTransport - .Setup(transport => transport.StopAsync()) - .Returns(Task.CompletedTask); - - var mockTransportFactory = new Mock(); - mockTransportFactory - .Setup(transportFactory => transportFactory.Create(It.IsAny(), It.IsAny())) - .Returns(mockTransport.Object); var mockLoggerFactory = new Mock(); var mockLogger = new Mock(); @@ -312,9 +318,9 @@ public async Task StopAsyncCallsCompleteWithThrownException() 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 @@ public async Task StopAsyncCallsCompleteWithThrownException() 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 @@ public async Task StopAsyncDispatchesSubsequentStopAsyncContinuations() 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 @@ public async Task StopAsyncDispatchesSubsequentStopAsyncContinuations() 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 @@ private static void StartDummyApplication(IServer server) 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 b334a87ed9db..fa82cf0beb3c 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 749e2473a561..53aa300f1cd6 100644 --- a/src/Servers/Kestrel/Core/test/OutputProducerTests.cs +++ b/src/Servers/Kestrel/Core/test/OutputProducerTests.cs @@ -9,7 +9,6 @@ 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 @@ public class OutputProducerTests : IDisposable 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 1f5b3773608d..0b06bec0e9eb 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 1e5f82677d72..e2dfc2efd472 100644 --- a/src/Servers/Kestrel/Core/test/PipelineExtensionTests.cs +++ b/src/Servers/Kestrel/Core/test/PipelineExtensionTests.cs @@ -6,7 +6,6 @@ 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 @@ public class PipelineExtensionTests : IDisposable 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 9b9656fbba10..bd4d5f4d5b5b 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.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 @@ public void AuthorityForms(string rawTarget, string path, string query) 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 81f046b10dd7..3a8ce70e67ee 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.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 @@ class TestInput : IDisposable 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 62411b168d44..361667936996 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 @@ public static IWebHostBuilder UseKestrel(this IWebHostBuilder hostBuilder) 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 c2bb4938459c..12d32431ae13 100644 --- a/src/Servers/Kestrel/Kestrel/test/KestrelConfigurationBuilderTests.cs +++ b/src/Servers/Kestrel/Kestrel/test/KestrelConfigurationBuilderTests.cs @@ -133,7 +133,6 @@ public void ConfigureDefaultsAppliesToNewConfigureEndpoints() serverOptions.ConfigureEndpointDefaults(opt => { - opt.NoDelay = false; opt.Protocols = HttpProtocols.Http2; }); @@ -156,13 +155,11 @@ public void ConfigureDefaultsAppliesToNewConfigureEndpoints() 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 @@ public void ConfigureEndpointDefaultCanEnableHttps() serverOptions.ConfigureEndpointDefaults(opt => { - opt.NoDelay = false; opt.UseHttps(TestResources.GetTestCertificate()); }); @@ -202,12 +198,10 @@ public void ConfigureEndpointDefaultCanEnableHttps() 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 814c905495ec..cb32fe57c11a 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 7ea16d4c858e..fd5c31d032d0 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 @@ public void SocketTransportIsTheDefault() .UseKestrel() .Configure(app => { }); - Assert.IsType(hostBuilder.Build().Services.GetService()); + Assert.IsType(hostBuilder.Build().Services.GetService()); } [Fact] @@ -68,14 +68,14 @@ public void LibuvTransportCanBeManuallySelectedIndependentOfOrder() .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 @@ public void SocketsTransportCanBeManuallySelectedIndependentOfOrder() .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 5420d07eacf4..618082bc4a8a 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 8aa8328a6b2a..000000000000 --- 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 813c541d1a56..000000000000 --- 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 1b7abfa4975e..000000000000 --- 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 5a6dc0c20c81..000000000000 --- 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 4037467e87a5..000000000000 --- 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 c4df6d5a3799..000000000000 --- 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 3616f1967ee5..000000000000 --- 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 881006087c71..000000000000 --- 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 7f98162507c8..000000000000 --- 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 e3d012cc27f1..000000000000 --- 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 9f9270d232ba..a20f6d2640b3 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 381c885bdd0f..366ac6ec5c68 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 8e37f8714db2..f24337e79d12 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 adcc5cd09d23..962359bd8f43 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.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 @@ internal partial class LibuvConnection : TransportConnection, IDisposable private volatile ConnectionAbortedException _abortReason; private MemoryHandle _bufferHandle; - - public LibuvConnection(UvStreamHandle socket, ILibuvTrace log, LibuvThread thread, IPEndPoint remoteEndPoint, IPEndPoint localEndPoint) + private Task _processingTask; + + 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 @@ public async Task Start() } 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 @@ public async Task Start() // 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 @@ public async Task Start() public override void Abort(ConnectionAbortedException abortReason) { _abortReason = abortReason; - + // Cancel WriteOutputAsync loop after setting _abortReason. Output.CancelPendingRead(); @@ -131,11 +169,17 @@ public override void Abort(ConnectionAbortedException abortReason) 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 4bbeae88f13f..d1b2e9a8e765 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 @@ public LibuvTransport(LibuvFunctions uv, LibuvTransportContext context, IEndPoin 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 @@ await Task.WhenAll(Threads.Select(thread => thread.StopAsync(TimeSpan.FromSecond #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 @@ public async Task BindAsync() { 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 @@ public async Task BindAsync() 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 @@ public async Task BindAsync() } } - 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 9d50eb5f6778..de212ca89df7 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 @@ internal class LibuvThread : PipeScheduler // 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 @@ internal class LibuvThread : PipeScheduler 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 @@ public LibuvThread(LibuvTransport transport) #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 @@ public async Task StopAsync(TimeSpan timeout) 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 @@ public void Walk(Action callback) private void Walk(LibuvFunctions.uv_walk_cb callback, IntPtr arg) { - _transport.Libuv.walk( + _libuv.walk( _loop, callback, arg @@ -282,7 +285,7 @@ private void ThreadStart(object parameter) 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 7f9e1d62f900..973a0fec061b 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 @@ internal class LibuvTransportContext 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 485582355d45..689c473195a8 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 @@ public LibuvTransportFactory( 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 @@ public LibuvTransportFactory( }; } - 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 010a84e00660..9dc11a31a440 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 @@ public Listener(LibuvTransportContext transportContext) : base(transportContext) 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 @@ public Task StartAsync( /// 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 @@ private UvTcpHandle ListenTcp(bool useFileHandle) 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 @@ private UvPipeHandle ListenPipe(bool useFileHandle) 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 @@ private UvPipeHandle ListenPipe(bool useFileHandle) private UvStreamHandle ListenHandle() { - switch (EndPointInformation.HandleType) + var handleEndPoint = (FileHandleEndPoint)EndPoint; + + switch (handleEndPoint.FileHandleType) { case FileHandleType.Auto: break; @@ -130,7 +136,7 @@ private UvStreamHandle ListenHandle() 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 @@ private UvStreamHandle ListenHandle() } handle = ListenPipe(useFileHandle: true); - EndPointInformation.HandleType = FileHandleType.Pipe; + EndPoint = new FileHandleEndPoint(handleEndPoint.FileHandle, FileHandleType.Pipe); return handle; } @@ -186,9 +192,7 @@ private void OnConnection(UvStreamHandle listenSocket, int status) 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 @@ await Thread.PostAsync(listener => 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 e153565af80a..528ec5ca3b68 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 @@ public ListenerContext(LibuvTransportContext transportContext) 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 @@ protected async Task HandleConnectionAsync(UvStreamHandle socket) } } - var connection = new LibuvConnection(socket, TransportContext.Log, Thread, remoteEndPoint, localEndPoint); - var middlewareTask = TransportContext.ConnectionDispatcher.OnConnection(connection); - var transportTask = connection.Start(); - - await transportTask; - await middlewareTask; + var options = TransportContext.Options; + var connection = new LibuvConnection(socket, TransportContext.Log, Thread, remoteEndPoint, localEndPoint, InputOptions, OutputOptions, options.MaxReadBufferSize, options.MaxWriteBufferSize); + connection.Start(); - 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 @@ private UvTcpHandle AcceptTcp() try { socket.Init(Thread.Loop, Thread.QueueCloseHandle); - socket.NoDelay(EndPointInformation.NoDelay); + socket.NoDelay(TransportContext.Options.NoDelay); } catch { @@ -113,9 +156,16 @@ private UvPipeHandle AcceptPipe() 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 4ce0afab29be..acbc356294d3 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 @@ public ListenerPrimary(LibuvTransportContext transportContext) : base(transportC public async Task StartAsync( string pipeName, byte[] pipeMessage, - IEndPointInformation endPointInformation, + EndPoint endPoint, LibuvThread thread) { _pipeName = pipeName; @@ -59,7 +59,7 @@ public async Task StartAsync( 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 6597df28d77d..477d391f8897 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 @@ public ListenerSecondary(LibuvTransportContext transportContext) : base(transpor 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 @@ private void ReadStartCallback(UvStreamHandle handle, int status) { 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 @@ await Thread.PostAsync(listener => 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 a040dea87b79..9110b39587bd 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 @@ public class LibuvTransportOptions /// 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 2b52fe7f4627..d6bd6d4e450e 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 386d0b667950..8f0986d3341a 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 @@ public static IWebHostBuilder UseLibuv(this IWebHostBuilder hostBuilder) { 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 c284ec1584d4..d1ff9b380c4e 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 @@ public class LibuvConnectionTests [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 @@ await thread.PostAsync(_ => [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); - - // We don't set the output writer scheduler here since we want to run the callback inline - - mockConnectionDispatcher.OutputOptions = pool => new PipeOptions(pool: pool, readerScheduler: thread, writerScheduler: PipeScheduler.Inline, useSynchronizationContext: false); + useSynchronizationContext: false), + // 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) + }; - Task connectionTask = null; try { await thread.StartAsync(); @@ -85,28 +81,22 @@ public async Task ConnectionDoesNotResumeAfterSocketCloseIfBackpressureIsApplied // 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 @@ await thread.PostAsync(_ => } finally { - mockConnectionDispatcher.Input.Reader.Complete(); - mockConnectionDispatcher.Output.Writer.Complete(); - await thread.StopAsync(TimeSpan.FromSeconds(5)); } } @@ -124,29 +111,32 @@ await thread.PostAsync(_ => [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); - - mockConnectionDispatcher.OutputOptions = pool => new PipeOptions(pool: pool, readerScheduler: thread, writerScheduler: PipeScheduler.Inline, useSynchronizationContext: false); + useSynchronizationContext: false), + OutputOptions = new PipeOptions( + pool: thread.MemoryPool, + readerScheduler: thread, + writerScheduler: PipeScheduler.Inline, + useSynchronizationContext: false) + }; - Task connectionTask = null; try { await thread.StartAsync(); @@ -154,32 +144,28 @@ public async Task ConnectionDoesNotResumeAfterReadCallbackScheduledAndSocketClos // 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 @@ await thread.PostAsync(_ => 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 @@ await thread.PostAsync(_ => [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 d72e6485b50a..f0a9769357ed 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.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 @@ public class LibuvOutputConsumerTests : IDisposable 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 @@ private async Task WriteOutputAsync(LibuvOutputConsumer consumer, PipeReader out 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 69daacab351a..7bfd5280ef6b 100644 --- a/src/Servers/Kestrel/Transport.Libuv/test/LibuvThreadTests.cs +++ b/src/Servers/Kestrel/Transport.Libuv/test/LibuvThreadTests.cs @@ -14,11 +14,9 @@ public class LibuvThreadTests [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 abdd8e1c7a13..d18af3aa39bf 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(); + await using var transport = new LibuvConnectionListener(transportContext, new IPEndPoint(IPAddress.Loopback, 0)); - var transportContext = new TestLibuvTransportContext + await transport.BindAsync(); + var endpoint = (IPEndPoint)transport.EndPoint; + + async Task EchoServerAsync() { - ConnectionDispatcher = new ConnectionDispatcher(serviceContext, listenOptions.Build()) - }; + while (true) + { + await using var connection = await transport.AcceptAsync(); - var transport = new LibuvTransport(transportContext, listenOptions); + if (connection == null) + { + break; + } - await transport.BindAsync(); + 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(listenOptions.IPEndPoint.Port)) + using (var socket = TestConnection.CreateConnectedLoopbackSocket(endpoint.Port)) { - var data = "Hello World"; - socket.Send(Encoding.ASCII.GetBytes($"POST / HTTP/1.0\r\nContent-Length: 11\r\n\r\n{data}")); + 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.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(); - await transport.StopAsync(); + + var connection = await acceptTask.DefaultTimeout(); + + Assert.Null(connection); } [ConditionalTheory] @@ -106,13 +189,15 @@ public async Task OneToTenThreads(int threadCount) 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 @@ public async Task OneToTenThreads(int threadCount) await transport.UnbindAsync(); - if (!await serviceContext.ConnectionManager.CloseAllConnectionsAsync(default).ConfigureAwait(false)) + await acceptTask; + + if (!await serviceContext.ConnectionManager.CloseAllConnectionsAsync(default)) { - await serviceContext.ConnectionManager.AbortAllConnectionsAsync().ConfigureAwait(false); + await serviceContext.ConnectionManager.AbortAllConnectionsAsync(); } - - await transport.StopAsync(); } } } diff --git a/src/Servers/Kestrel/Transport.Libuv/test/ListenerPrimaryTests.cs b/src/Servers/Kestrel/Transport.Libuv/test/ListenerPrimaryTests.cs index b1a338bd56dd..b3ed1b94fe07 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 @@ public async Task ConnectionsGetRoundRobinedToSecondaryListeners() { 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 @@ public async Task ConnectionsGetRoundRobinedToSecondaryListeners() } // 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 @@ public async Task ConnectionsGetRoundRobinedToSecondaryListeners() 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 @@ public async Task NonListenerPipeConnectionsAreLoggedAndIgnored() 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 @@ public async Task NonListenerPipeConnectionsAreLoggedAndIgnored() } // 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 @@ public async Task NonListenerPipeConnectionsAreLoggedAndIgnored() 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 @@ public async Task PipeConnectionsWithWrongMessageAreLoggedAndIgnored() 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 @@ public async Task PipeConnectionsWithWrongMessageAreLoggedAndIgnored() 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; } - - if (allowed != null) + else { - Assert.Contains(response, allowed); + expected ??= listenerPrimary.AcceptAsync().AsTask(); + currentListener = listenerPrimary; } - await Task.Delay(retryDelay); - } + using var socket = await HttpClientSlim.GetSocket(address); - Assert.True(false, $"'{address}' failed to respond with '{expected}' in {maxRetries} retries."); - } + await using var connection = await expected.DefaultTimeout(); - private static Uri GetUri(ListenOptions options) - { - if (options.Type != ListenType.IPEndPoint) - { - throw new InvalidOperationException($"Could not determine a proper URI for options with Type {options.Type}"); + expected = null; } - - var scheme = options.ConnectionAdapters.Any(f => f.IsHttps) - ? "https" - : "http"; - - return new Uri($"{scheme}://{options.IPEndPoint}"); } - private class ConnectionBuilder : IConnectionBuilder + private static async Task> WaitForSecondaryListener(Uri address, ListenerContext listenerPrimary, ListenerContext listenerSecondary) { - private readonly List> _components = new List>(); + int maxRetries = 100; + int retryDelay = 100; - public IServiceProvider ApplicationServices { get; set; } + Task primary = null; + Task secondary = null; - public IConnectionBuilder Use(Func middleware) + for (var i = 0; i < maxRetries; i++) { - _components.Add(middleware); - return this; - } + primary ??= listenerPrimary.AcceptAsync().AsTask(); + secondary ??= listenerSecondary.AcceptAsync().AsTask(); - public ConnectionDelegate Build() - { - ConnectionDelegate app = context => + using var _ = await HttpClientSlim.GetSocket(address); + + var task = await Task.WhenAny(primary, secondary); + + if (task == secondary) { - return Task.CompletedTask; - }; + // Dispose this connection now that we know the seconary listener is working + await (await secondary).DisposeAsync(); - for (int i = _components.Count - 1; i >= 0; i--) + // Return the primary task (it should be incomplete), we do this so that we can + return primary; + } + else { - var component = _components[i]; - app = component(app); + // Dispose the connection + await (await primary).DisposeAsync(); + + primary = null; } - return app; + await Task.Delay(retryDelay); } + + Assert.True(false, $"'{address}' failed to get queued connection in secondary listener in {maxRetries} retries."); + return null; + } + + private static Uri GetUri(EndPoint endpoint) + { + 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 01e0c049d032..000000000000 --- 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 155c31b2ebcf..1bccf57d32e2 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 @@ public TestLibuvTransportContext() 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 d41703c771eb..d5f1350fec57 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 2b9b9b1b87f1..34c7f982bb60 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 @@ public static partial class WebHostBuilderSocketExtensions } 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 bfb467f94abb..1c867f3199d2 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.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 @@ internal sealed class SocketConnection : TransportConnection, IDisposable private readonly object _shutdownLock = new object(); private volatile bool _socketDisposed; private volatile Exception _shutdownReason; - - internal SocketConnection(Socket socket, MemoryPool memoryPool, PipeScheduler scheduler, ISocketsTrace trace) + private Task _processingTask; + + 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 @@ internal SocketConnection(Socket socket, MemoryPool memoryPool, PipeSchedu _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 PipeWriter Input => Application.Output; + + public PipeReader Output => Application.Input; + public override MemoryPool MemoryPool { get; } - public override PipeScheduler InputWriterScheduler => _scheduler; - public override PipeScheduler OutputReaderScheduler => _scheduler; - public async Task StartAsync() + public void Start() + { + _processingTask = StartAsync(); + } + + private async Task StartAsync() { try { @@ -83,7 +98,22 @@ public async Task StartAsync() _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 @@ public override void Abort(ConnectionAbortedException abortReason) } // 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 @@ private async Task DoSend() } 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 e5ffbb53b647..337685e9b776 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 000000000000..b51e22e61ea1 --- /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 a1b01d45bba7..000000000000 --- 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 f6ef805fbbde..1988897855f1 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 2dad91442311..2ec6c52fd076 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 @@ public class SocketTransportOptions /// 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 95d27e46db04..d073f91aa45d 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 @@ public static IWebHostBuilder UseSockets(this IWebHostBuilder hostBuilder) { 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 3b24e49cabec..c327d8d51f90 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.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 @@ public class ChunkWriterBenchmark [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 c404a16c2795..d7bdd0a7095d 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.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 @@ public class Http1ConnectionBenchmark [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 84ac57e630a3..8ffe0a0059e1 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.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 @@ public class Http1ConnectionParsingOverheadBenchmark [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 122c5b694c19..2faca73d6ab2 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.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 @@ public class Http1ReadingBenchmark [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 d0c1cf3370f7..94dc4e66c52d 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.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 @@ public class Http1WritingBenchmark [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 3f8bae879f79..7f948c5a4bb9 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.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 @@ public object GetViaGeneric_NotFound() 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 d5421ab448af..b3e4e13eff1a 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.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 @@ public void GlobalSetupPlaintext() .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 @@ public async Task PlaintextPipelined() 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 @@ public ITransport Create(IEndPointInformation endPointInformation, IConnectionDi 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 e801048b6769..93d77cab5f02 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 b37656faec82..03b3eb2f1da5 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 @@ public class PipeThroughputBenchmark [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 da9f0f7faf30..a6cba511786a 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.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 @@ public class RequestParsingBenchmark [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 530a0ec968ce..417c5a8bbe05 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.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 @@ private void Unknown(int count) [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 f1fc2223cfee..794f011ecc7b 100644 --- a/src/Servers/Kestrel/samples/Http2SampleApp/Program.cs +++ b/src/Servers/Kestrel/samples/Http2SampleApp/Program.cs @@ -7,7 +7,6 @@ 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 @@ public static void Main(string[] args) { 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 86fd68771b90..8b48f958a95a 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 98dc353f23d9..044e8b5dfed2 100644 --- a/src/Servers/Kestrel/samples/PlaintextApp/Startup.cs +++ b/src/Servers/Kestrel/samples/PlaintextApp/Startup.cs @@ -8,6 +8,7 @@ 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 @@ public void Configure(IApplicationBuilder app) }); } - public static Task Main(string[] args) + public static async Task Main(string[] args) { var host = new WebHostBuilder() .UseKestrel(options => @@ -42,7 +43,7 @@ public static Task Main(string[] args) .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 71f218d81603..82f505c51007 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.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 @@ public static Task Main(string[] args) 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 @@ public static Task Main(string[] args) .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 e91322a82795..56e6f3f980da 100644 --- a/src/Servers/Kestrel/samples/SystemdTestApp/Startup.cs +++ b/src/Servers/Kestrel/samples/SystemdTestApp/Startup.cs @@ -9,7 +9,6 @@ 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 @@ public static Task Main(string[] args) { 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 @@ public static Task Main(string[] args) 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 8e83a7a70e94..68010b090a16 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 7b6abf939df5..e5332963aa8d 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.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 @@ private void Initialize(ILoggerFactory loggerFactory, IKestrelTrace kestrelTrace 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 d3a33bf2cf1e..26c8746e3985 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.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 @@ public MemoryPool Create() { 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 @@ public Task WhenAllBlocksReturned(TimeSpan span) } } } -} \ 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 bbb6d6fe49de..1382a7017599 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.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 @@ public TestServer(RequestDelegate app, TestServiceContext context, Action { configureKestrel(options); @@ -86,7 +86,7 @@ public TestServer(RequestDelegate app, TestServiceContext context, Action 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 540690317b6e..039b148db018 100644 --- a/src/Servers/Kestrel/test/FunctionalTests/MaxRequestBufferSizeTests.cs +++ b/src/Servers/Kestrel/test/FunctionalTests/MaxRequestBufferSizeTests.cs @@ -289,7 +289,7 @@ private async Task StartWebHost(long? maxRequestBufferSize, 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 2f3f01a941c1..12ac5791ff6e 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/ChunkedRequestTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/ChunkedRequestTests.cs @@ -127,7 +127,7 @@ await connection.ReceiveEnd( [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 6751565db19c..424307ac26d3 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.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 @@ protected static IEnumerable> ReadRateRequestHeader 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 @@ protected void CreateConnection() // 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 @@ protected void AdvanceClock(TimeSpan timeSpan) _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 c7b344c2c8c9..c1c91075984c 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 422520f5b8db..34f3932129cd 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 @@ public async Task ConnectionClosedWhenKeepAliveTimeoutExpires() { using (var connection = server.CreateConnection()) { + await connection.TransportConnection.WaitForReadTask; + await connection.Send( "GET / HTTP/1.1", "Host:", @@ -59,6 +62,8 @@ public async Task ConnectionKeptAliveBetweenRequests() { using (var connection = server.CreateConnection()) { + await connection.TransportConnection.WaitForReadTask; + for (var i = 0; i < 10; i++) { await connection.Send( @@ -86,6 +91,8 @@ public async Task ConnectionNotTimedOutWhileRequestBeingSent() { using (var connection = server.CreateConnection()) { + await connection.TransportConnection.WaitForReadTask; + await connection.Send( "POST /consume HTTP/1.1", "Host:", @@ -126,6 +133,8 @@ private async Task ConnectionNotTimedOutWhileAppIsRunning() { using (var connection = server.CreateConnection()) { + await connection.TransportConnection.WaitForReadTask; + await connection.Send( "GET /longrunning HTTP/1.1", "Host:", @@ -164,6 +173,8 @@ private async Task ConnectionTimesOutWhenOpenedButNoRequestSent() { 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 @@ private async Task KeepAliveTimeoutDoesNotApplyToUpgradedConnections() { using (var connection = server.CreateConnection()) { + await connection.TransportConnection.WaitForReadTask; + await connection.Send( "GET /upgrade HTTP/1.1", "Host:", @@ -212,6 +225,8 @@ await connection.Receive( 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 65448bd6d231..e217ece3f0db 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestHeadersTimeoutTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestHeadersTimeoutTests.cs @@ -32,6 +32,8 @@ public async Task ConnectionAbortedWhenRequestHeadersNotReceivedInTime(string he { using (var connection = server.CreateConnection()) { + await connection.TransportConnection.WaitForReadTask; + await connection.Send( "GET / HTTP/1.1", headers); @@ -55,6 +57,8 @@ public async Task RequestHeadersTimeoutCanceledAfterHeadersReceived() { using (var connection = server.CreateConnection()) { + await connection.TransportConnection.WaitForReadTask; + await connection.Send( "POST / HTTP/1.1", "Host:", @@ -86,6 +90,8 @@ public async Task ConnectionAbortedWhenRequestLineNotReceivedInTime(string reque { 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 @@ public async Task TimeoutNotResetOnEachRequestLineCharacterReceived() { 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 95c1907c0a72..cc91efd9b27f 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.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 PipeReader Output => Application.Input; - public override PipeScheduler InputWriterScheduler => PipeScheduler.ThreadPool; - public override PipeScheduler OutputReaderScheduler => PipeScheduler.ThreadPool; + public Task WaitForReadTask { get; } + + public override MemoryPool MemoryPool { get; } public ConnectionAbortedException AbortReason { get; private set; } @@ -57,14 +68,141 @@ public void OnClosed() 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 eeb271a70602..6c5ff148821c 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) + { + _acceptQueue.Writer.TryWrite(connection); + } + + public async ValueTask AcceptAsync(CancellationToken cancellationToken = default) { - if (ConnectionDispatcher != null) + if (await _acceptQueue.Reader.WaitToReadAsync(cancellationToken)) { - throw new InvalidOperationException("InMemoryTransportFactory doesn't support creating multiple endpoints"); + while (_acceptQueue.Reader.TryRead(out var item)) + { + return item; + } } - ConnectionDispatcher = dispatcher; + return null; - return new NoopTransport(); } - public IConnectionDispatcher ConnectionDispatcher { get; private set; } + public ValueTask BindAsync(EndPoint endpoint, CancellationToken cancellationToken = default) + { + EndPoint = endpoint; + + return new ValueTask(this); + } - private class NoopTransport : ITransport + public ValueTask DisposeAsync() { - public Task BindAsync() - { - return Task.CompletedTask; - } + return UnbindAsync(default); + } - public Task StopAsync() - { - return Task.CompletedTask; - } + public ValueTask UnbindAsync(CancellationToken cancellationToken = default) + { + _acceptQueue.Writer.TryComplete(); - public Task UnbindAsync() - { - return Task.CompletedTask; - } + 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 9f3828f07b27..c7bab027d888 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 @@ public TestServer(RequestDelegate app, TestServiceContext context, Action(); - 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 ca209ba6e767..4bcae2a6cff4 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 3e3cfe3f6c74..6d2461866ef6 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 afb106a5cff8..90f9bb3627a5 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 93d0339a53d7..a9eabbe47435 100644 --- a/src/Servers/Kestrel/tools/CodeGenerator/TransportConnectionFeatureCollection.cs +++ b/src/Servers/Kestrel/tools/CodeGenerator/TransportConnectionFeatureCollection.cs @@ -11,17 +11,11 @@ public static string GenerateFile() // 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 @@ public static string GenerateFile() 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 b3577f2c0acf..c7ee26ca2508 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 @@ public static MemoryPool CreateSlabMemoryPool() { 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 0359c7219351..e80862401073 100644 --- a/src/Shared/Buffers.MemoryPool/SlabMemoryPool.cs +++ b/src/Shared/Buffers.MemoryPool/SlabMemoryPool.cs @@ -30,6 +30,11 @@ internal sealed class SlabMemoryPool : MemoryPool /// 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 bfe46375f32a..8a76052bfdb7 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 99da56fbb436..3ecf4dacd085 100644 --- a/src/SignalR/clients/csharp/Client/src/HttpConnectionFactory.cs +++ b/src/SignalR/clients/csharp/Client/src/HttpConnectionFactory.cs @@ -60,7 +60,7 @@ public async Task ConnectAsync(TransferFormat transferFormat, /// 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 5a8d26f060f1..d7239736292c 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 @@ private HubConnection CreateHubConnection( 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 f48742474bc4..e4d001b2050d 100644 --- a/src/SignalR/clients/csharp/Client/test/UnitTests/HubConnectionTests.ConnectionLifecycle.cs +++ b/src/SignalR/clients/csharp/Client/test/UnitTests/HubConnectionTests.ConnectionLifecycle.cs @@ -85,7 +85,7 @@ Task ConnectionFactory(TransferFormat format) Task DisposeAsync(ConnectionContext connection) { - return ((TestConnection)connection).DisposeAsync(); + return connection.DisposeAsync().AsTask(); } var builder = new HubConnectionBuilder(); @@ -121,7 +121,7 @@ Task ConnectionFactory(TransferFormat format) 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); @@ -600,7 +600,7 @@ public async Task HubConnectionClosesWithErrorIfTerminatedWithPartialMessage() 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 0ec53e6479bf..96c18db8d0e8 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 @@ private static HubConnection CreateHubConnection(TestConnection connection, IHub 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 40cca161c0c8..74a2da0c7f32 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 @@ public async Task ClosedEventRaisedWhenTheClientIsStopped() 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 dc9e1b9d563b..cdb61db868be 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 @@ public TestConnection(Func onStart = null, Func onDispose = null, bo null); } - public Task DisposeAsync() => DisposeCoreAsync(); + public override ValueTask DisposeAsync() => DisposeCoreAsync(); public async Task StartAsync(TransferFormat transferFormat = TransferFormat.Binary) { @@ -195,7 +195,7 @@ public void CompleteFromTransport(Exception ex = null) 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 dd78ecdf90eb..3c74a6e8c1fa 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 @@ public HttpConnection(System.Uri url, Microsoft.AspNetCore.Http.Connections.Http 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 dd78ecdf90eb..3c74a6e8c1fa 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 @@ public HttpConnection(System.Uri url, Microsoft.AspNetCore.Http.Connections.Http 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 fdbecdef6422..4953e82c92b2 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 @@ private async Task StartAsyncCore(TransferFormat transferFormat, CancellationTok /// 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 5aa16750125e..2e0a5215c3bb 100644 --- a/src/SignalR/common/testassets/Tests.Utils/TaskExtensions.cs +++ b/src/SignalR/common/testassets/Tests.Utils/TaskExtensions.cs @@ -17,6 +17,16 @@ static class TaskExtensions { 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 1fbb213b9b70..d2802b2e0c3a 100644 --- a/src/SignalR/samples/ClientSample/Tcp/TcpConnection.cs +++ b/src/SignalR/samples/ClientSample/Tcp/TcpConnection.cs @@ -43,14 +43,14 @@ public TcpConnection(EndPoint endPoint) // 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 763725cb69bf..a52b18600e68 100644 --- a/src/SignalR/samples/ClientSample/Tcp/TcpHubConnectionBuilderExtensions.cs +++ b/src/SignalR/samples/ClientSample/Tcp/TcpHubConnectionBuilderExtensions.cs @@ -54,7 +54,7 @@ public Task ConnectAsync(TransferFormat transferFormat, Cance public Task DisposeAsync(ConnectionContext connection) { - return ((TcpConnection)connection).DisposeAsync(); + return connection.DisposeAsync().AsTask(); } } }