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;
public Http1Connection(HttpConnectionContext context)
: base(context)
{
Initialize(context);
_context = context;
_parser = ServiceContext.HttpParser;
_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 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;

View File

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

View File

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

View File

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

View File

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

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;
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();

View File

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

View File

@ -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()

View File

@ -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()
{