Always cache headers and streams across frames (#754).

This commit is contained in:
Cesar Blum Silveira 2016-05-19 15:24:36 -07:00
parent 67ed6c5958
commit 925d8e0200
17 changed files with 155 additions and 291 deletions

View File

@ -174,19 +174,29 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
public void InitializeHeaders()
{
_frameHeaders = HttpComponentFactory.CreateHeaders(DateHeaderValueManager);
RequestHeaders = _frameHeaders.RequestHeaders;
ResponseHeaders = _frameHeaders.ResponseHeaders;
if (_frameHeaders == null)
{
_frameHeaders = new Headers(ServerOptions);
RequestHeaders = _frameHeaders.RequestHeaders;
ResponseHeaders = _frameHeaders.ResponseHeaders;
}
_frameHeaders.Initialize(DateHeaderValueManager);
}
public void InitializeStreams(MessageBody messageBody)
{
_frameStreams = HttpComponentFactory.CreateStreams(this);
if (_frameStreams == null)
{
_frameStreams = new Streams(this);
RequestBody = _frameStreams.RequestBody;
ResponseBody = _frameStreams.ResponseBody;
DuplexStream = _frameStreams.DuplexStream;
}
RequestBody = _frameStreams.RequestBody.StartAcceptingReads(messageBody);
ResponseBody = _frameStreams.ResponseBody.StartAcceptingWrites();
DuplexStream = _frameStreams.DuplexStream;
_frameStreams.RequestBody.StartAcceptingReads(messageBody);
_frameStreams.ResponseBody.StartAcceptingWrites();
}
public void PauseStreams()
@ -209,7 +219,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
public void Reset()
{
ResetComponents();
_frameHeaders?.Reset();
_onStarting = null;
_onCompleted = null;
@ -246,26 +256,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
_abortedCts = null;
}
protected void ResetComponents()
{
var frameHeaders = Interlocked.Exchange(ref _frameHeaders, null);
if (frameHeaders != null)
{
RequestHeaders = null;
ResponseHeaders = null;
HttpComponentFactory.DisposeHeaders(frameHeaders);
}
var frameStreams = Interlocked.Exchange(ref _frameStreams, null);
if (frameStreams != null)
{
RequestBody = null;
ResponseBody = null;
DuplexStream = null;
HttpComponentFactory.DisposeStreams(frameStreams);
}
}
/// <summary>
/// Called once by Connection class to begin the RequestProcessingAsync loop.
/// </summary>

View File

@ -155,7 +155,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
{
await TryProduceInvalidRequestResponse();
ResetComponents();
_abortedCts = null;
// If _requestAborted is set, the connection has already been closed.

View File

@ -9,7 +9,7 @@ using Microsoft.AspNetCore.Server.Kestrel.Infrastructure;
namespace Microsoft.AspNetCore.Server.Kestrel.Http
{
public class FrameRequestStream : Stream
class FrameRequestStream : Stream
{
private MessageBody _body;
private FrameStreamState _state;
@ -132,7 +132,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
throw new NotSupportedException();
}
public Stream StartAcceptingReads(MessageBody body)
public void StartAcceptingReads(MessageBody body)
{
// Only start if not aborted
if (_state == FrameStreamState.Closed)
@ -140,7 +140,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
_state = FrameStreamState.Open;
_body = body;
}
return this;
}
public void PauseAcceptingReads()

View File

@ -14,8 +14,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
private IFrameControl _frameControl;
private FrameStreamState _state;
public FrameResponseStream()
public FrameResponseStream(IFrameControl frameControl)
{
_frameControl = frameControl;
_state = FrameStreamState.Closed;
}
@ -94,15 +95,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
return task;
}
public Stream StartAcceptingWrites()
public void StartAcceptingWrites()
{
// Only start if not aborted
if (_state == FrameStreamState.Closed)
{
_state = FrameStreamState.Open;
}
return this;
}
public void PauseAcceptingWrites()
@ -134,17 +133,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Http
}
}
public void Initialize(IFrameControl frameControl)
{
_frameControl = frameControl;
}
public void Uninitialize()
{
_frameControl = null;
_state = FrameStreamState.Closed;
}
private Task ValidateState(CancellationToken cancellationToken)
{
switch (_state)

View File

@ -0,0 +1,40 @@
// 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.Text;
using Microsoft.AspNetCore.Server.Kestrel.Http;
namespace Microsoft.AspNetCore.Server.Kestrel.Infrastructure
{
class Headers
{
public static readonly byte[] BytesServer = Encoding.ASCII.GetBytes("\r\nServer: Kestrel");
private readonly KestrelServerOptions _options;
public Headers(KestrelServerOptions options)
{
_options = options;
}
public void Initialize(DateHeaderValueManager dateValueManager)
{
var dateHeaderValues = dateValueManager.GetDateHeaderValues();
ResponseHeaders.SetRawDate(dateHeaderValues.String, dateHeaderValues.Bytes);
if (_options.AddServerHeader)
{
ResponseHeaders.SetRawServer("Kestrel", BytesServer);
}
}
public FrameRequestHeaders RequestHeaders { get; } = new FrameRequestHeaders();
public FrameResponseHeaders ResponseHeaders { get; } = new FrameResponseHeaders();
public void Reset()
{
RequestHeaders.Reset();
ResponseHeaders.Reset();
}
}
}

View File

@ -1,128 +0,0 @@
// 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.Collections.Concurrent;
using System.Text;
using Microsoft.AspNetCore.Server.Kestrel.Http;
namespace Microsoft.AspNetCore.Server.Kestrel.Infrastructure
{
class HttpComponentFactory : IHttpComponentFactory
{
private ConcurrentQueue<Streams> _streamPool = new ConcurrentQueue<Streams>();
private ConcurrentQueue<Headers> _headerPool = new ConcurrentQueue<Headers>();
public KestrelServerOptions ServerOptions { get; set; }
public HttpComponentFactory(KestrelServerOptions serverOptions)
{
ServerOptions = serverOptions;
}
public Streams CreateStreams(IFrameControl frameControl)
{
Streams streams;
if (!_streamPool.TryDequeue(out streams))
{
streams = new Streams();
}
streams.Initialize(frameControl);
return streams;
}
public void DisposeStreams(Streams streams)
{
if (_streamPool.Count < ServerOptions.MaxPooledStreams)
{
streams.Uninitialize();
_streamPool.Enqueue(streams);
}
}
public Headers CreateHeaders(DateHeaderValueManager dateValueManager)
{
Headers headers;
if (!_headerPool.TryDequeue(out headers))
{
headers = new Headers(ServerOptions);
}
headers.Initialize(dateValueManager);
return headers;
}
public void DisposeHeaders(Headers headers)
{
if (_headerPool.Count < ServerOptions.MaxPooledHeaders)
{
headers.Uninitialize();
_headerPool.Enqueue(headers);
}
}
}
internal class Headers
{
public static readonly byte[] BytesServer = Encoding.ASCII.GetBytes("\r\nServer: Kestrel");
public readonly FrameRequestHeaders RequestHeaders = new FrameRequestHeaders();
public readonly FrameResponseHeaders ResponseHeaders = new FrameResponseHeaders();
private readonly KestrelServerOptions _options;
public Headers(KestrelServerOptions options)
{
_options = options;
}
public void Initialize(DateHeaderValueManager dateValueManager)
{
var dateHeaderValues = dateValueManager.GetDateHeaderValues();
ResponseHeaders.SetRawDate(dateHeaderValues.String, dateHeaderValues.Bytes);
if (_options.AddServerHeader)
{
ResponseHeaders.SetRawServer(Constants.ServerName, BytesServer);
}
}
public void Uninitialize()
{
RequestHeaders.Reset();
ResponseHeaders.Reset();
}
}
internal class Streams
{
public readonly FrameRequestStream RequestBody;
public readonly FrameResponseStream ResponseBody;
public readonly FrameDuplexStream DuplexStream;
public Streams()
{
RequestBody = new FrameRequestStream();
ResponseBody = new FrameResponseStream();
DuplexStream = new FrameDuplexStream(RequestBody, ResponseBody);
}
public void Initialize(IFrameControl frameControl)
{
ResponseBody.Initialize(frameControl);
}
public void Uninitialize()
{
ResponseBody.Uninitialize();
}
}
}

View File

@ -1,20 +0,0 @@
// 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 Microsoft.AspNetCore.Server.Kestrel.Http;
namespace Microsoft.AspNetCore.Server.Kestrel.Infrastructure
{
interface IHttpComponentFactory
{
KestrelServerOptions ServerOptions { get; set; }
Streams CreateStreams(IFrameControl frameControl);
void DisposeStreams(Streams streams);
Headers CreateHeaders(DateHeaderValueManager dateValueManager);
void DisposeHeaders(Headers headers);
}
}

View File

@ -0,0 +1,21 @@
// 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 Microsoft.AspNetCore.Server.Kestrel.Http;
namespace Microsoft.AspNetCore.Server.Kestrel.Infrastructure
{
class Streams
{
public Streams(IFrameControl frameControl)
{
RequestBody = new FrameRequestStream();
ResponseBody = new FrameResponseStream(frameControl);
DuplexStream = new FrameDuplexStream(RequestBody, ResponseBody);
}
public FrameRequestStream RequestBody { get; }
public FrameResponseStream ResponseBody { get; }
public FrameDuplexStream DuplexStream { get; }
}
}

View File

@ -44,8 +44,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel
_applicationLifetime = applicationLifetime;
_logger = loggerFactory.CreateLogger(typeof(KestrelServer).GetTypeInfo().Namespace);
Features = new FeatureCollection();
var componentFactory = new HttpComponentFactory(Options);
Features.Set<IHttpComponentFactory>(componentFactory);
_serverAddresses = new ServerAddressesFeature();
Features.Set<IServerAddressesFeature>(_serverAddresses);
}
@ -65,7 +63,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel
try
{
var componentFactory = Features.Get<IHttpComponentFactory>();
var dateHeaderValueManager = new DateHeaderValueManager();
var trace = new KestrelTrace(_logger);
var engine = new KestrelEngine(new ServiceContext
@ -78,8 +75,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel
Log = trace,
ThreadPool = new LoggingThreadPool(trace),
DateHeaderValueManager = dateHeaderValueManager,
ServerOptions = Options,
HttpComponentFactory = componentFactory
ServerOptions = Options
});
_disposables.Push(engine);

