Improve keep-alive timeout.

- Track time more accurately
- Control timeout in Connection instead of Frame
This commit is contained in:
Cesar Blum Silveira 2016-09-12 11:40:30 -07:00
parent 84efe624a6
commit 1a273f5a34
16 changed files with 304 additions and 163 deletions

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -38,6 +39,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
private TaskCompletionSource<object> _socketClosedTcs = new TaskCompletionSource<object>(); private TaskCompletionSource<object> _socketClosedTcs = new TaskCompletionSource<object>();
private BufferSizeControl _bufferSizeControl; private BufferSizeControl _bufferSizeControl;
private long _lastTimestamp;
private long _timeoutTimestamp = long.MaxValue;
public Connection(ListenerContext context, UvStreamHandle socket) : base(context) public Connection(ListenerContext context, UvStreamHandle socket) : base(context)
{ {
_socket = socket; _socket = socket;
@ -62,6 +66,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
} }
_frame = FrameFactory(this); _frame = FrameFactory(this);
_lastTimestamp = Thread.Loop.Now();
} }
// Internal for testing // Internal for testing
@ -156,9 +162,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
} }
// Called on Libuv thread // Called on Libuv thread
public void Tick() public void Tick(long timestamp)
{ {
_frame.Tick(); if (timestamp > _timeoutTimestamp)
{
StopAsync();
}
Interlocked.Exchange(ref _lastTimestamp, timestamp);
} }
private void ApplyConnectionFilter() private void ApplyConnectionFilter()
@ -282,9 +293,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
} }
} }
void IConnectionControl.Stop() void IConnectionControl.SetTimeout(long milliseconds)
{ {
StopAsync(); Debug.Assert(_timeoutTimestamp == long.MaxValue, "Concurrent timeouts are not supported");
// Add KestrelThread.HeartbeatMilliseconds extra milliseconds since this can be called right before the next heartbeat.
Interlocked.Exchange(ref _timeoutTimestamp, _lastTimestamp + milliseconds + KestrelThread.HeartbeatMilliseconds);
}
void IConnectionControl.CancelTimeout()
{
Interlocked.Exchange(ref _timeoutTimestamp, long.MaxValue);
} }
private static unsafe string GenerateConnectionId(long id) private static unsafe string GenerateConnectionId(long id)

View File

@ -69,7 +69,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
private int _remainingRequestHeadersBytesAllowed; private int _remainingRequestHeadersBytesAllowed;
private int _requestHeadersParsed; private int _requestHeadersParsed;
private int _secondsSinceLastRequest; protected readonly long _keepAliveMilliseconds;
public Frame(ConnectionContext context) public Frame(ConnectionContext context)
: base(context) : base(context)
@ -77,6 +77,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
_pathBase = context.ServerAddress.PathBase; _pathBase = context.ServerAddress.PathBase;
FrameControl = this; FrameControl = this;
_keepAliveMilliseconds = (long)ServerOptions.Limits.KeepAliveTimeout.TotalMilliseconds;
} }
public string ConnectionIdFeature { get; set; } public string ConnectionIdFeature { get; set; }
@ -805,6 +806,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
return RequestLineStatus.Empty; return RequestLineStatus.Empty;
} }
ConnectionControl.CancelTimeout();
_requestProcessingStatus = RequestProcessingStatus.RequestStarted; _requestProcessingStatus = RequestProcessingStatus.RequestStarted;
int bytesScanned; int bytesScanned;
@ -1269,25 +1272,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
Log.ApplicationError(ConnectionId, ex); Log.ApplicationError(ConnectionId, ex);
} }
public void Tick()
{
// we're in between requests and not about to start processing a new one
if (_requestProcessingStatus == RequestProcessingStatus.RequestPending && !SocketInput.IsCompleted)
{
if (_secondsSinceLastRequest > ServerOptions.Limits.KeepAliveTimeout.TotalSeconds)
{
ConnectionControl.Stop();
}
_secondsSinceLastRequest++;
}
}
public void RequestFinished()
{
_secondsSinceLastRequest = 0;
}
public enum RequestLineStatus public enum RequestLineStatus
{ {
Empty, Empty,

View File

@ -32,6 +32,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{ {
while (!_requestProcessingStopping) while (!_requestProcessingStopping)
{ {
ConnectionControl.SetTimeout(_keepAliveMilliseconds);
while (!_requestProcessingStopping && TakeStartLine(SocketInput) != RequestLineStatus.Done) while (!_requestProcessingStopping && TakeStartLine(SocketInput) != RequestLineStatus.Done)
{ {
if (SocketInput.CheckFinOrThrow()) if (SocketInput.CheckFinOrThrow())

View File

@ -8,6 +8,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
void Pause(); void Pause();
void Resume(); void Resume();
void End(ProduceEndType endType); void End(ProduceEndType endType);
void Stop(); void SetTimeout(long milliseconds);
void CancelTimeout();
} }
} }

View File

@ -172,7 +172,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
var limit = buffer.Array == null ? inputLengthLimit : Math.Min(buffer.Count, inputLengthLimit); var limit = buffer.Array == null ? inputLengthLimit : Math.Min(buffer.Count, inputLengthLimit);
if (limit == 0) if (limit == 0)
{ {
_context.RequestFinished();
return new ValueTask<int>(0); return new ValueTask<int>(0);
} }
@ -189,11 +188,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
_context.RejectRequest(RequestRejectionReason.UnexpectedEndOfRequestContent); _context.RejectRequest(RequestRejectionReason.UnexpectedEndOfRequestContent);
} }
if (_inputLength == 0)
{
_context.RequestFinished();
}
return new ValueTask<int>(actual); return new ValueTask<int>(actual);
} }
else else
@ -212,11 +206,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
_context.RejectRequest(RequestRejectionReason.UnexpectedEndOfRequestContent); _context.RejectRequest(RequestRejectionReason.UnexpectedEndOfRequestContent);
} }
if (_inputLength == 0)
{
_context.RequestFinished();
}
return actual; return actual;
} }
} }
@ -368,8 +357,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
_mode = Mode.Complete; _mode = Mode.Complete;
} }
_context.RequestFinished();
return 0; return 0;
} }

View File

@ -19,6 +19,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal
/// </summary> /// </summary>
public class KestrelThread public class KestrelThread
{ {
public const long HeartbeatMilliseconds = 1000;
private static readonly Action<object, object> _postCallbackAdapter = (callback, state) => ((Action<object>)callback).Invoke(state); private static readonly Action<object, object> _postCallbackAdapter = (callback, state) => ((Action<object>)callback).Invoke(state);
private static readonly Action<object, object> _postAsyncCallbackAdapter = (callback, state) => ((Action<object>)callback).Invoke(state); private static readonly Action<object, object> _postAsyncCallbackAdapter = (callback, state) => ((Action<object>)callback).Invoke(state);
@ -27,9 +29,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal
// otherwise it needs to wait till the next pass of the libuv loop // otherwise it needs to wait till the next pass of the libuv loop
private readonly int _maxLoops = 8; private readonly int _maxLoops = 8;
// how often the heartbeat timer will tick connections
private const int _heartbeatMilliseconds = 1000;
private readonly KestrelEngine _engine; private readonly KestrelEngine _engine;
private readonly IApplicationLifetime _appLifetime; private readonly IApplicationLifetime _appLifetime;
private readonly Thread _thread; private readonly Thread _thread;
@ -280,7 +279,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal
_loop.Init(_engine.Libuv); _loop.Init(_engine.Libuv);
_post.Init(_loop, OnPost, EnqueueCloseHandle); _post.Init(_loop, OnPost, EnqueueCloseHandle);
_heartbeatTimer.Init(_loop, EnqueueCloseHandle); _heartbeatTimer.Init(_loop, EnqueueCloseHandle);
_heartbeatTimer.Start(OnHeartbeat, timeout: 1000, repeat: 1000); _heartbeatTimer.Start(OnHeartbeat, timeout: HeartbeatMilliseconds, repeat: HeartbeatMilliseconds);
_initCompleted = true; _initCompleted = true;
tcs.SetResult(0); tcs.SetResult(0);
} }
@ -337,10 +336,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal
private void OnHeartbeat(UvTimerHandle timer) private void OnHeartbeat(UvTimerHandle timer)
{ {
var now = Loop.Now();
Walk(ptr => Walk(ptr =>
{ {
var handle = UvMemory.FromIntPtr<UvHandle>(ptr); var handle = UvMemory.FromIntPtr<UvHandle>(ptr);
(handle as UvStreamHandle)?.Connection?.Tick(); (handle as UvStreamHandle)?.Connection?.Tick(now);
}); });
} }

