Start pooling Http2Streams (#18601)
This commit is contained in:
parent
602f4678c1
commit
bc6fb44840
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
private Stack<KeyValuePair<Func<object, Task>, 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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,7 +560,31 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
|||
}
|
||||
|
||||
// Start a new stream
|
||||
_currentHeadersStream = new Http2Stream<TContext>(application, new Http2StreamContext
|
||||
_currentHeadersStream = GetStream(application);
|
||||
|
||||
_headerFlags = _incomingFrame.HeadersFlags;
|
||||
|
||||
var headersPayload = payload.Slice(0, _incomingFrame.HeadersPayloadLength); // Minus padding
|
||||
return DecodeHeadersAsync(_incomingFrame.HeadersEndHeaders, headersPayload);
|
||||
}
|
||||
}
|
||||
|
||||
private Http2Stream GetStream<TContext>(IHttpApplication<TContext> application)
|
||||
{
|
||||
if (_streamPool.TryPop(out var stream))
|
||||
{
|
||||
stream.InitializeWithExistingContext(_incomingFrame.StreamId);
|
||||
return stream;
|
||||
}
|
||||
|
||||
return new Http2Stream<TContext>(
|
||||
application,
|
||||
CreateHttp2StreamContext());
|
||||
}
|
||||
|
||||
private Http2StreamContext CreateHttp2StreamContext()
|
||||
{
|
||||
return new Http2StreamContext
|
||||
{
|
||||
ConnectionId = ConnectionId,
|
||||
StreamId = _incomingFrame.StreamId,
|
||||
|
|
@ -570,13 +600,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
|||
ConnectionInputFlowControl = _inputFlowControl,
|
||||
ConnectionOutputFlowControl = _outputFlowControl,
|
||||
TimeoutControl = TimeoutControl,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
_currentHeadersStream.Reset();
|
||||
_headerFlags = _incomingFrame.HeadersFlags;
|
||||
|
||||
var headersPayload = payload.Slice(0, _incomingFrame.HeadersPayloadLength); // Minus padding
|
||||
return DecodeHeadersAsync(_incomingFrame.HeadersEndHeaders, headersPayload);
|
||||
private void ReturnStream(Http2Stream stream)
|
||||
{
|
||||
if (_streamPool.Count < MaxStreamPoolSize)
|
||||
{
|
||||
_streamPool.Push(stream);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,8 +11,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
|
|||
{
|
||||
private readonly IHttpApplication<TContext> _application;
|
||||
|
||||
public Http2Stream(IHttpApplication<TContext> application, Http2StreamContext context) : base(context)
|
||||
public Http2Stream(IHttpApplication<TContext> application, Http2StreamContext context)
|
||||
{
|
||||
Initialize(context);
|
||||
_application = application;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -69,7 +69,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
};
|
||||
|
||||
_http1Connection = new TestHttp1Connection(_http1ConnectionContext);
|
||||
_http1Connection.Reset();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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<IHttpResponseTrailersFeature>().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()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue