From 9fa9c45eda32528f34e0ca564e6fe7581fc6d653 Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Tue, 15 Dec 2015 06:34:30 +0000 Subject: [PATCH] ReuseStreams config and tests --- .../Http/Frame.cs | 9 +- .../Http/FrameOfT.cs | 9 ++ .../Http/FrameRequestStream.cs | 33 ++--- .../Http/FrameResponseStream.cs | 33 ++--- .../Http/FrameStreamState.cs | 12 ++ .../IKestrelServerInformation.cs | 13 ++ .../KestrelServer.cs | 3 +- .../KestrelServerInformation.cs | 16 +++ .../ServiceContext.cs | 3 + .../ReuseStreamsTests.cs | 127 ++++++++++++++++++ .../KestrelServerInformationTests.cs | 25 ++++ 11 files changed, 239 insertions(+), 44 deletions(-) create mode 100644 src/Microsoft.AspNet.Server.Kestrel/Http/FrameStreamState.cs create mode 100644 test/Microsoft.AspNet.Server.Kestrel.FunctionalTests/ReuseStreamsTests.cs diff --git a/src/Microsoft.AspNet.Server.Kestrel/Http/Frame.cs b/src/Microsoft.AspNet.Server.Kestrel/Http/Frame.cs index 5dfca52bb9..74f3ef6889 100644 --- a/src/Microsoft.AspNet.Server.Kestrel/Http/Frame.cs +++ b/src/Microsoft.AspNet.Server.Kestrel/Http/Frame.cs @@ -87,9 +87,12 @@ namespace Microsoft.AspNet.Server.Kestrel.Http _localEndPoint = localEndPoint; _prepareRequest = prepareRequest; _pathBase = context.ServerAddress.PathBase; - _requestBody = new FrameRequestStream(); - _responseBody = new FrameResponseStream(this); - _duplexStream = new FrameDuplexStream(_requestBody, _responseBody); + if (ReuseStreams) + { + _requestBody = new FrameRequestStream(); + _responseBody = new FrameResponseStream(this); + _duplexStream = new FrameDuplexStream(_requestBody, _responseBody); + } FrameControl = this; Reset(); diff --git a/src/Microsoft.AspNet.Server.Kestrel/Http/FrameOfT.cs b/src/Microsoft.AspNet.Server.Kestrel/Http/FrameOfT.cs index d9afe93d92..32b067fc76 100644 --- a/src/Microsoft.AspNet.Server.Kestrel/Http/FrameOfT.cs +++ b/src/Microsoft.AspNet.Server.Kestrel/Http/FrameOfT.cs @@ -64,6 +64,15 @@ namespace Microsoft.AspNet.Server.Kestrel.Http { var messageBody = MessageBody.For(HttpVersion, _requestHeaders, this); _keepAlive = messageBody.RequestKeepAlive; + + // _duplexStream may be null if flag switched while running + if (!ReuseStreams || _duplexStream == null) + { + _requestBody = new FrameRequestStream(); + _responseBody = new FrameResponseStream(this); + _duplexStream = new FrameDuplexStream(_requestBody, _responseBody); + } + RequestBody = _requestBody.StartAcceptingReads(messageBody); ResponseBody = _responseBody.StartAcceptingWrites(); DuplexStream = _duplexStream; diff --git a/src/Microsoft.AspNet.Server.Kestrel/Http/FrameRequestStream.cs b/src/Microsoft.AspNet.Server.Kestrel/Http/FrameRequestStream.cs index 0c8ea64e81..f9ff5a4197 100644 --- a/src/Microsoft.AspNet.Server.Kestrel/Http/FrameRequestStream.cs +++ b/src/Microsoft.AspNet.Server.Kestrel/Http/FrameRequestStream.cs @@ -11,11 +11,11 @@ namespace Microsoft.AspNet.Server.Kestrel.Http public class FrameRequestStream : Stream { private MessageBody _body; - private StreamState _state; + private FrameStreamState _state; public FrameRequestStream() { - _state = StreamState.Closed; + _state = FrameStreamState.Closed; } public override bool CanRead { get { return true; } } @@ -115,9 +115,9 @@ namespace Microsoft.AspNet.Server.Kestrel.Http public Stream StartAcceptingReads(MessageBody body) { // Only start if not aborted - if (_state == StreamState.Closed) + if (_state == FrameStreamState.Closed) { - _state = StreamState.Open; + _state = FrameStreamState.Open; _body = body; } return this; @@ -125,14 +125,14 @@ namespace Microsoft.AspNet.Server.Kestrel.Http public void PauseAcceptingReads() { - _state = StreamState.Closed; + _state = FrameStreamState.Closed; } public void ResumeAcceptingReads() { - if (_state == StreamState.Closed) + if (_state == FrameStreamState.Closed) { - _state = StreamState.Open; + _state = FrameStreamState.Open; } } @@ -140,7 +140,7 @@ namespace Microsoft.AspNet.Server.Kestrel.Http { // Can't use dispose (or close) as can be disposed too early by user code // As exampled in EngineTests.ZeroContentLengthNotSetAutomaticallyForCertainStatusCodes - _state = StreamState.Closed; + _state = FrameStreamState.Closed; _body = null; } @@ -148,9 +148,9 @@ namespace Microsoft.AspNet.Server.Kestrel.Http { // We don't want to throw an ODE until the app func actually completes. // If the request is aborted, we throw an IOException instead. - if (_state != StreamState.Closed) + if (_state != FrameStreamState.Closed) { - _state = StreamState.Aborted; + _state = FrameStreamState.Aborted; } } @@ -158,20 +158,13 @@ namespace Microsoft.AspNet.Server.Kestrel.Http { switch (_state) { - case StreamState.Open: + case FrameStreamState.Open: return; - case StreamState.Closed: + case FrameStreamState.Closed: throw new ObjectDisposedException(nameof(FrameRequestStream)); - case StreamState.Aborted: + case FrameStreamState.Aborted: throw new IOException("The request has been aborted."); } } - - private enum StreamState - { - Open, - Closed, - Aborted - } } } diff --git a/src/Microsoft.AspNet.Server.Kestrel/Http/FrameResponseStream.cs b/src/Microsoft.AspNet.Server.Kestrel/Http/FrameResponseStream.cs index 9d0658f89b..e544723af4 100644 --- a/src/Microsoft.AspNet.Server.Kestrel/Http/FrameResponseStream.cs +++ b/src/Microsoft.AspNet.Server.Kestrel/Http/FrameResponseStream.cs @@ -11,12 +11,12 @@ namespace Microsoft.AspNet.Server.Kestrel.Http class FrameResponseStream : Stream { private readonly FrameContext _context; - private StreamState _state; + private FrameStreamState _state; public FrameResponseStream(FrameContext context) { _context = context; - _state = StreamState.Closed; + _state = FrameStreamState.Closed; } public override bool CanRead => false; @@ -81,9 +81,9 @@ namespace Microsoft.AspNet.Server.Kestrel.Http public Stream StartAcceptingWrites() { // Only start if not aborted - if (_state == StreamState.Closed) + if (_state == FrameStreamState.Closed) { - _state = StreamState.Open; + _state = FrameStreamState.Open; } return this; @@ -91,14 +91,14 @@ namespace Microsoft.AspNet.Server.Kestrel.Http public void PauseAcceptingWrites() { - _state = StreamState.Closed; + _state = FrameStreamState.Closed; } public void ResumeAcceptingWrites() { - if (_state == StreamState.Closed) + if (_state == FrameStreamState.Closed) { - _state = StreamState.Open; + _state = FrameStreamState.Open; } } @@ -106,16 +106,16 @@ namespace Microsoft.AspNet.Server.Kestrel.Http { // Can't use dispose (or close) as can be disposed too early by user code // As exampled in EngineTests.ZeroContentLengthNotSetAutomaticallyForCertainStatusCodes - _state = StreamState.Closed; + _state = FrameStreamState.Closed; } public void Abort() { // We don't want to throw an ODE until the app func actually completes. // If the request is aborted, we throw an IOException instead. - if (_state != StreamState.Closed) + if (_state != FrameStreamState.Closed) { - _state = StreamState.Aborted; + _state = FrameStreamState.Aborted; } } @@ -123,20 +123,13 @@ namespace Microsoft.AspNet.Server.Kestrel.Http { switch (_state) { - case StreamState.Open: + case FrameStreamState.Open: return; - case StreamState.Closed: + case FrameStreamState.Closed: throw new ObjectDisposedException(nameof(FrameResponseStream)); - case StreamState.Aborted: + case FrameStreamState.Aborted: throw new IOException("The request has been aborted."); } } - - private enum StreamState - { - Open, - Closed, - Aborted - } } } diff --git a/src/Microsoft.AspNet.Server.Kestrel/Http/FrameStreamState.cs b/src/Microsoft.AspNet.Server.Kestrel/Http/FrameStreamState.cs new file mode 100644 index 0000000000..12c393ee1a --- /dev/null +++ b/src/Microsoft.AspNet.Server.Kestrel/Http/FrameStreamState.cs @@ -0,0 +1,12 @@ +// 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. + +namespace Microsoft.AspNet.Server.Kestrel.Http +{ + enum FrameStreamState + { + Open, + Closed, + Aborted + } +} diff --git a/src/Microsoft.AspNet.Server.Kestrel/IKestrelServerInformation.cs b/src/Microsoft.AspNet.Server.Kestrel/IKestrelServerInformation.cs index 870fe2e533..3a634afae8 100644 --- a/src/Microsoft.AspNet.Server.Kestrel/IKestrelServerInformation.cs +++ b/src/Microsoft.AspNet.Server.Kestrel/IKestrelServerInformation.cs @@ -11,6 +11,19 @@ namespace Microsoft.AspNet.Server.Kestrel bool NoDelay { get; set; } + /// + /// Gets or sets a flag that instructs whether it is safe to + /// reuse the Request and Response objects + /// for another request after the Response's OnCompleted callback has fired. + /// When this is set to true it is not safe to retain references to these streams after this event has fired. + /// It is false by default. + /// + /// + /// When this is set to true it is not safe to retain references to these streams after this event has fired. + /// It is false by default. + /// + bool ReuseStreams { get; set; } + IConnectionFilter ConnectionFilter { get; set; } } } diff --git a/src/Microsoft.AspNet.Server.Kestrel/KestrelServer.cs b/src/Microsoft.AspNet.Server.Kestrel/KestrelServer.cs index 3a1295de76..34cef6394b 100644 --- a/src/Microsoft.AspNet.Server.Kestrel/KestrelServer.cs +++ b/src/Microsoft.AspNet.Server.Kestrel/KestrelServer.cs @@ -67,7 +67,8 @@ namespace Microsoft.AspNet.Server.Kestrel ThreadPool = new LoggingThreadPool(trace), DateHeaderValueManager = dateHeaderValueManager, ConnectionFilter = information.ConnectionFilter, - NoDelay = information.NoDelay + NoDelay = information.NoDelay, + ReuseStreams = information.ReuseStreams }); _disposables.Push(engine); diff --git a/src/Microsoft.AspNet.Server.Kestrel/KestrelServerInformation.cs b/src/Microsoft.AspNet.Server.Kestrel/KestrelServerInformation.cs index f9f4ae8c8e..8ff73cc03e 100644 --- a/src/Microsoft.AspNet.Server.Kestrel/KestrelServerInformation.cs +++ b/src/Microsoft.AspNet.Server.Kestrel/KestrelServerInformation.cs @@ -22,6 +22,7 @@ namespace Microsoft.AspNet.Server.Kestrel Addresses = GetAddresses(configuration); ThreadCount = GetThreadCount(configuration); NoDelay = GetNoDelay(configuration); + ReuseStreams = GetReuseStreams(configuration); } public ICollection Addresses { get; } @@ -30,6 +31,8 @@ namespace Microsoft.AspNet.Server.Kestrel public bool NoDelay { get; set; } + public bool ReuseStreams { get; set; } + public IConnectionFilter ConnectionFilter { get; set; } private static int ProcessorThreadCount @@ -107,5 +110,18 @@ namespace Microsoft.AspNet.Server.Kestrel return true; } + + private static bool GetReuseStreams(IConfiguration configuration) + { + var reuseStreamsString = configuration["kestrel.reuseStreams"]; + + bool reuseStreams; + if (bool.TryParse(reuseStreamsString, out reuseStreams)) + { + return reuseStreams; + } + + return false; + } } } diff --git a/src/Microsoft.AspNet.Server.Kestrel/ServiceContext.cs b/src/Microsoft.AspNet.Server.Kestrel/ServiceContext.cs index 31eeea898d..4bab512ced 100644 --- a/src/Microsoft.AspNet.Server.Kestrel/ServiceContext.cs +++ b/src/Microsoft.AspNet.Server.Kestrel/ServiceContext.cs @@ -26,6 +26,7 @@ namespace Microsoft.AspNet.Server.Kestrel DateHeaderValueManager = context.DateHeaderValueManager; ConnectionFilter = context.ConnectionFilter; NoDelay = context.NoDelay; + ReuseStreams = context.ReuseStreams; } public IApplicationLifetime AppLifetime { get; set; } @@ -41,5 +42,7 @@ namespace Microsoft.AspNet.Server.Kestrel public IConnectionFilter ConnectionFilter { get; set; } public bool NoDelay { get; set; } + + public bool ReuseStreams { get; set; } } } diff --git a/test/Microsoft.AspNet.Server.Kestrel.FunctionalTests/ReuseStreamsTests.cs b/test/Microsoft.AspNet.Server.Kestrel.FunctionalTests/ReuseStreamsTests.cs new file mode 100644 index 0000000000..d61a2759e2 --- /dev/null +++ b/test/Microsoft.AspNet.Server.Kestrel.FunctionalTests/ReuseStreamsTests.cs @@ -0,0 +1,127 @@ +// 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.Collections.Generic; +using System.IO; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Hosting; +using Microsoft.AspNet.Http.Features; +using Microsoft.Extensions.Configuration; +using Xunit; + +namespace Microsoft.AspNet.Server.Kestrel.FunctionalTests +{ + public class ReuseStreamsTests + { + [Fact] + public async Task ReuseStreamsOn() + { + var streamCount = 0; + var loopCount = 20; + Stream lastStream = null; + var config = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + { "server.urls", "http://localhost:8801/" }, + { "kestrel.reuseStreams", "true" } + }) + .Build(); + + var hostBuilder = new WebHostBuilder(config); + hostBuilder.UseServerFactory("Microsoft.AspNet.Server.Kestrel"); + hostBuilder.UseStartup(app => + { + var serverInfo = app.ServerFeatures.Get(); + app.Run(context => + { + if (context.Request.Body != lastStream) + { + lastStream = context.Request.Body; + streamCount++; + } + return context.Request.Body.CopyToAsync(context.Response.Body); + }); + }); + + using (var app = hostBuilder.Build().Start()) + { + using (var client = new HttpClient()) + { + for (int i = 0; i < loopCount; i++) + { + var content = $"{i} Hello World {i}"; + var request = new HttpRequestMessage() + { + RequestUri = new Uri("http://localhost:8801/"), + Method = HttpMethod.Post, + Content = new StringContent(content) + }; + request.Headers.Add("Connection", new string[] { "Keep-Alive" }); + var responseMessage = await client.SendAsync(request); + var result = await responseMessage.Content.ReadAsStringAsync(); + Assert.Equal(content, result); + } + } + } + + Assert.True(streamCount < loopCount); + } + + [Fact] + public async Task ReuseStreamsOff() + { + var streamCount = 0; + var loopCount = 20; + Stream lastStream = null; + var config = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + { "server.urls", "http://localhost:8802/" }, + { "kestrel.reuseStreams", "false" } + }) + .Build(); + + var hostBuilder = new WebHostBuilder(config); + hostBuilder.UseServerFactory("Microsoft.AspNet.Server.Kestrel"); + hostBuilder.UseStartup(app => + { + var serverInfo = app.ServerFeatures.Get(); + app.Run(context => + { + if (context.Request.Body != lastStream) + { + lastStream = context.Request.Body; + streamCount++; + } + return context.Request.Body.CopyToAsync(context.Response.Body); + }); + }); + + using (var app = hostBuilder.Build().Start()) + { + using (var client = new HttpClient()) + { + for (int i = 0; i < loopCount; i++) + { + var content = $"{i} Hello World {i}"; + var request = new HttpRequestMessage() + { + RequestUri = new Uri("http://localhost:8802/"), + Method = HttpMethod.Post, + Content = new StringContent(content) + }; + request.Headers.Add("Connection", new string[] { "Keep-Alive" }); + var responseMessage = await client.SendAsync(request); + var result = await responseMessage.Content.ReadAsStringAsync(); + Assert.Equal(content, result); + } + } + } + + Assert.Equal(loopCount, streamCount); + } + } +} diff --git a/test/Microsoft.AspNet.Server.KestrelTests/KestrelServerInformationTests.cs b/test/Microsoft.AspNet.Server.KestrelTests/KestrelServerInformationTests.cs index 2513d93e84..e2376c0d94 100644 --- a/test/Microsoft.AspNet.Server.KestrelTests/KestrelServerInformationTests.cs +++ b/test/Microsoft.AspNet.Server.KestrelTests/KestrelServerInformationTests.cs @@ -85,6 +85,31 @@ namespace Microsoft.AspNet.Server.KestrelTests Assert.False(information.NoDelay); } + [Theory] + [InlineData(null, false)] + [InlineData("", false)] + [InlineData("false", false)] + [InlineData("False", false)] + [InlineData("true", true)] + [InlineData("True", true)] + [InlineData("Foo", false)] + [InlineData("FooBar", false)] + public void SetReuseStreamsUsingConfiguration(string input, bool expected) + { + var values = new Dictionary + { + { "kestrel.reuseStreams", input } + }; + + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(values) + .Build(); + + var information = new KestrelServerInformation(configuration); + + Assert.Equal(expected, information.ReuseStreams); + } + private static int Clamp(int value, int min, int max) { return value < min ? min : value > max ? max : value;