View File

@ -56,6 +56,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Networking
_uv_timer_init = NativeMethods.uv_timer_init; _uv_timer_init = NativeMethods.uv_timer_init;
_uv_timer_start = NativeMethods.uv_timer_start; _uv_timer_start = NativeMethods.uv_timer_start;
_uv_timer_stop = NativeMethods.uv_timer_stop; _uv_timer_stop = NativeMethods.uv_timer_stop;
_uv_now = NativeMethods.uv_now;
} }
// Second ctor that doesn't set any fields only to be used by MockLibuv // Second ctor that doesn't set any fields only to be used by MockLibuv
@ -434,6 +435,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Networking
ThrowIfErrored(_uv_timer_stop(handle)); ThrowIfErrored(_uv_timer_stop(handle));
} }
protected Func<UvLoopHandle, long> _uv_now;
unsafe public long now(UvLoopHandle loop)
{
loop.Validate();
return _uv_now(loop);
}
public delegate int uv_tcp_getsockname_func(UvTcpHandle handle, out SockAddr addr, ref int namelen); public delegate int uv_tcp_getsockname_func(UvTcpHandle handle, out SockAddr addr, ref int namelen);
protected uv_tcp_getsockname_func _uv_tcp_getsockname; protected uv_tcp_getsockname_func _uv_tcp_getsockname;
public void tcp_getsockname(UvTcpHandle handle, out SockAddr addr, ref int namelen) public void tcp_getsockname(UvTcpHandle handle, out SockAddr addr, ref int namelen)
@ -640,6 +648,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Networking
[DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)]
unsafe public static extern int uv_timer_stop(UvTimerHandle handle); unsafe public static extern int uv_timer_stop(UvTimerHandle handle);
[DllImport("libuv", CallingConvention = CallingConvention.Cdecl)]
unsafe public static extern long uv_now(UvLoopHandle loop);
[DllImport("WS2_32.dll", CallingConvention = CallingConvention.Winapi)] [DllImport("WS2_32.dll", CallingConvention = CallingConvention.Winapi)]
unsafe public static extern int WSAIoctl( unsafe public static extern int WSAIoctl(
IntPtr socket, IntPtr socket,

View File

@ -33,6 +33,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Networking
_uv.stop(this); _uv.stop(this);
} }
public long Now()
{
return _uv.now(this);
}
unsafe protected override bool ReleaseHandle() unsafe protected override bool ReleaseHandle()
{ {
var memory = handle; var memory = handle;

View File

@ -23,8 +23,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel
// Matches the default LimitRequestFields in Apache httpd. // Matches the default LimitRequestFields in Apache httpd.
private int _maxRequestHeaderCount = 100; private int _maxRequestHeaderCount = 100;
// Matches the default http.sys keep-alive timouet. // Matches the default http.sys connection timeout.
private TimeSpan _keepAliveTimeout = TimeSpan.FromMinutes(2); private TimeSpan _connectionTimeout = TimeSpan.FromMinutes(2);
/// <summary> /// <summary>
/// Gets or sets the maximum size of the response buffer before write /// Gets or sets the maximum size of the response buffer before write
@ -146,17 +146,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel
/// Gets or sets the keep-alive timeout. /// Gets or sets the keep-alive timeout.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Defaults to 2 minutes. Timeout granularity is in seconds. Sub-second values will be rounded to the next second. /// Defaults to 2 minutes.
/// </remarks> /// </remarks>
public TimeSpan KeepAliveTimeout public TimeSpan KeepAliveTimeout
{ {
get get
{ {
return _keepAliveTimeout; return _connectionTimeout;
} }
set set
{ {
_keepAliveTimeout = TimeSpan.FromSeconds(Math.Ceiling(value.TotalSeconds)); _connectionTimeout = value;
} }
} }
} }

View File

@ -3,8 +3,11 @@
using System; using System;
using System.IO; using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Testing; using Microsoft.AspNetCore.Testing;
using Xunit; using Xunit;
@ -13,23 +16,25 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
public class KeepAliveTimeoutTests public class KeepAliveTimeoutTests
{ {
private static readonly TimeSpan KeepAliveTimeout = TimeSpan.FromSeconds(10); private static readonly TimeSpan KeepAliveTimeout = TimeSpan.FromSeconds(10);
private static readonly int LongDelay = (int)TimeSpan.FromSeconds(30).TotalMilliseconds; private static readonly TimeSpan LongDelay = TimeSpan.FromSeconds(30);
private static readonly int ShortDelay = LongDelay / 10; private static readonly TimeSpan ShortDelay = TimeSpan.FromSeconds(LongDelay.TotalSeconds / 10);
[Fact] [Fact]
public async Task TestKeepAliveTimeout() public async Task TestKeepAliveTimeout()
{ {
using (var server = CreateServer()) var longRunningCancellationTokenSource = new CancellationTokenSource();
var upgradeCancellationTokenSource = new CancellationTokenSource();
using (var server = CreateServer(longRunningCancellationTokenSource.Token, upgradeCancellationTokenSource.Token))
{ {
var tasks = new[] var tasks = new[]
{ {
ConnectionClosedWhenKeepAliveTimeoutExpires(server), ConnectionClosedWhenKeepAliveTimeoutExpires(server),
ConnectionClosedWhenKeepAliveTimeoutExpiresAfterChunkedRequest(server), ConnectionKeptAliveBetweenRequests(server),
KeepAliveTimeoutResetsBetweenContentLengthRequests(server), ConnectionNotTimedOutWhileRequestBeingSent(server),
KeepAliveTimeoutResetsBetweenChunkedRequests(server), ConnectionNotTimedOutWhileAppIsRunning(server, longRunningCancellationTokenSource),
KeepAliveTimeoutNotTriggeredMidContentLengthRequest(server), ConnectionTimesOutWhenOpenedButNoRequestSent(server),
KeepAliveTimeoutNotTriggeredMidChunkedRequest(server), KeepAliveTimeoutDoesNotApplyToUpgradedConnections(server, upgradeCancellationTokenSource)
ConnectionTimesOutWhenOpenedButNoRequestSent(server)
}; };
await Task.WhenAll(tasks); await Task.WhenAll(tasks);
@ -59,110 +64,76 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
} }
} }
private async Task ConnectionClosedWhenKeepAliveTimeoutExpiresAfterChunkedRequest(TestServer server) private async Task ConnectionKeptAliveBetweenRequests(TestServer server)
{ {
using (var connection = new TestConnection(server.Port)) using (var connection = new TestConnection(server.Port))
{ {
for (var i = 0; i < 10; i++)
{
await connection.Send(
"GET / HTTP/1.1",
"",
"");
await Task.Delay(ShortDelay);
}
for (var i = 0; i < 10; i++)
{
await ReceiveResponse(connection, server.Context);
}
}
}
private async Task ConnectionNotTimedOutWhileRequestBeingSent(TestServer server)
{
using (var connection = new TestConnection(server.Port))
{
var cts = new CancellationTokenSource();
cts.CancelAfter(LongDelay);
await connection.Send( await connection.Send(
"POST / HTTP/1.1", "POST / HTTP/1.1",
"Transfer-Encoding: chunked", "Transfer-Encoding: chunked",
"", "",
"5", "hello", "");
"6", " world",
while (!cts.IsCancellationRequested)
{
await connection.Send(
"1",
"a",
"");
}
await connection.Send(
"0", "0",
"", "",
""); "");
await ReceiveResponse(connection, server.Context); await ReceiveResponse(connection, server.Context);
await Task.Delay(LongDelay);
await Assert.ThrowsAsync<IOException>(async () =>
{
await connection.Send(
"GET / HTTP/1.1",
"",
"");
await ReceiveResponse(connection, server.Context);
});
} }
} }
private async Task KeepAliveTimeoutResetsBetweenContentLengthRequests(TestServer server) private async Task ConnectionNotTimedOutWhileAppIsRunning(TestServer server, CancellationTokenSource cts)
{
using (var connection = new TestConnection(server.Port))
{
for (var i = 0; i < 10; i++)
{
await connection.Send(
"GET / HTTP/1.1",
"",
"");
await Task.Delay(ShortDelay);
}
for (var i = 0; i < 10; i++)
{
await ReceiveResponse(connection, server.Context);
}
}
}
private async Task KeepAliveTimeoutResetsBetweenChunkedRequests(TestServer server)
{
using (var connection = new TestConnection(server.Port))
{
for (var i = 0; i < 10; i++)
{
await connection.Send(
"POST / HTTP/1.1",
"Transfer-Encoding: chunked",
"",
"5", "hello",
"6", " world",
"0",
"",
"");
await Task.Delay(ShortDelay);
}
for (var i = 0; i < 10; i++)
{
await ReceiveResponse(connection, server.Context);
}
}
}
private async Task KeepAliveTimeoutNotTriggeredMidContentLengthRequest(TestServer server)
{ {
using (var connection = new TestConnection(server.Port)) using (var connection = new TestConnection(server.Port))
{ {
await connection.Send( await connection.Send(
"POST / HTTP/1.1", "GET /longrunning HTTP/1.1",
"Content-Length: 8",
"", "",
"a"); "");
await Task.Delay(LongDelay); cts.CancelAfter(LongDelay);
await connection.Send("bcdefgh");
await ReceiveResponse(connection, server.Context); while (!cts.IsCancellationRequested)
} {
} await Task.Delay(1000);
}
await ReceiveResponse(connection, server.Context);
private async Task KeepAliveTimeoutNotTriggeredMidChunkedRequest(TestServer server)
{
using (var connection = new TestConnection(server.Port))
{
await connection.Send( await connection.Send(
"POST / HTTP/1.1", "GET / HTTP/1.1",
"Transfer-Encoding: chunked", "",
"", "");
"5", "hello",
"");
await Task.Delay(LongDelay);
await connection.Send(
"6", " world",
"0",
"",
"");
await ReceiveResponse(connection, server.Context); await ReceiveResponse(connection, server.Context);
} }
} }
@ -182,9 +153,34 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
} }
} }
private TestServer CreateServer() private async Task KeepAliveTimeoutDoesNotApplyToUpgradedConnections(TestServer server, CancellationTokenSource cts)
{ {
return new TestServer(App, new TestServiceContext using (var connection = new TestConnection(server.Port))
{
await connection.Send(
"GET /upgrade HTTP/1.1",
"",
"");
await connection.Receive(
"HTTP/1.1 101 Switching Protocols",
"Connection: Upgrade",
$"Date: {server.Context.DateHeaderValue}",
"",
"");
cts.CancelAfter(LongDelay);
while (!cts.IsCancellationRequested)
{
await Task.Delay(1000);
}
await connection.Receive("hello, world");
}
}
private TestServer CreateServer(CancellationToken longRunningCt, CancellationToken upgradeCt)
{
return new TestServer(httpContext => App(httpContext, longRunningCt, upgradeCt), new TestServiceContext
{ {
ServerOptions = new KestrelServerOptions ServerOptions = new KestrelServerOptions
{ {
@ -197,11 +193,36 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
}); });
} }
private async Task App(HttpContext httpContext) private async Task App(HttpContext httpContext, CancellationToken longRunningCt, CancellationToken upgradeCt)
{ {
const string response = "hello, world"; var ct = httpContext.RequestAborted;
httpContext.Response.ContentLength = response.Length;
await httpContext.Response.WriteAsync(response); if (httpContext.Request.Path == "/longrunning")
{
while (!longRunningCt.IsCancellationRequested)
{
await Task.Delay(1000);
}
await httpContext.Response.WriteAsync("hello, world");
}
else if (httpContext.Request.Path == "/upgrade")
{
using (var stream = await httpContext.Features.Get<IHttpUpgradeFeature>().UpgradeAsync())
{
while (!upgradeCt.IsCancellationRequested)
{
await Task.Delay(LongDelay);
}
var responseBytes = Encoding.ASCII.GetBytes("hello, world");
await stream.WriteAsync(responseBytes, 0, responseBytes.Length);
}
}
else
{
await httpContext.Response.WriteAsync("hello, world");
}
} }
private async Task ReceiveResponse(TestConnection connection, TestServiceContext testServiceContext) private async Task ReceiveResponse(TestConnection connection, TestServiceContext testServiceContext)
@ -209,9 +230,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
await connection.Receive( await connection.Receive(
"HTTP/1.1 200 OK", "HTTP/1.1 200 OK",
$"Date: {testServiceContext.DateHeaderValue}", $"Date: {testServiceContext.DateHeaderValue}",
"Content-Length: 12", "Transfer-Encoding: chunked",
"", "",
"hello, world"); "c",
"hello, world",
"0",
"",
"");
} }
} }
} }

View File

@ -1,4 +1,8 @@
using System.Threading; // 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.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Server.Kestrel; using Microsoft.AspNetCore.Server.Kestrel;
using Microsoft.AspNetCore.Server.Kestrel.Internal; using Microsoft.AspNetCore.Server.Kestrel.Internal;
@ -14,7 +18,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
public class ConnectionTests public class ConnectionTests
{ {
[Fact] [Fact]
public void DoesNotEndConnectionOnZeroRead() public async Task DoesNotEndConnectionOnZeroRead()
{ {
var mockLibuv = new MockLibuv(); var mockLibuv = new MockLibuv();
@ -30,14 +34,19 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
ServerAddress = ServerAddress.FromUrl("http://127.0.0.1:0"), ServerAddress = ServerAddress.FromUrl("http://127.0.0.1:0"),
Thread = engine.Threads[0] Thread = engine.Threads[0]
}; };
var socket = new MockSocket(mockLibuv, Thread.CurrentThread.ManagedThreadId, trace);
var connection = new Connection(context, socket);
connection.Start();
Libuv.uv_buf_t ignored; Connection connection = null;
mockLibuv.AllocCallback(socket.InternalGetHandle(), 2048, out ignored); await context.Thread.PostAsync(_ =>
mockLibuv.ReadCallback(socket.InternalGetHandle(), 0, ref ignored); {
Assert.False(connection.SocketInput.CheckFinOrThrow()); var socket = new MockSocket(mockLibuv, Thread.CurrentThread.ManagedThreadId, trace);
connection = new Connection(context, socket);
connection.Start();
Libuv.uv_buf_t ignored;
mockLibuv.AllocCallback(socket.InternalGetHandle(), 2048, out ignored);
mockLibuv.ReadCallback(socket.InternalGetHandle(), 0, ref ignored);
Assert.False(connection.SocketInput.CheckFinOrThrow());
}, null);
connection.ConnectionControl.End(ProduceEndType.SocketDisconnect); connection.ConnectionControl.End(ProduceEndType.SocketDisconnect);
} }