View File

@ -12,24 +12,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel
public IConnectionFilter ConnectionFilter { get; set; }
/// <summary>
/// Gets or sets value that instructs <seealso cref="KestrelServer"/> whether it is safe to
/// pool the Request and Response <seealso cref="System.IO.Stream"/> objects
/// for another request after the Response's OnCompleted callback has fired.
/// When this values is greater than zero, it is not safe to retain references to feature components after this event has fired.
/// Value is zero by default.
/// </summary>
public int MaxPooledStreams { get; set; }
/// <summary>
/// Gets or sets value that instructs <seealso cref="KestrelServer"/> whether it is safe to
/// pool the Request and Response headers
/// for another request after the Response's OnCompleted callback has fired.
/// When this values is greater than zero, it is not safe to retain references to feature components after this event has fired.
/// Value is zero by default.
/// </summary>
public int MaxPooledHeaders { get; set; }
public bool NoDelay { get; set; } = true;
/// <summary>

View File

@ -2,10 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Net;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.Filter;
using Microsoft.AspNetCore.Server.Kestrel.Http;
using Microsoft.AspNetCore.Server.Kestrel.Infrastructure;
@ -25,7 +22,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel
FrameFactory = context.FrameFactory;
DateHeaderValueManager = context.DateHeaderValueManager;
ServerOptions = context.ServerOptions;
HttpComponentFactory = context.HttpComponentFactory;
}
public IApplicationLifetime AppLifetime { get; set; }
@ -39,7 +35,5 @@ namespace Microsoft.AspNetCore.Server.Kestrel
public DateHeaderValueManager DateHeaderValueManager { get; set; }
public KestrelServerOptions ServerOptions { get; set; }
internal IHttpComponentFactory HttpComponentFactory { get; set; }
}
}

