ReuseStreams config and tests

This commit is contained in:
Ben Adams 2015-12-15 06:34:30 +00:00
parent 841ec73497
commit 9fa9c45eda
11 changed files with 239 additions and 44 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,6 +11,19 @@ namespace Microsoft.AspNet.Server.Kestrel
bool NoDelay { get; set; }
/// <summary>
/// Gets or sets a flag that instructs <seealso cref="KestrelServer"/> whether it is safe to
/// reuse the Request and Response <seealso cref="System.IO.Stream"/> 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.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
bool ReuseStreams { get; set; }
IConnectionFilter ConnectionFilter { get; set; }
}
}

View File

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

View File

@ -22,6 +22,7 @@ namespace Microsoft.AspNet.Server.Kestrel
Addresses = GetAddresses(configuration);
ThreadCount = GetThreadCount(configuration);
NoDelay = GetNoDelay(configuration);
ReuseStreams = GetReuseStreams(configuration);
}
public ICollection<string> 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;
}
}
}

View File

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

View File

@ -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<string, string>
{
{ "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<IKestrelServerInformation>();
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<string, string>
{
{ "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<IKestrelServerInformation>();
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);
}
}
}

View File

@ -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<string, string>
{
{ "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;