View File

@ -12,6 +12,7 @@ using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure;
using Microsoft.AspNetCore.Server.KestrelTests.TestHelpers; using Microsoft.AspNetCore.Server.KestrelTests.TestHelpers;
using Microsoft.AspNetCore.Testing; using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.Internal; using Microsoft.Extensions.Internal;
using Moq;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Server.KestrelTests namespace Microsoft.AspNetCore.Server.KestrelTests
@ -724,6 +725,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
{ {
var connectionContext = new ConnectionContext() var connectionContext = new ConnectionContext()
{ {
ConnectionControl = new Mock<IConnectionControl>().Object,
DateHeaderValueManager = new DateHeaderValueManager(), DateHeaderValueManager = new DateHeaderValueManager(),
ServerAddress = ServerAddress.FromUrl("http://localhost:5000"), ServerAddress = ServerAddress.FromUrl("http://localhost:5000"),
ServerOptions = new KestrelServerOptions(), ServerOptions = new KestrelServerOptions(),
@ -770,6 +772,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
{ {
var connectionContext = new ConnectionContext() var connectionContext = new ConnectionContext()
{ {
ConnectionControl = new Mock<IConnectionControl>().Object,
DateHeaderValueManager = new DateHeaderValueManager(), DateHeaderValueManager = new DateHeaderValueManager(),
ServerAddress = ServerAddress.FromUrl("http://localhost:5000"), ServerAddress = ServerAddress.FromUrl("http://localhost:5000"),
ServerOptions = new KestrelServerOptions(), ServerOptions = new KestrelServerOptions(),
@ -786,6 +789,59 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
} }
} }
[Fact]
public void TakeStartLineDisablesKeepAliveTimeoutOnFirstByteAvailable()
{
var trace = new KestrelTrace(new TestKestrelTrace());
var ltp = new LoggingThreadPool(trace);
using (var pool = new MemoryPool())
using (var socketInput = new SocketInput(pool, ltp))
{
var connectionControl = new Mock<IConnectionControl>();
var connectionContext = new ConnectionContext()
{
ConnectionControl = connectionControl.Object,
DateHeaderValueManager = new DateHeaderValueManager(),
ServerAddress = ServerAddress.FromUrl("http://localhost:5000"),
ServerOptions = new KestrelServerOptions(),
Log = trace
};
var frame = new Frame<object>(application: null, context: connectionContext);
frame.Reset();
var requestLineBytes = Encoding.ASCII.GetBytes("G");
socketInput.IncomingData(requestLineBytes, 0, requestLineBytes.Length);
frame.TakeStartLine(socketInput);
connectionControl.Verify(cc => cc.CancelTimeout());
}
}
[Fact]
public void TakeStartLineDoesNotDisableKeepAliveTimeoutIfNoDataAvailable()
{
var trace = new KestrelTrace(new TestKestrelTrace());
var ltp = new LoggingThreadPool(trace);
using (var pool = new MemoryPool())
using (var socketInput = new SocketInput(pool, ltp))
{
var connectionControl = new Mock<IConnectionControl>();
var connectionContext = new ConnectionContext()
{
ConnectionControl = connectionControl.Object,
DateHeaderValueManager = new DateHeaderValueManager(),
ServerAddress = ServerAddress.FromUrl("http://localhost:5000"),
ServerOptions = new KestrelServerOptions(),
Log = trace
};
var frame = new Frame<object>(application: null, context: connectionContext);
frame.Reset();
frame.TakeStartLine(socketInput);
connectionControl.Verify(cc => cc.CancelTimeout(), Times.Never);
}
}
[Fact] [Fact]
public void TakeMessageHeadersCallsConsumingCompleteWithFurthestExamined() public void TakeMessageHeadersCallsConsumingCompleteWithFurthestExamined()
{ {
@ -864,5 +920,35 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
Assert.Equal(false, frame.TakeMessageHeaders(socketInput, (FrameRequestHeaders)frame.RequestHeaders)); Assert.Equal(false, frame.TakeMessageHeaders(socketInput, (FrameRequestHeaders)frame.RequestHeaders));
} }
} }
[Fact]
public void RequestProcessingAsyncEnablesKeepAliveTimeout()
{
var trace = new KestrelTrace(new TestKestrelTrace());
var ltp = new LoggingThreadPool(trace);
using (var pool = new MemoryPool())
using (var socketInput = new SocketInput(pool, ltp))
{
var connectionControl = new Mock<IConnectionControl>();
var connectionContext = new ConnectionContext()
{
ConnectionControl = connectionControl.Object,
DateHeaderValueManager = new DateHeaderValueManager(),
ServerAddress = ServerAddress.FromUrl("http://localhost:5000"),
ServerOptions = new KestrelServerOptions(),
Log = trace
};
var frame = new Frame<object>(application: null, context: connectionContext);
frame.Reset();
var requestProcessingTask = frame.RequestProcessingAsync();
connectionControl.Verify(cc => cc.SetTimeout((long)connectionContext.ServerOptions.Limits.KeepAliveTimeout.TotalMilliseconds));
frame.Stop();
socketInput.IncomingFin();
requestProcessingTask.Wait();
}
}
} }
} }

