Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/dotnet/aspnetcore.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJames Newton-King <james@newtonking.com>2022-07-12 12:11:20 +0300
committerJames Newton-King <james@newtonking.com>2022-07-21 04:22:30 +0300
commit024e72fd9eba0a908e9b8aeb38e38042da03c0ba (patch)
treec6a6b7c6146a7c427306d1aba03e9c444aec5e4b
parentc10f4c4484ace16981c4a397fb18bfdcd905367b (diff)
HTTP/3: Avoid per-request cancellation token allocationsjamesnk/http3-completefeature
-rw-r--r--src/Servers/Connections.Abstractions/src/Features/IStreamClosedFeature.cs21
-rw-r--r--src/Servers/Connections.Abstractions/src/PublicAPI.Unshipped.txt2
-rw-r--r--src/Servers/Kestrel/Core/src/Internal/Http3/Http3ControlStream.cs12
-rw-r--r--src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs67
-rw-r--r--src/Servers/Kestrel/Core/test/Http3/Http3HttpProtocolFeatureCollectionTests.cs8
-rw-r--r--src/Servers/Kestrel/Transport.Quic/src/Internal/QuicStreamContext.FeatureCollection.cs31
-rw-r--r--src/Servers/Kestrel/Transport.Quic/src/Internal/QuicStreamContext.cs64
-rw-r--r--src/Servers/Kestrel/shared/TransportConnection.Generated.cs22
-rw-r--r--src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs15
-rw-r--r--src/Servers/Kestrel/tools/CodeGenerator/HttpProtocolFeatureCollection.cs110
-rw-r--r--src/Servers/Kestrel/tools/CodeGenerator/TransportConnectionFeatureCollection.cs37
11 files changed, 248 insertions, 141 deletions
diff --git a/src/Servers/Connections.Abstractions/src/Features/IStreamClosedFeature.cs b/src/Servers/Connections.Abstractions/src/Features/IStreamClosedFeature.cs
new file mode 100644
index 0000000000..8fe03352ba
--- /dev/null
+++ b/src/Servers/Connections.Abstractions/src/Features/IStreamClosedFeature.cs
@@ -0,0 +1,21 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Threading.Tasks;
+using System;
+
+namespace Microsoft.AspNetCore.Connections.Features;
+
+/// <summary>
+/// Represents the close action for a stream.
+/// </summary>
+public interface IStreamClosedFeature
+{
+ /// <summary>
+ /// Registers a callback to be invoked when a stream is closed.
+ /// If the stream is already in a closed state, the callback will be run immediately.
+ /// </summary>
+ /// <param name="callback">The callback to invoke after the stream is closed.</param>
+ /// <param name="state">The state to pass into the callback.</param>
+ void OnClosed(Action<object?> callback, object? state);
+}
diff --git a/src/Servers/Connections.Abstractions/src/PublicAPI.Unshipped.txt b/src/Servers/Connections.Abstractions/src/PublicAPI.Unshipped.txt
index 7dc5c58110..553973838a 100644
--- a/src/Servers/Connections.Abstractions/src/PublicAPI.Unshipped.txt
+++ b/src/Servers/Connections.Abstractions/src/PublicAPI.Unshipped.txt
@@ -1 +1,3 @@
#nullable enable
+Microsoft.AspNetCore.Connections.Features.IStreamClosedFeature
+Microsoft.AspNetCore.Connections.Features.IStreamClosedFeature.OnClosed(System.Action<object?>! callback, object? state) -> void
diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ControlStream.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ControlStream.cs
index 38b083580e..adf2080c06 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ControlStream.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ControlStream.cs
@@ -23,11 +23,13 @@ internal abstract class Http3ControlStream : IHttp3Stream, IThreadPoolWorkItem
private readonly Http3StreamContext _context;
private readonly Http3PeerSettings _serverPeerSettings;
private readonly IStreamIdFeature _streamIdFeature;
+ private readonly IStreamClosedFeature _streamClosedFeature;
private readonly IProtocolErrorCodeFeature _errorCodeFeature;
private readonly Http3RawFrame _incomingFrame = new Http3RawFrame();
private volatile int _isClosed;
private long _headerType;
private int _gracefulCloseInitiator;
+ private bool _connectionClosed;
private bool _haveReceivedSettingsFrame;
@@ -39,6 +41,7 @@ internal abstract class Http3ControlStream : IHttp3Stream, IThreadPoolWorkItem
_context = context;
_serverPeerSettings = context.ServerPeerSettings;
_streamIdFeature = context.ConnectionFeatures.GetRequiredFeature<IStreamIdFeature>();
+ _streamClosedFeature = context.ConnectionFeatures.GetRequiredFeature<IStreamClosedFeature>();
_errorCodeFeature = context.ConnectionFeatures.GetRequiredFeature<IProtocolErrorCodeFeature>();
_headerType = headerType ?? -1;
@@ -57,6 +60,7 @@ internal abstract class Http3ControlStream : IHttp3Stream, IThreadPoolWorkItem
private void OnStreamClosed()
{
Abort(new ConnectionAbortedException("HTTP_CLOSED_CRITICAL_STREAM"), Http3ErrorCode.InternalError);
+ _connectionClosed = true;
}
public PipeReader Input => _context.Transport.Input;
@@ -237,7 +241,7 @@ internal abstract class Http3ControlStream : IHttp3Stream, IThreadPoolWorkItem
if (result.IsCompleted)
{
- if (!_context.StreamContext.ConnectionClosed.IsCancellationRequested)
+ if (!_connectionClosed)
{
// https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-6.2.1-2
throw new Http3ConnectionErrorException(CoreStrings.Http3ErrorControlStreamClientClosedInbound, Http3ErrorCode.ClosedCriticalStream);
@@ -298,7 +302,11 @@ internal abstract class Http3ControlStream : IHttp3Stream, IThreadPoolWorkItem
}
_haveReceivedSettingsFrame = true;
- using var closedRegistration = _context.StreamContext.ConnectionClosed.Register(state => ((Http3ControlStream)state!).OnStreamClosed(), this);
+ _streamClosedFeature.OnClosed(static state =>
+ {
+ var stream = (Http3ControlStream)state!;
+ stream.OnStreamClosed();
+ }, this);
while (true)
{
diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs
index 5f3a7dc23d..1ee524277e 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs
@@ -46,6 +46,7 @@ internal abstract partial class Http3Stream : HttpProtocol, IHttp3Stream, IHttpS
private IProtocolErrorCodeFeature _errorCodeFeature = default!;
private IStreamIdFeature _streamIdFeature = default!;
private IStreamAbortFeature _streamAbortFeature = default!;
+ private IStreamClosedFeature _streamClosedFeature = default!;
private PseudoHeaderFields _parsedPseudoHeaderFields;
private StreamCompletionFlags _completionState;
private int _isClosed;
@@ -88,6 +89,7 @@ internal abstract partial class Http3Stream : HttpProtocol, IHttp3Stream, IHttpS
_errorCodeFeature = _context.ConnectionFeatures.GetRequiredFeature<IProtocolErrorCodeFeature>();
_streamIdFeature = _context.ConnectionFeatures.GetRequiredFeature<IStreamIdFeature>();
_streamAbortFeature = _context.ConnectionFeatures.GetRequiredFeature<IStreamAbortFeature>();
+ _streamClosedFeature = _context.ConnectionFeatures.GetRequiredFeature<IStreamClosedFeature>();
_appCompletedTaskSource.Reset();
_isClosed = 0;
@@ -144,7 +146,7 @@ internal abstract partial class Http3Stream : HttpProtocol, IHttp3Stream, IHttpS
{
lock (_completionLock)
{
- if (IsCompleted)
+ if (IsCompleted || IsAborted)
{
return;
}
@@ -573,6 +575,28 @@ internal abstract partial class Http3Stream : HttpProtocol, IHttp3Stream, IHttpS
{
Exception? error = null;
+ // With HTTP/3 the write-side of the stream can be aborted by the client after the server
+ // has finished reading incoming content. That means errors can happen after the Input loop
+ // has finished reading.
+ //
+ // To get notification of request aborted we register to the stream closing or complete.
+ // It will notify this type that the client has aborted the request and Kestrel will complete
+ // pipes and cancel the HttpContext.RequestAborted token.
+ _streamClosedFeature.OnClosed(static s =>
+ {
+ var stream = (Http3Stream)s!;
+
+ if (!stream.IsCompleted)
+ {
+ // An error code value other than -1 indicates a value was set and the request didn't gracefully complete.
+ var errorCode = stream._errorCodeFeature.Error;
+ if (errorCode >= 0)
+ {
+ stream.AbortCore(new IOException(CoreStrings.HttpStreamResetByClient), (Http3ErrorCode)errorCode);
+ }
+ }
+ }, this);
+
try
{
while (_isClosed == 0)
@@ -643,44 +667,9 @@ internal abstract partial class Http3Stream : HttpProtocol, IHttp3Stream, IHttpS
? new ValueTask(_appCompletedTaskSource, _appCompletedTaskSource.Version)
: ValueTask.CompletedTask;
- if (!appCompletedTask.IsCompletedSuccessfully)
- {
- // At this point in the stream's read-side is complete. However, with HTTP/3
- // the write-side of the stream can still be aborted by the client on request
- // aborted.
- //
- // To get notification of request aborted we register to connection closed
- // token. It will notify this type that the client has aborted the request
- // and Kestrel will complete pipes and cancel the RequestAborted token.
- //
- // Only subscribe to this event after the stream's read-side is complete to
- // avoid interactions between reading that is in-progress and an abort.
- // This means while reading, read-side abort will handle getting abort notifications.
- //
- // We don't need to hang on to the CancellationTokenRegistration from register.
- // The CTS is cleaned up in StreamContext.DisposeAsync.
- //
- // TODO: Consider a better way to provide this notification. For perf we want to
- // make the ConnectionClosed CTS pay-for-play, and change this event to use
- // something that is more lightweight than a CTS.
- _context.StreamContext.ConnectionClosed.Register(static s =>
- {
- var stream = (Http3Stream)s!;
-
- if (!stream.IsCompleted)
- {
- // An error code value other than -1 indicates a value was set and the request didn't gracefully complete.
- var errorCode = stream._errorCodeFeature.Error;
- if (errorCode >= 0)
- {
- stream.AbortCore(new IOException(CoreStrings.HttpStreamResetByClient), (Http3ErrorCode)errorCode);
- }
- }
- }, this);
-
- // Make sure application func is completed before completing writer.
- await appCompletedTask;
- }
+ // At this point, assuming an error wasn't thrown, the stream's read-side is complete.
+ // Make sure application func is completed before completing writer.
+ await appCompletedTask;
try
{
diff --git a/src/Servers/Kestrel/Core/test/Http3/Http3HttpProtocolFeatureCollectionTests.cs b/src/Servers/Kestrel/Core/test/Http3/Http3HttpProtocolFeatureCollectionTests.cs
index 4cca5d8890..ba9d460474 100644
--- a/src/Servers/Kestrel/Core/test/Http3/Http3HttpProtocolFeatureCollectionTests.cs
+++ b/src/Servers/Kestrel/Core/test/Http3/Http3HttpProtocolFeatureCollectionTests.cs
@@ -67,7 +67,7 @@ public class Http3HttpProtocolFeatureCollectionTests
}
}
- private class TestConnectionFeatures : IProtocolErrorCodeFeature, IStreamIdFeature, IStreamAbortFeature
+ private class TestConnectionFeatures : IProtocolErrorCodeFeature, IStreamIdFeature, IStreamAbortFeature, IStreamClosedFeature
{
public TestConnectionFeatures()
{
@@ -75,6 +75,7 @@ public class Http3HttpProtocolFeatureCollectionTests
featureCollection.Set<IProtocolErrorCodeFeature>(this);
featureCollection.Set<IStreamIdFeature>(this);
featureCollection.Set<IStreamAbortFeature>(this);
+ featureCollection.Set<IStreamClosedFeature>(this);
FeatureCollection = featureCollection;
}
@@ -92,5 +93,10 @@ public class Http3HttpProtocolFeatureCollectionTests
{
throw new NotImplementedException();
}
+
+ void IStreamClosedFeature.OnClosed(Action<object> callback, object state)
+ {
+ throw new NotImplementedException();
+ }
}
}
diff --git a/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicStreamContext.FeatureCollection.cs b/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicStreamContext.FeatureCollection.cs
index b3728699f0..ed0807aedf 100644
--- a/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicStreamContext.FeatureCollection.cs
+++ b/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicStreamContext.FeatureCollection.cs
@@ -7,10 +7,19 @@ using Microsoft.AspNetCore.Connections.Features;
namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Internal;
-internal sealed partial class QuicStreamContext : IPersistentStateFeature, IStreamDirectionFeature, IProtocolErrorCodeFeature, IStreamIdFeature, IStreamAbortFeature
+internal sealed partial class QuicStreamContext :
+ IPersistentStateFeature,
+ IStreamDirectionFeature,
+ IProtocolErrorCodeFeature,
+ IStreamIdFeature,
+ IStreamAbortFeature,
+ IStreamClosedFeature
{
+ private readonly record struct CloseAction(Action<object?> Callback, object? State);
+
private IDictionary<object, object?>? _persistentState;
private long? _error;
+ private List<CloseAction>? _onClosed;
public bool CanRead { get; private set; }
public bool CanWrite { get; private set; }
@@ -72,6 +81,25 @@ internal sealed partial class QuicStreamContext : IPersistentStateFeature, IStre
}
}
+ void IStreamClosedFeature.OnClosed(Action<object?> callback, object? state)
+ {
+ lock (_shutdownLock)
+ {
+ if (!_streamClosed)
+ {
+ if (_onClosed == null)
+ {
+ _onClosed = new List<CloseAction>();
+ }
+ _onClosed.Add(new CloseAction(callback, state));
+ return;
+ }
+ }
+
+ // Stream has already closed. Execute callback inline.
+ callback(state);
+ }
+
private void InitializeFeatures()
{
_currentIPersistentStateFeature = this;
@@ -79,5 +107,6 @@ internal sealed partial class QuicStreamContext : IPersistentStateFeature, IStre
_currentIProtocolErrorCodeFeature = this;
_currentIStreamIdFeature = this;
_currentIStreamAbortFeature = this;
+ _currentIStreamClosedFeature = this;
}
}
diff --git a/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicStreamContext.cs b/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicStreamContext.cs
index 8944301048..d725385523 100644
--- a/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicStreamContext.cs
+++ b/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicStreamContext.cs
@@ -29,7 +29,7 @@ internal partial class QuicStreamContext : TransportConnection, IPooledStream, I
private readonly CompletionPipeReader _transportPipeReader;
private readonly CompletionPipeWriter _transportPipeWriter;
private readonly ILogger _log;
- private CancellationTokenSource _streamClosedTokenSource = default!;
+ private CancellationTokenSource? _streamClosedTokenSource;
private string? _connectionId;
private const int MinAllocBufferSize = 4096;
private volatile Exception? _shutdownReadReason;
@@ -39,7 +39,6 @@ internal partial class QuicStreamContext : TransportConnection, IPooledStream, I
private bool _streamClosed;
private bool _serverAborted;
private bool _clientAbort;
- private TaskCompletionSource _waitForConnectionClosedTcs = default!;
private readonly object _shutdownLock = new object();
public QuicStreamContext(QuicConnectionContext connection, QuicTransportContext context)
@@ -82,12 +81,8 @@ internal partial class QuicStreamContext : TransportConnection, IPooledStream, I
_stream = stream;
- if (!(_streamClosedTokenSource?.TryReset() ?? false))
- {
- _streamClosedTokenSource = new CancellationTokenSource();
- }
-
- ConnectionClosed = _streamClosedTokenSource.Token;
+ _streamClosedTokenSource = null;
+ _onClosed?.Clear();
InitializeFeatures();
@@ -109,8 +104,6 @@ internal partial class QuicStreamContext : TransportConnection, IPooledStream, I
_streamClosed = false;
_serverAborted = false;
_clientAbort = false;
- // TODO - resetable TCS
- _waitForConnectionClosedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
// Only reset pipes if the stream has been reused.
if (CanReuse)
@@ -122,6 +115,20 @@ internal partial class QuicStreamContext : TransportConnection, IPooledStream, I
CanReuse = false;
}
+ public override CancellationToken ConnectionClosed
+ {
+ get
+ {
+ // Allocate CTS only if requested.
+ if (_streamClosedTokenSource == null)
+ {
+ _streamClosedTokenSource = new CancellationTokenSource();
+ }
+ return _streamClosedTokenSource.Token;
+ }
+ set => throw new NotSupportedException();
+ }
+
public override string ConnectionId
{
get => _connectionId ??= StringUtilities.ConcatAsHexSuffix(_connection.ConnectionId, ':', (uint)StreamId);
@@ -162,7 +169,7 @@ internal partial class QuicStreamContext : TransportConnection, IPooledStream, I
await receiveTask;
await sendTask;
- await FireStreamClosedAsync();
+ FireStreamClosed();
}
catch (Exception ex)
{
@@ -311,30 +318,39 @@ internal partial class QuicStreamContext : TransportConnection, IPooledStream, I
return _shutdownReadReason ?? _shutdownReason ?? error;
}
- private Task FireStreamClosedAsync()
+ private void FireStreamClosed()
{
// Guard against scheduling this multiple times
- if (_streamClosed)
+ lock (_shutdownLock)
{
- return Task.CompletedTask;
+ if (_streamClosed)
+ {
+ return;
+ }
+
+ _streamClosed = true;
}
- _streamClosed = true;
+ var onClosed = _onClosed;
- ThreadPool.UnsafeQueueUserWorkItem(state =>
+ if (onClosed != null)
{
- state.CancelConnectionClosedToken();
-
- state._waitForConnectionClosedTcs.TrySetResult();
- },
- this,
- preferLocal: false);
+ foreach (var closeAction in onClosed)
+ {
+ closeAction.Callback(closeAction.State);
+ }
+ }
- return _waitForConnectionClosedTcs.Task;
+ if (_streamClosedTokenSource != null)
+ {
+ CancelConnectionClosedToken();
+ }
}
private void CancelConnectionClosedToken()
{
+ Debug.Assert(_streamClosedTokenSource != null);
+
try
{
_streamClosedTokenSource.Cancel();
@@ -567,6 +583,6 @@ internal partial class QuicStreamContext : TransportConnection, IPooledStream, I
// Called when the stream is no longer reused.
public void DisposeCore()
{
- _streamClosedTokenSource.Dispose();
+ _streamClosedTokenSource?.Dispose();
}
}
diff --git a/src/Servers/Kestrel/shared/TransportConnection.Generated.cs b/src/Servers/Kestrel/shared/TransportConnection.Generated.cs
index b530dc8c15..090fa212e3 100644
--- a/src/Servers/Kestrel/shared/TransportConnection.Generated.cs
+++ b/src/Servers/Kestrel/shared/TransportConnection.Generated.cs
@@ -34,6 +34,7 @@ namespace Microsoft.AspNetCore.Connections
internal protected IStreamDirectionFeature? _currentIStreamDirectionFeature;
internal protected IStreamIdFeature? _currentIStreamIdFeature;
internal protected IStreamAbortFeature? _currentIStreamAbortFeature;
+ internal protected IStreamClosedFeature? _currentIStreamClosedFeature;
private int _featureRevision;
@@ -53,6 +54,7 @@ namespace Microsoft.AspNetCore.Connections
_currentIStreamDirectionFeature = null;
_currentIStreamIdFeature = null;
_currentIStreamAbortFeature = null;
+ _currentIStreamClosedFeature = null;
}
// Internal for testing
@@ -168,6 +170,10 @@ namespace Microsoft.AspNetCore.Connections
{
feature = _currentIStreamAbortFeature;
}
+ else if (key == typeof(IStreamClosedFeature))
+ {
+ feature = _currentIStreamClosedFeature;
+ }
else if (MaybeExtra != null)
{
feature = ExtraFeatureGet(key);
@@ -224,6 +230,10 @@ namespace Microsoft.AspNetCore.Connections
{
_currentIStreamAbortFeature = (IStreamAbortFeature?)value;
}
+ else if (key == typeof(IStreamClosedFeature))
+ {
+ _currentIStreamClosedFeature = (IStreamClosedFeature?)value;
+ }
else
{
ExtraFeatureSet(key, value);
@@ -282,6 +292,10 @@ namespace Microsoft.AspNetCore.Connections
{
feature = Unsafe.As<IStreamAbortFeature?, TFeature?>(ref _currentIStreamAbortFeature);
}
+ else if (typeof(TFeature) == typeof(IStreamClosedFeature))
+ {
+ feature = Unsafe.As<IStreamClosedFeature?, TFeature?>(ref _currentIStreamClosedFeature);
+ }
else if (MaybeExtra != null)
{
feature = (TFeature?)(ExtraFeatureGet(typeof(TFeature)));
@@ -346,6 +360,10 @@ namespace Microsoft.AspNetCore.Connections
{
_currentIStreamAbortFeature = Unsafe.As<TFeature?, IStreamAbortFeature?>(ref feature);
}
+ else if (typeof(TFeature) == typeof(IStreamClosedFeature))
+ {
+ _currentIStreamClosedFeature = Unsafe.As<TFeature?, IStreamClosedFeature?>(ref feature);
+ }
else
{
ExtraFeatureSet(typeof(TFeature), feature);
@@ -398,6 +416,10 @@ namespace Microsoft.AspNetCore.Connections
{
yield return new KeyValuePair<Type, object>(typeof(IStreamAbortFeature), _currentIStreamAbortFeature);
}
+ if (_currentIStreamClosedFeature != null)
+ {
+ yield return new KeyValuePair<Type, object>(typeof(IStreamClosedFeature), _currentIStreamClosedFeature);
+ }
if (MaybeExtra != null)
{
diff --git a/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs b/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs
index 9923e06cf2..443a1eb5e0 100644
--- a/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs
+++ b/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs
@@ -1077,8 +1077,10 @@ internal class TestMultiplexedConnectionContext : MultiplexedConnectionContext,
}
}
-internal class TestStreamContext : ConnectionContext, IStreamDirectionFeature, IStreamIdFeature, IProtocolErrorCodeFeature, IPersistentStateFeature, IStreamAbortFeature, IDisposable
+internal class TestStreamContext : ConnectionContext, IStreamDirectionFeature, IStreamIdFeature, IProtocolErrorCodeFeature, IPersistentStateFeature, IStreamAbortFeature, IDisposable, IStreamClosedFeature
{
+ private readonly record struct CloseAction(Action<object> Callback, object State);
+
private readonly Http3InMemory _testBase;
internal DuplexPipePair _pair;
@@ -1096,6 +1098,7 @@ internal class TestStreamContext : ConnectionContext, IStreamDirectionFeature, I
private TaskCompletionSource _disposingTcs;
private TaskCompletionSource _disposedTcs;
internal long? _error;
+ private List<CloseAction> _onClosed;
public TestStreamContext(bool canRead, bool canWrite, Http3InMemory testBase)
{
@@ -1143,6 +1146,7 @@ internal class TestStreamContext : ConnectionContext, IStreamDirectionFeature, I
Features.Set<IStreamAbortFeature>(this);
Features.Set<IProtocolErrorCodeFeature>(this);
Features.Set<IPersistentStateFeature>(this);
+ Features.Set<IStreamClosedFeature>(this);
StreamId = streamId;
_testBase.Logger.LogInformation($"Initializing stream {streamId}");
@@ -1264,4 +1268,13 @@ internal class TestStreamContext : ConnectionContext, IStreamDirectionFeature, I
{
AbortWriteException = abortReason;
}
+
+ public void OnClosed(Action<object> callback, object state)
+ {
+ if (_onClosed == null)
+ {
+ _onClosed = new List<CloseAction>();
+ }
+ _onClosed.Add(new CloseAction(callback, state));
+ }
}
diff --git a/src/Servers/Kestrel/tools/CodeGenerator/HttpProtocolFeatureCollection.cs b/src/Servers/Kestrel/tools/CodeGenerator/HttpProtocolFeatureCollection.cs
index 3880971c75..6d43d799af 100644
--- a/src/Servers/Kestrel/tools/CodeGenerator/HttpProtocolFeatureCollection.cs
+++ b/src/Servers/Kestrel/tools/CodeGenerator/HttpProtocolFeatureCollection.cs
@@ -9,51 +9,51 @@ public class HttpProtocolFeatureCollection
{
var alwaysFeatures = new[]
{
- "IHttpRequestFeature",
- "IHttpResponseFeature",
- "IHttpResponseBodyFeature",
- "IRouteValuesFeature",
- "IEndpointFeature",
- "IServiceProvidersFeature",
- "IHttpActivityFeature"
- };
+ "IHttpRequestFeature",
+ "IHttpResponseFeature",
+ "IHttpResponseBodyFeature",
+ "IRouteValuesFeature",
+ "IEndpointFeature",
+ "IServiceProvidersFeature",
+ "IHttpActivityFeature"
+ };
var commonFeatures = new[]
{
- "IItemsFeature",
- "IQueryFeature",
- "IRequestBodyPipeFeature",
- "IFormFeature",
- "IHttpAuthenticationFeature",
- "IHttpRequestIdentifierFeature",
- };
+ "IItemsFeature",
+ "IQueryFeature",
+ "IRequestBodyPipeFeature",
+ "IFormFeature",
+ "IHttpAuthenticationFeature",
+ "IHttpRequestIdentifierFeature",
+ };
var sometimesFeatures = new[]
{
- "IHttpConnectionFeature",
- "ISessionFeature",
- "IResponseCookiesFeature",
- "IHttpRequestTrailersFeature",
- "IHttpResponseTrailersFeature",
- "ITlsConnectionFeature",
- "IHttpExtendedConnectFeature",
- "IHttpUpgradeFeature",
- "IHttpWebSocketFeature",
- "IHttpWebTransportFeature",
- "IBadRequestExceptionFeature"
- };
+ "IHttpConnectionFeature",
+ "ISessionFeature",
+ "IResponseCookiesFeature",
+ "IHttpRequestTrailersFeature",
+ "IHttpResponseTrailersFeature",
+ "ITlsConnectionFeature",
+ "IHttpExtendedConnectFeature",
+ "IHttpUpgradeFeature",
+ "IHttpWebSocketFeature",
+ "IHttpWebTransportFeature",
+ "IBadRequestExceptionFeature"
+ };
var maybeFeatures = new[]
{
- "IHttp2StreamIdFeature",
- "IHttpRequestLifetimeFeature",
- "IHttpMaxRequestBodySizeFeature",
- "IHttpMinRequestBodyDataRateFeature",
- "IHttpMinResponseDataRateFeature",
- "IHttpBodyControlFeature",
- "IHttpRequestBodyDetectionFeature",
- "IHttpResetFeature",
- "IPersistentStateFeature"
- };
+ "IHttp2StreamIdFeature",
+ "IHttpRequestLifetimeFeature",
+ "IHttpMaxRequestBodySizeFeature",
+ "IHttpMinRequestBodyDataRateFeature",
+ "IHttpMinResponseDataRateFeature",
+ "IHttpBodyControlFeature",
+ "IHttpRequestBodyDetectionFeature",
+ "IHttpResetFeature",
+ "IPersistentStateFeature"
+ };
var allFeatures = alwaysFeatures
.Concat(commonFeatures)
@@ -65,24 +65,24 @@ public class HttpProtocolFeatureCollection
// See also: src/Kestrel.Core/Internal/Http/HttpProtocol.FeatureCollection.cs
var implementedFeatures = new[]
{
- "IHttpRequestFeature",
- "IHttpResponseFeature",
- "IHttpResponseBodyFeature",
- "IRouteValuesFeature",
- "IEndpointFeature",
- "IHttpRequestIdentifierFeature",
- "IHttpRequestTrailersFeature",
- "IHttpExtendedConnectFeature",
- "IHttpUpgradeFeature",
- "IRequestBodyPipeFeature",
- "IHttpConnectionFeature",
- "IHttpRequestLifetimeFeature",
- "IHttpBodyControlFeature",
- "IHttpMaxRequestBodySizeFeature",
- "IHttpRequestBodyDetectionFeature",
- "IHttpWebTransportFeature",
- "IBadRequestExceptionFeature"
- };
+ "IHttpRequestFeature",
+ "IHttpResponseFeature",
+ "IHttpResponseBodyFeature",
+ "IRouteValuesFeature",
+ "IEndpointFeature",
+ "IHttpRequestIdentifierFeature",
+ "IHttpRequestTrailersFeature",
+ "IHttpExtendedConnectFeature",
+ "IHttpUpgradeFeature",
+ "IRequestBodyPipeFeature",
+ "IHttpConnectionFeature",
+ "IHttpRequestLifetimeFeature",
+ "IHttpBodyControlFeature",
+ "IHttpMaxRequestBodySizeFeature",
+ "IHttpRequestBodyDetectionFeature",
+ "IHttpWebTransportFeature",
+ "IBadRequestExceptionFeature"
+ };
var usings = $@"
using Microsoft.AspNetCore.Connections.Features;
diff --git a/src/Servers/Kestrel/tools/CodeGenerator/TransportConnectionFeatureCollection.cs b/src/Servers/Kestrel/tools/CodeGenerator/TransportConnectionFeatureCollection.cs
index 08443507ab..056320c238 100644
--- a/src/Servers/Kestrel/tools/CodeGenerator/TransportConnectionFeatureCollection.cs
+++ b/src/Servers/Kestrel/tools/CodeGenerator/TransportConnectionFeatureCollection.cs
@@ -12,27 +12,28 @@ public class TransportConnectionFeatureCollection
var allFeatures = new[]
{
- "IConnectionIdFeature",
- "IConnectionTransportFeature",
- "IConnectionItemsFeature",
- "IPersistentStateFeature",
- "IMemoryPoolFeature",
- "IConnectionLifetimeFeature",
- "IConnectionSocketFeature",
- "IProtocolErrorCodeFeature",
- "IStreamDirectionFeature",
- "IStreamIdFeature",
- "IStreamAbortFeature"
- };
+ "IConnectionIdFeature",
+ "IConnectionTransportFeature",
+ "IConnectionItemsFeature",
+ "IPersistentStateFeature",
+ "IMemoryPoolFeature",
+ "IConnectionLifetimeFeature",
+ "IConnectionSocketFeature",
+ "IProtocolErrorCodeFeature",
+ "IStreamDirectionFeature",
+ "IStreamIdFeature",
+ "IStreamAbortFeature",
+ "IStreamClosedFeature"
+ };
var implementedFeatures = new[]
{
- "IConnectionIdFeature",
- "IConnectionTransportFeature",
- "IConnectionItemsFeature",
- "IMemoryPoolFeature",
- "IConnectionLifetimeFeature"
- };
+ "IConnectionIdFeature",
+ "IConnectionTransportFeature",
+ "IConnectionItemsFeature",
+ "IMemoryPoolFeature",
+ "IConnectionLifetimeFeature"
+ };
var usings = $@"
using Microsoft.AspNetCore.Connections.Features;