diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs b/src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs index 8cf1bd068b..09c8d7923c 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs @@ -37,8 +37,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http private int _remainingRequestHeadersBytesAllowed; public Http1Connection(HttpConnectionContext context) - : base(context) { + Initialize(context); + _context = context; _parser = ServiceContext.HttpParser; _keepAliveTicks = ServerOptions.Limits.KeepAliveTimeout.Ticks; diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs index 7c4b8a91d5..f4419c44c7 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs @@ -37,7 +37,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http private Stack, object>> _onCompleted; private readonly object _abortLock = new object(); - private volatile bool _connectionAborted; + protected volatile bool _connectionAborted; private bool _preventRequestAbortedCancellation; private CancellationTokenSource _abortedCts; private CancellationToken? _manuallySetRequestAbortToken; @@ -66,7 +66,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http private long _responseBytesWritten; - private readonly HttpConnectionContext _context; + private HttpConnectionContext _context; private RouteValueDictionary _routeValues; private Endpoint _endpoint; @@ -75,12 +75,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http private Stream _requestStreamInternal; private Stream _responseStreamInternal; - public HttpProtocol(HttpConnectionContext context) + public void Initialize(HttpConnectionContext context) { _context = context; ServerOptions = ServiceContext.ServerOptions; - HttpRequestHeaders = new HttpRequestHeaders(reuseHeaderValues: !ServerOptions.DisableStringReuse); + + Reset(); + + HttpRequestHeaders.ReuseHeaderValues = !ServerOptions.DisableStringReuse; + HttpResponseControl = this; } @@ -97,7 +101,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http protected IKestrelTrace Log => ServiceContext.Log; private DateHeaderValueManager DateHeaderValueManager => ServiceContext.DateHeaderValueManager; // Hold direct reference to ServerOptions since this is used very often in the request processing path - protected KestrelServerOptions ServerOptions { get; } + protected KestrelServerOptions ServerOptions { get; set; } protected string ConnectionId => _context.ConnectionId; public string ConnectionIdFeature { get; set; } @@ -306,7 +310,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http public bool HasResponseCompleted => _requestProcessingStatus == RequestProcessingStatus.ResponseCompleted; - protected HttpRequestHeaders HttpRequestHeaders { get; } + protected HttpRequestHeaders HttpRequestHeaders { get; } = new HttpRequestHeaders(); protected HttpResponseHeaders HttpResponseHeaders { get; } = new HttpResponseHeaders(); @@ -361,7 +365,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http var remoteEndPoint = RemoteEndPoint; RemoteIpAddress = remoteEndPoint?.Address; RemotePort = remoteEndPoint?.Port ?? 0; - var localEndPoint = LocalEndPoint; LocalIpAddress = localEndPoint?.Address; LocalPort = localEndPoint?.Port ?? 0; @@ -373,6 +376,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http RequestHeaders = HttpRequestHeaders; ResponseHeaders = HttpResponseHeaders; RequestTrailers.Clear(); + ResponseTrailers?.Reset(); RequestTrailersAvailable = false; _isLeasedMemoryInvalid = true; diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpRequestHeaders.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpRequestHeaders.cs index 66ae0aad54..329ce4b312 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpRequestHeaders.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpRequestHeaders.cs @@ -14,14 +14,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { internal sealed partial class HttpRequestHeaders : HttpHeaders { - private readonly bool _reuseHeaderValues; private long _previousBits = 0; public HttpRequestHeaders(bool reuseHeaderValues = true) { - _reuseHeaderValues = reuseHeaderValues; + ReuseHeaderValues = reuseHeaderValues; } + public bool ReuseHeaderValues { get; set; } + public void OnHeadersComplete() { var bitsToClear = _previousBits & ~_bits; @@ -40,7 +41,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http protected override void ClearFast() { - if (!_reuseHeaderValues) + if (!ReuseHeaderValues) { // If we aren't reusing headers clear them all Clear(_bits); diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs index 99df38ed6b..5ef8372da3 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs @@ -8,11 +8,9 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.IO.Pipelines; -using System.Net.Http; using System.Net.Http.HPack; using System.Runtime.CompilerServices; using System.Security.Authentication; -using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; @@ -23,7 +21,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.Extensions.Logging; -using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 { @@ -67,6 +64,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 private int _gracefulCloseInitiator; private int _isClosed; + private Http2StreamStack _streamPool; + + internal const int InitialStreamPoolSize = 5; + internal const int MaxStreamPoolSize = 40; + public Http2Connection(HttpConnectionContext context) { var httpLimits = context.ServiceContext.ServerOptions.Limits; @@ -106,6 +108,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 _serverSettings.HeaderTableSize = (uint)http2Limits.HeaderTableSize; _serverSettings.MaxHeaderListSize = (uint)httpLimits.MaxRequestHeadersTotalSize; _serverSettings.InitialWindowSize = (uint)http2Limits.InitialStreamWindowSize; + + // Start pool off at a smaller size if the max number of streams is less than the InitialStreamPoolSize + _streamPool = new Http2StreamStack(Math.Min(InitialStreamPoolSize, http2Limits.MaxStreamsPerConnection)); + _inputTask = ReadInputAsync(); } @@ -554,25 +560,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 } // Start a new stream - _currentHeadersStream = new Http2Stream(application, new Http2StreamContext - { - ConnectionId = ConnectionId, - StreamId = _incomingFrame.StreamId, - ServiceContext = _context.ServiceContext, - ConnectionFeatures = _context.ConnectionFeatures, - MemoryPool = _context.MemoryPool, - LocalEndPoint = _context.LocalEndPoint, - RemoteEndPoint = _context.RemoteEndPoint, - StreamLifetimeHandler = this, - ClientPeerSettings = _clientSettings, - ServerPeerSettings = _serverSettings, - FrameWriter = _frameWriter, - ConnectionInputFlowControl = _inputFlowControl, - ConnectionOutputFlowControl = _outputFlowControl, - TimeoutControl = TimeoutControl, - }); + _currentHeadersStream = GetStream(application); - _currentHeadersStream.Reset(); _headerFlags = _incomingFrame.HeadersFlags; var headersPayload = payload.Slice(0, _incomingFrame.HeadersPayloadLength); // Minus padding @@ -580,6 +569,48 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 } } + private Http2Stream GetStream(IHttpApplication application) + { + if (_streamPool.TryPop(out var stream)) + { + stream.InitializeWithExistingContext(_incomingFrame.StreamId); + return stream; + } + + return new Http2Stream( + application, + CreateHttp2StreamContext()); + } + + private Http2StreamContext CreateHttp2StreamContext() + { + return new Http2StreamContext + { + ConnectionId = ConnectionId, + StreamId = _incomingFrame.StreamId, + ServiceContext = _context.ServiceContext, + ConnectionFeatures = _context.ConnectionFeatures, + MemoryPool = _context.MemoryPool, + LocalEndPoint = _context.LocalEndPoint, + RemoteEndPoint = _context.RemoteEndPoint, + StreamLifetimeHandler = this, + ClientPeerSettings = _clientSettings, + ServerPeerSettings = _serverSettings, + FrameWriter = _frameWriter, + ConnectionInputFlowControl = _inputFlowControl, + ConnectionOutputFlowControl = _outputFlowControl, + TimeoutControl = TimeoutControl, + }; + } + + private void ReturnStream(Http2Stream stream) + { + if (_streamPool.Count < MaxStreamPoolSize) + { + _streamPool.Push(stream); + } + } + private Task ProcessPriorityFrameAsync() { if (_currentHeadersStream != null) @@ -1028,6 +1059,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 } _streams.Remove(stream.StreamId); + ReturnStream(stream); } else { @@ -1054,6 +1086,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 } _streams.Remove(stream.StreamId); + ReturnStream(stream); } } @@ -1400,6 +1433,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 Unknown = 0x40000000 } + private static class GracefulCloseInitiator { public const int None = 0; diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs index 27dd7762ae..0daad7a04d 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs @@ -19,22 +19,30 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 { internal abstract partial class Http2Stream : HttpProtocol, IThreadPoolWorkItem { - private readonly Http2StreamContext _context; - private readonly Http2OutputProducer _http2Output; - private readonly StreamInputFlowControl _inputFlowControl; - private readonly StreamOutputFlowControl _outputFlowControl; + private Http2StreamContext _context; + private Http2OutputProducer _http2Output; + private StreamInputFlowControl _inputFlowControl; + private StreamOutputFlowControl _outputFlowControl; private bool _decrementCalled; - public Pipe RequestBodyPipe { get; } + + public Pipe RequestBodyPipe { get; set; } internal long DrainExpirationTicks { get; set; } private StreamCompletionFlags _completionState; private readonly object _completionLock = new object(); - public Http2Stream(Http2StreamContext context) - : base(context) + public void Initialize(Http2StreamContext context) { + base.Initialize(context); + + _decrementCalled = false; + _completionState = StreamCompletionFlags.None; + InputRemaining = null; + RequestBodyStarted = false; + DrainExpirationTicks = 0; + _context = context; _inputFlowControl = new StreamInputFlowControl( @@ -60,6 +68,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 Output = _http2Output; } + public void InitializeWithExistingContext(int streamId) + { + _context.StreamId = streamId; + Initialize(_context); + } + public int StreamId => _context.StreamId; public long? InputRemaining { get; internal set; } @@ -82,6 +96,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 protected override void OnReset() { + _keepAlive = true; + _connectionAborted = false; + ResetHttp2Features(); } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2StreamOfT.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2StreamOfT.cs index e602618976..2ba223f43c 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2StreamOfT.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2StreamOfT.cs @@ -11,8 +11,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 { private readonly IHttpApplication _application; - public Http2Stream(IHttpApplication application, Http2StreamContext context) : base(context) + public Http2Stream(IHttpApplication application, Http2StreamContext context) { + Initialize(context); _application = application; } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2StreamStack.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2StreamStack.cs new file mode 100644 index 0000000000..1c141dd236 --- /dev/null +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2StreamStack.cs @@ -0,0 +1,74 @@ +// 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.Runtime.CompilerServices; + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 +{ + // See https://github.com/dotnet/runtime/blob/master/src/libraries/System.IO.Pipelines/src/System/IO/Pipelines/BufferSegmentStack.cs + internal struct Http2StreamStack + { + private Http2StreamAsValueType[] _array; + private int _size; + + public Http2StreamStack(int size) + { + _array = new Http2StreamAsValueType[size]; + _size = 0; + } + + public int Count => _size; + + public bool TryPop(out Http2Stream result) + { + int size = _size - 1; + Http2StreamAsValueType[] array = _array; + + if ((uint)size >= (uint)array.Length) + { + result = default; + return false; + } + + _size = size; + result = array[size]; + array[size] = default; + return true; + } + + // Pushes an item to the top of the stack. + public void Push(Http2Stream item) + { + int size = _size; + Http2StreamAsValueType[] array = _array; + + if ((uint)size < (uint)array.Length) + { + array[size] = item; + _size = size + 1; + } + else + { + PushWithResize(item); + } + } + + // Non-inline from Stack.Push to improve its code quality as uncommon path + [MethodImpl(MethodImplOptions.NoInlining)] + private void PushWithResize(Http2Stream item) + { + Array.Resize(ref _array, 2 * _array.Length); + _array[_size] = item; + _size++; + } + + private readonly struct Http2StreamAsValueType + { + private readonly Http2Stream _value; + private Http2StreamAsValueType(Http2Stream value) => _value = value; + public static implicit operator Http2StreamAsValueType(Http2Stream s) => new Http2StreamAsValueType(s); + public static implicit operator Http2Stream(Http2StreamAsValueType s) => s._value; + } + } +} diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs index e708a89967..fb7679fa7f 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs @@ -31,8 +31,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 private bool _receivedHeaders; public Pipe RequestBodyPipe { get; } - public Http3Stream(Http3Connection http3Connection, HttpConnectionContext context) : base(context) + public Http3Stream(Http3Connection http3Connection, HttpConnectionContext context) { + Initialize(context); // First, determine how we know if an Http3stream is unidirectional or bidirectional var httpLimits = context.ServiceContext.ServerOptions.Limits; var http3Limits = httpLimits.Http3; @@ -115,7 +116,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 { try { - while (_isClosed == 0) { var result = await Input.ReadAsync(); diff --git a/src/Servers/Kestrel/Core/test/Http1ConnectionTests.cs b/src/Servers/Kestrel/Core/test/Http1ConnectionTests.cs index 9db4d37f9c..0992626a80 100644 --- a/src/Servers/Kestrel/Core/test/Http1ConnectionTests.cs +++ b/src/Servers/Kestrel/Core/test/Http1ConnectionTests.cs @@ -69,7 +69,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests }; _http1Connection = new TestHttp1Connection(_http1ConnectionContext); - _http1Connection.Reset(); } public void Dispose() diff --git a/src/Servers/Kestrel/Core/test/HttpProtocolFeatureCollectionTests.cs b/src/Servers/Kestrel/Core/test/HttpProtocolFeatureCollectionTests.cs index 030931dae4..f62bad9117 100644 --- a/src/Servers/Kestrel/Core/test/HttpProtocolFeatureCollectionTests.cs +++ b/src/Servers/Kestrel/Core/test/HttpProtocolFeatureCollectionTests.cs @@ -249,8 +249,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests private class TestHttp2Stream : Http2Stream { - public TestHttp2Stream(Http2StreamContext context) : base(context) + public TestHttp2Stream(Http2StreamContext context) { + Initialize(context); } public override void Execute() diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs index ea2c0d4ce8..109e513bff 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs @@ -1866,6 +1866,58 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests Assert.Equal("0", _decodedHeaders[HeaderNames.ContentLength]); } + [Fact] + public async Task ResponseTrailers_WorksAcrossMultipleStreams_Cleared() + { + await InitializeConnectionAsync(context => + { + Assert.True(context.Response.SupportsTrailers(), "SupportsTrailers"); + + var trailers = context.Features.Get().Trailers; + Assert.False(trailers.IsReadOnly); + + context.Response.AppendTrailer("CustomName", "Custom Value"); + return Task.CompletedTask; + }); + + await StartStreamAsync(1, _browserRequestHeaders, endStream: true); + + var headersFrame1 = await ExpectAsync(Http2FrameType.HEADERS, + withLength: 55, + withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS), + withStreamId: 1); + var trailersFrame1 = await ExpectAsync(Http2FrameType.HEADERS, + withLength: 25, + withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), + withStreamId: 1); + + await StartStreamAsync(3, _browserRequestHeaders, endStream: true); + + var headersFrame2 = await ExpectAsync(Http2FrameType.HEADERS, + withLength: 55, + withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS), + withStreamId: 3); + + var trailersFrame2 = await ExpectAsync(Http2FrameType.HEADERS, + withLength: 25, + withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), + withStreamId: 3); + + await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false); + + _hpackDecoder.Decode(trailersFrame1.PayloadSequence, endHeaders: true, handler: this); + + Assert.Single(_decodedHeaders); + Assert.Equal("Custom Value", _decodedHeaders["CustomName"]); + + _decodedHeaders.Clear(); + + _hpackDecoder.Decode(trailersFrame2.PayloadSequence, endHeaders: true, handler: this); + + Assert.Single(_decodedHeaders); + Assert.Equal("Custom Value", _decodedHeaders["CustomName"]); + } + [Fact] public async Task ResponseTrailers_WithData_Sent() {