View File

@ -161,11 +161,11 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
[InlineData(2.1)] [InlineData(2.1)]
[InlineData(2.5)] [InlineData(2.5)]
[InlineData(2.9)] [InlineData(2.9)]
public void KeepAliveTimeoutIsRoundedToTheNextSecond(double seconds) public void KeepAliveTimeoutValid(double seconds)
{ {
var o = new KestrelServerLimits(); var o = new KestrelServerLimits();
o.KeepAliveTimeout = TimeSpan.FromSeconds(seconds); o.KeepAliveTimeout = TimeSpan.FromSeconds(seconds);
Assert.Equal(Math.Ceiling(seconds), o.KeepAliveTimeout.TotalSeconds); Assert.Equal(seconds, o.KeepAliveTimeout.TotalSeconds);
} }
} }
} }

View File

@ -15,6 +15,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests.TestHelpers
public MockConnection(KestrelServerOptions options) public MockConnection(KestrelServerOptions options)
{ {
ConnectionControl = this;
RequestAbortedSource = new CancellationTokenSource(); RequestAbortedSource = new CancellationTokenSource();
ServerOptions = options; ServerOptions = options;
} }

View File

@ -129,6 +129,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests.TestHelpers
_uv_timer_init = (loop, handle) => 0; _uv_timer_init = (loop, handle) => 0;
_uv_timer_start = (handle, callback, timeout, repeat) => 0; _uv_timer_start = (handle, callback, timeout, repeat) => 0;
_uv_timer_stop = handle => 0; _uv_timer_stop = handle => 0;
_uv_now = (loop) => DateTime.UtcNow.Ticks / TimeSpan.TicksPerMillisecond;
} }
public Func<UvStreamHandle, int, Action<int>, int> OnWrite { get; set; } public Func<UvStreamHandle, int, Action<int>, int> OnWrite { get; set; }

View File

@ -22,7 +22,12 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
{ {
var trace = new KestrelTrace(new TestKestrelTrace()); var trace = new KestrelTrace(new TestKestrelTrace());
var ltp = new LoggingThreadPool(trace); var ltp = new LoggingThreadPool(trace);
var context = new Frame<object>(null, new ConnectionContext() { ServerAddress = new ServerAddress() }) var connectionContext = new ConnectionContext()
{
ServerAddress = new ServerAddress(),
ServerOptions = new KestrelServerOptions()
};
var context = new Frame<object>(null, connectionContext)
{ {
DateHeaderValueManager = new DateHeaderValueManager(), DateHeaderValueManager = new DateHeaderValueManager(),
ServerAddress = ServerAddress.FromUrl("http://localhost:5000"), ServerAddress = ServerAddress.FromUrl("http://localhost:5000"),
@ -63,7 +68,11 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
{ {
} }
public void Stop() public void SetTimeout(long milliseconds)
{
}
public void CancelTimeout()
{ {
} }