View File

@ -170,13 +170,15 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
[Theory]
[MemberData(nameof(ConnectionFilterData))]
public async Task ReuseStreamsOn(ServiceContext testContext)
public async Task HeadersAndStreamsAreReused(ServiceContext testContext)
{
testContext.ServerOptions.MaxPooledStreams = 120;
var streamCount = 0;
var requestHeadersCount = 0;
var responseHeadersCount = 0;
var loopCount = 20;
Stream lastStream = null;
IHeaderDictionary lastRequestHeaders = null;
IHeaderDictionary lastResponseHeaders = null;
using (var server = new TestServer(
context =>
@ -186,6 +188,16 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
lastStream = context.Request.Body;
streamCount++;
}
if (context.Request.Headers != lastRequestHeaders)
{
lastRequestHeaders = context.Request.Headers;
requestHeadersCount++;
}
if (context.Response.Headers != lastResponseHeaders)
{
lastResponseHeaders = context.Response.Headers;
responseHeadersCount++;
}
context.Response.Headers.Clear();
return context.Request.Body.CopyToAsync(context.Response.Body);
},
@ -208,49 +220,8 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
}
Assert.Equal(1, streamCount);
}
}
[Theory]
[MemberData(nameof(ConnectionFilterData))]
public async Task ReuseStreamsOff(ServiceContext testContext)
{
testContext.ServerOptions.MaxPooledStreams = 0;
var streamCount = 0;
var loopCount = 20;
Stream lastStream = null;
using (var server = new TestServer(
context =>
{
if (context.Request.Body != lastStream)
{
lastStream = context.Request.Body;
streamCount++;
}
context.Response.Headers.Clear();
return context.Request.Body.CopyToAsync(context.Response.Body);
},
testContext))
{
using (var connection = new TestConnection(server.Port))
{
var requestData =
Enumerable.Repeat("GET / HTTP/1.1\r\n", loopCount)
.Concat(new[] { "GET / HTTP/1.1\r\nConnection: close\r\n\r\nGoodbye" });
var responseData =
Enumerable.Repeat("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n", loopCount)
.Concat(new[] { "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\nGoodbye" });
await connection.SendEnd(requestData.ToArray());
await connection.ReceiveEnd(responseData.ToArray());
}
Assert.Equal(loopCount + 1, streamCount);
Assert.Equal(1, requestHeadersCount);
Assert.Equal(1, responseHeadersCount);
}
}

View File

@ -6,8 +6,6 @@ using System.Collections.Generic;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Server.Kestrel;
using Microsoft.AspNetCore.Server.Kestrel.Http;
using Microsoft.AspNetCore.Server.Kestrel.Infrastructure;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Primitives;
using Xunit;
@ -27,7 +25,6 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
DateHeaderValueManager = new DateHeaderValueManager(),
ServerAddress = ServerAddress.FromUrl("http://localhost:5000"),
ServerOptions = serverOptions,
HttpComponentFactory = new HttpComponentFactory(serverOptions)
};
var frame = new Frame<object>(application: null, context: connectionContext);
frame.InitializeHeaders();
@ -70,7 +67,6 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
DateHeaderValueManager = new DateHeaderValueManager(),
ServerAddress = ServerAddress.FromUrl("http://localhost:5000"),
ServerOptions = serverOptions,
HttpComponentFactory = new HttpComponentFactory(serverOptions)
};
var frame = new Frame<object>(application: null, context: connectionContext);
frame.InitializeHeaders();

View File

@ -5,6 +5,7 @@ using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Server.Kestrel.Http;
using Microsoft.AspNetCore.Server.KestrelTests.TestHelpers;
using Xunit;
namespace Microsoft.AspNetCore.Server.KestrelTests
@ -14,42 +15,42 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
[Fact]
public void CanReadReturnsFalse()
{
var stream = new FrameResponseStream();
var stream = new FrameResponseStream(new MockFrameControl());
Assert.False(stream.CanRead);
}
[Fact]
public void CanSeekReturnsFalse()
{
var stream = new FrameResponseStream();
var stream = new FrameResponseStream(new MockFrameControl());
Assert.False(stream.CanSeek);
}
[Fact]
public void CanWriteReturnsTrue()
{
var stream = new FrameResponseStream();
var stream = new FrameResponseStream(new MockFrameControl());
Assert.True(stream.CanWrite);
}
[Fact]
public void ReadThrows()
{
var stream = new FrameResponseStream();
var stream = new FrameResponseStream(new MockFrameControl());
Assert.Throws<NotSupportedException>(() => stream.Read(new byte[1], 0, 1));
}
[Fact]
public void ReadByteThrows()
{
var stream = new FrameResponseStream();
var stream = new FrameResponseStream(new MockFrameControl());
Assert.Throws<NotSupportedException>(() => stream.ReadByte());
}
[Fact]
public async Task ReadAsyncThrows()
{
var stream = new FrameResponseStream();
var stream = new FrameResponseStream(new MockFrameControl());
await Assert.ThrowsAsync<NotSupportedException>(() => stream.ReadAsync(new byte[1], 0, 1));
}
@ -57,7 +58,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
[Fact]
public void BeginReadThrows()
{
var stream = new FrameResponseStream();
var stream = new FrameResponseStream(new MockFrameControl());
Assert.Throws<NotSupportedException>(() => stream.BeginRead(new byte[1], 0, 1, null, null));
}
#endif
@ -65,28 +66,28 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
[Fact]
public void SeekThrows()
{
var stream = new FrameResponseStream();
var stream = new FrameResponseStream(new MockFrameControl());
Assert.Throws<NotSupportedException>(() => stream.Seek(0, SeekOrigin.Begin));
}
[Fact]
public void LengthThrows()
{
var stream = new FrameResponseStream();
var stream = new FrameResponseStream(new MockFrameControl());
Assert.Throws<NotSupportedException>(() => stream.Length);
}
[Fact]
public void SetLengthThrows()
{
var stream = new FrameResponseStream();
var stream = new FrameResponseStream(new MockFrameControl());
Assert.Throws<NotSupportedException>(() => stream.SetLength(0));
}
[Fact]
public void PositionThrows()
{
var stream = new FrameResponseStream();
var stream = new FrameResponseStream(new MockFrameControl());
Assert.Throws<NotSupportedException>(() => stream.Position);
Assert.Throws<NotSupportedException>(() => stream.Position = 0);
}

View File

@ -20,7 +20,8 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
using (var input = new TestInput())
{
var body = MessageBody.For("HTTP/1.0", new FrameRequestHeaders(), input.FrameContext);
var stream = new FrameRequestStream().StartAcceptingReads(body);
var stream = new FrameRequestStream();
stream.StartAcceptingReads(body);
input.Add("Hello", true);
@ -40,7 +41,8 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
using (var input = new TestInput())
{
var body = MessageBody.For("HTTP/1.0", new FrameRequestHeaders(), input.FrameContext);
var stream = new FrameRequestStream().StartAcceptingReads(body);
var stream = new FrameRequestStream();
stream.StartAcceptingReads(body);
input.Add("Hello", true);
@ -60,7 +62,8 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
using (var input = new TestInput())
{
var body = MessageBody.For("HTTP/1.0", new FrameRequestHeaders(), input.FrameContext);
var stream = new FrameRequestStream().StartAcceptingReads(body);
var stream = new FrameRequestStream();
stream.StartAcceptingReads(body);
// Input needs to be greater than 4032 bytes to allocate a block not backed by a slab.
var largeInput = new string('a', 8192);

View File

@ -0,0 +1,35 @@
// 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.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Server.Kestrel.Http;
namespace Microsoft.AspNetCore.Server.KestrelTests.TestHelpers
{
public class MockFrameControl : IFrameControl
{
public void Flush()
{
}
public Task FlushAsync(CancellationToken cancellationToken)
{
return Task.FromResult(0);
}
public void ProduceContinue()
{
}
public void Write(ArraySegment<byte> data)
{
}
public Task WriteAsync(ArraySegment<byte> data, CancellationToken cancellationToken)
{
return Task.FromResult(0);
}
}
}

View File

@ -7,7 +7,6 @@ using Microsoft.AspNetCore.Server.Kestrel;
using Microsoft.AspNetCore.Server.Kestrel.Filter;
using Microsoft.AspNetCore.Server.Kestrel.Http;
using Microsoft.AspNetCore.Server.Kestrel.Infrastructure;
using Microsoft.Extensions.Configuration;
namespace Microsoft.AspNetCore.Server.KestrelTests
{
@ -24,8 +23,6 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
ServerOptions = new KestrelServerOptions();
ServerOptions.ShutdownTimeout = TimeSpan.FromSeconds(5);
HttpComponentFactory = new HttpComponentFactory(ServerOptions);
}
public TestServiceContext(IConnectionFilter filter)