Start pooling Http2Streams (#18601)

This commit is contained in:
Justin Kotalik 2020-02-08 19:28:18 -08:00 committed by GitHub
parent 602f4678c1
commit bc6fb44840
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 228 additions and 44 deletions

View File

@ -37,8 +37,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
private int _remainingRequestHeadersBytesAllowed; private int _remainingRequestHeadersBytesAllowed;
public Http1Connection(HttpConnectionContext context) public Http1Connection(HttpConnectionContext context)
: base(context)
{ {
Initialize(context);
_context = context; _context = context;
_parser = ServiceContext.HttpParser; _parser = ServiceContext.HttpParser;
_keepAliveTicks = ServerOptions.Limits.KeepAliveTimeout.Ticks; _keepAliveTicks = ServerOptions.Limits.KeepAliveTimeout.Ticks;

View File

@ -37,7 +37,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
private Stack<KeyValuePair<Func<object, Task>, object>> _onCompleted; private Stack<KeyValuePair<Func<object, Task>, object>> _onCompleted;
private readonly object _abortLock = new object(); private readonly object _abortLock = new object();
private volatile bool _connectionAborted; protected volatile bool _connectionAborted;
private bool _preventRequestAbortedCancellation; private bool _preventRequestAbortedCancellation;
private CancellationTokenSource _abortedCts; private CancellationTokenSource _abortedCts;
private CancellationToken? _manuallySetRequestAbortToken; private CancellationToken? _manuallySetRequestAbortToken;
@ -66,7 +66,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
private long _responseBytesWritten; private long _responseBytesWritten;
private readonly HttpConnectionContext _context; private HttpConnectionContext _context;
private RouteValueDictionary _routeValues; private RouteValueDictionary _routeValues;
private Endpoint _endpoint; private Endpoint _endpoint;
@ -75,12 +75,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
private Stream _requestStreamInternal; private Stream _requestStreamInternal;
private Stream _responseStreamInternal; private Stream _responseStreamInternal;
public HttpProtocol(HttpConnectionContext context) public void Initialize(HttpConnectionContext context)
{ {
_context = context; _context = context;
ServerOptions = ServiceContext.ServerOptions; ServerOptions = ServiceContext.ServerOptions;
HttpRequestHeaders = new HttpRequestHeaders(reuseHeaderValues: !ServerOptions.DisableStringReuse);
Reset();
HttpRequestHeaders.ReuseHeaderValues = !ServerOptions.DisableStringReuse;
HttpResponseControl = this; HttpResponseControl = this;
} }
@ -97,7 +101,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
protected IKestrelTrace Log => ServiceContext.Log; protected IKestrelTrace Log => ServiceContext.Log;
private DateHeaderValueManager DateHeaderValueManager => ServiceContext.DateHeaderValueManager; private DateHeaderValueManager DateHeaderValueManager => ServiceContext.DateHeaderValueManager;
// Hold direct reference to ServerOptions since this is used very often in the request processing path // 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; protected string ConnectionId => _context.ConnectionId;
public string ConnectionIdFeature { get; set; } public string ConnectionIdFeature { get; set; }
@ -306,7 +310,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
public bool HasResponseCompleted => _requestProcessingStatus == RequestProcessingStatus.ResponseCompleted; public bool HasResponseCompleted => _requestProcessingStatus == RequestProcessingStatus.ResponseCompleted;
protected HttpRequestHeaders HttpRequestHeaders { get; } protected HttpRequestHeaders HttpRequestHeaders { get; } = new HttpRequestHeaders();
protected HttpResponseHeaders HttpResponseHeaders { get; } = new HttpResponseHeaders(); protected HttpResponseHeaders HttpResponseHeaders { get; } = new HttpResponseHeaders();
@ -361,7 +365,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
var remoteEndPoint = RemoteEndPoint; var remoteEndPoint = RemoteEndPoint;
RemoteIpAddress = remoteEndPoint?.Address; RemoteIpAddress = remoteEndPoint?.Address;
RemotePort = remoteEndPoint?.Port ?? 0; RemotePort = remoteEndPoint?.Port ?? 0;
var localEndPoint = LocalEndPoint; var localEndPoint = LocalEndPoint;
LocalIpAddress = localEndPoint?.Address; LocalIpAddress = localEndPoint?.Address;
LocalPort = localEndPoint?.Port ?? 0; LocalPort = localEndPoint?.Port ?? 0;
@ -373,6 +376,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
RequestHeaders = HttpRequestHeaders; RequestHeaders = HttpRequestHeaders;
ResponseHeaders = HttpResponseHeaders; ResponseHeaders = HttpResponseHeaders;
RequestTrailers.Clear(); RequestTrailers.Clear();
ResponseTrailers?.Reset();
RequestTrailersAvailable = false; RequestTrailersAvailable = false;
_isLeasedMemoryInvalid = true; _isLeasedMemoryInvalid = true;

View File

@ -14,14 +14,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{ {
internal sealed partial class HttpRequestHeaders : HttpHeaders internal sealed partial class HttpRequestHeaders : HttpHeaders
{ {
private readonly bool _reuseHeaderValues;
private long _previousBits = 0; private long _previousBits = 0;
public HttpRequestHeaders(bool reuseHeaderValues = true) public HttpRequestHeaders(bool reuseHeaderValues = true)
{ {
_reuseHeaderValues = reuseHeaderValues; ReuseHeaderValues = reuseHeaderValues;
} }
public bool ReuseHeaderValues { get; set; }
public void OnHeadersComplete() public void OnHeadersComplete()
{ {
var bitsToClear = _previousBits & ~_bits; var bitsToClear = _previousBits & ~_bits;
@ -40,7 +41,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
protected override void ClearFast() protected override void ClearFast()
{ {
if (!_reuseHeaderValues) if (!ReuseHeaderValues)
{ {
// If we aren't reusing headers clear them all // If we aren't reusing headers clear them all
Clear(_bits); Clear(_bits);

View File

@ -8,11 +8,9 @@ using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.IO.Pipelines; using System.IO.Pipelines;
using System.Net.Http;
using System.Net.Http.HPack; using System.Net.Http.HPack;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Security.Authentication; using System.Security.Authentication;
using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Connections; 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.Http2.FlowControl;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 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 _gracefulCloseInitiator;
private int _isClosed; private int _isClosed;
private Http2StreamStack _streamPool;
internal const int InitialStreamPoolSize = 5;
internal const int MaxStreamPoolSize = 40;
public Http2Connection(HttpConnectionContext context) public Http2Connection(HttpConnectionContext context)
{ {
var httpLimits = context.ServiceContext.ServerOptions.Limits; var httpLimits = context.ServiceContext.ServerOptions.Limits;
@ -106,6 +108,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
_serverSettings.HeaderTableSize = (uint)http2Limits.HeaderTableSize; _serverSettings.HeaderTableSize = (uint)http2Limits.HeaderTableSize;
_serverSettings.MaxHeaderListSize = (uint)httpLimits.MaxRequestHeadersTotalSize; _serverSettings.MaxHeaderListSize = (uint)httpLimits.MaxRequestHeadersTotalSize;
_serverSettings.InitialWindowSize = (uint)http2Limits.InitialStreamWindowSize; _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(); _inputTask = ReadInputAsync();
} }
@ -554,25 +560,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
} }
// Start a new stream // Start a new stream
_currentHeadersStream = new Http2Stream<TContext>(application, new Http2StreamContext _currentHeadersStream = GetStream(application);
{
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.Reset();
_headerFlags = _incomingFrame.HeadersFlags; _headerFlags = _incomingFrame.HeadersFlags;
var headersPayload = payload.Slice(0, _incomingFrame.HeadersPayloadLength); // Minus padding var headersPayload = payload.Slice(0, _incomingFrame.HeadersPayloadLength); // Minus padding
@ -580,6 +569,48 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
} }
} }
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,
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() private Task ProcessPriorityFrameAsync()
{ {
if (_currentHeadersStream != null) if (_currentHeadersStream != null)
@ -1028,6 +1059,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
} }
_streams.Remove(stream.StreamId); _streams.Remove(stream.StreamId);
ReturnStream(stream);
} }
else else
{ {
@ -1054,6 +1086,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
} }
_streams.Remove(stream.StreamId); _streams.Remove(stream.StreamId);
ReturnStream(stream);
} }
} }
@ -1400,6 +1433,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
Unknown = 0x40000000 Unknown = 0x40000000
} }
private static class GracefulCloseInitiator private static class GracefulCloseInitiator
{ {
public const int None = 0; public const int None = 0;

View File

@ -19,22 +19,30 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
{ {
internal abstract partial class Http2Stream : HttpProtocol, IThreadPoolWorkItem internal abstract partial class Http2Stream : HttpProtocol, IThreadPoolWorkItem
{ {
private readonly Http2StreamContext _context; private Http2StreamContext _context;
private readonly Http2OutputProducer _http2Output; private Http2OutputProducer _http2Output;
private readonly StreamInputFlowControl _inputFlowControl; private StreamInputFlowControl _inputFlowControl;
private readonly StreamOutputFlowControl _outputFlowControl; private StreamOutputFlowControl _outputFlowControl;
private bool _decrementCalled; private bool _decrementCalled;
public Pipe RequestBodyPipe { get; }
public Pipe RequestBodyPipe { get; set; }
internal long DrainExpirationTicks { get; set; } internal long DrainExpirationTicks { get; set; }
private StreamCompletionFlags _completionState; private StreamCompletionFlags _completionState;
private readonly object _completionLock = new object(); private readonly object _completionLock = new object();
public Http2Stream(Http2StreamContext context) public void Initialize(Http2StreamContext context)
: base(context)
{ {
base.Initialize(context);
_decrementCalled = false;
_completionState = StreamCompletionFlags.None;
InputRemaining = null;
RequestBodyStarted = false;
DrainExpirationTicks = 0;
_context = context; _context = context;
_inputFlowControl = new StreamInputFlowControl( _inputFlowControl = new StreamInputFlowControl(
@ -60,6 +68,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
Output = _http2Output; Output = _http2Output;
} }
public void InitializeWithExistingContext(int streamId)
{
_context.StreamId = streamId;
Initialize(_context);
}
public int StreamId => _context.StreamId; public int StreamId => _context.StreamId;
public long? InputRemaining { get; internal set; } public long? InputRemaining { get; internal set; }
@ -82,6 +96,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
protected override void OnReset() protected override void OnReset()
{ {
_keepAlive = true;
_connectionAborted = false;
ResetHttp2Features(); ResetHttp2Features();
} }

View File

@ -11,8 +11,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
{ {
private readonly IHttpApplication<TContext> _application; 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; _application = application;
} }

View File

@ -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;
}
}
}

View File

@ -31,8 +31,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
private bool _receivedHeaders; private bool _receivedHeaders;
public Pipe RequestBodyPipe { get; } 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 // First, determine how we know if an Http3stream is unidirectional or bidirectional
var httpLimits = context.ServiceContext.ServerOptions.Limits; var httpLimits = context.ServiceContext.ServerOptions.Limits;
var http3Limits = httpLimits.Http3; var http3Limits = httpLimits.Http3;
@ -115,7 +116,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
{ {
try try
{ {
while (_isClosed == 0) while (_isClosed == 0)
{ {
var result = await Input.ReadAsync(); var result = await Input.ReadAsync();

View File

@ -69,7 +69,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
}; };
_http1Connection = new TestHttp1Connection(_http1ConnectionContext); _http1Connection = new TestHttp1Connection(_http1ConnectionContext);
_http1Connection.Reset();
} }
public void Dispose() public void Dispose()

View File

@ -249,8 +249,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
private class TestHttp2Stream : Http2Stream private class TestHttp2Stream : Http2Stream
{ {
public TestHttp2Stream(Http2StreamContext context) : base(context) public TestHttp2Stream(Http2StreamContext context)
{ {
Initialize(context);
} }
public override void Execute() public override void Execute()

View File

@ -1866,6 +1866,58 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
Assert.Equal("0", _decodedHeaders[HeaderNames.ContentLength]); 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] [Fact]
public async Task ResponseTrailers_WithData_Sent() public async Task ResponseTrailers_WithData_Sent()
{ {