Add keep-alive timeout (#464).

This commit is contained in:
Cesar Blum Silveira 2016-08-18 16:40:46 -07:00
parent 19f8958fa8
commit f2085b1968
14 changed files with 501 additions and 15 deletions

View File

@ -2,7 +2,6 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
@ -156,6 +155,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
_socketClosedTcs.TrySetResult(null);
}
// Called on Libuv thread
public void Tick()
{
_frame.Tick();
}
private void ApplyConnectionFilter()
{
if (_filterContext.Connection != _libuvStream)
@ -277,6 +282,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
}
}
void IConnectionControl.Stop()
{
StopAsync();
}
private static unsafe string GenerateConnectionId(long id)
{
// The following routine is ~310% faster than calling long.ToString() on x64

View File

@ -56,7 +56,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
private CancellationTokenSource _abortedCts;
private CancellationToken? _manuallySetRequestAbortToken;
protected RequestProcessingStatus _requestProcessingStatus;
private RequestProcessingStatus _requestProcessingStatus;
protected bool _keepAlive;
private bool _autoChunk;
protected Exception _applicationException;
@ -68,6 +68,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
private int _remainingRequestHeadersBytesAllowed;
private int _requestHeadersParsed;
private int _secondsSinceLastRequest;
public Frame(ConnectionContext context)
: base(context)
{
@ -213,10 +215,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
}
}
public bool HasResponseStarted
{
get { return _requestProcessingStatus == RequestProcessingStatus.ResponseStarted; }
}
public bool HasResponseStarted => _requestProcessingStatus == RequestProcessingStatus.ResponseStarted;
protected FrameRequestHeaders FrameRequestHeaders { get; private set; }
@ -1267,6 +1266,25 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
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;
}
protected enum RequestLineStatus
{
Empty,
@ -1277,7 +1295,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
Done
}
protected enum RequestProcessingStatus
private enum RequestProcessingStatus
{
RequestPending,
RequestStarted,

View File

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

View File

@ -172,6 +172,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
var limit = buffer.Array == null ? inputLengthLimit : Math.Min(buffer.Count, inputLengthLimit);
if (limit == 0)
{
_context.RequestFinished();
return new ValueTask<int>(0);
}
@ -182,10 +183,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
// .GetAwaiter().GetResult() done by ValueTask if needed
var actual = task.Result;
_inputLength -= actual;
if (actual == 0)
{
_context.RejectRequest(RequestRejectionReason.UnexpectedEndOfRequestContent);
}
if (_inputLength == 0)
{
_context.RequestFinished();
}
return new ValueTask<int>(actual);
}
else
@ -198,11 +206,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{
var actual = await task;
_inputLength -= actual;
if (actual == 0)
{
_context.RejectRequest(RequestRejectionReason.UnexpectedEndOfRequestContent);
}
if (_inputLength == 0)
{
_context.RequestFinished();
}
return actual;
}
}
@ -354,6 +368,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
_mode = Mode.Complete;
}
_context.RequestFinished();
return 0;
}

View File

@ -27,12 +27,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal
// otherwise it needs to wait till the next pass of the libuv loop
private readonly int _maxLoops = 8;
// how often the heartbeat timer will tick connections
private const int _heartbeatMilliseconds = 1000;
private readonly KestrelEngine _engine;
private readonly IApplicationLifetime _appLifetime;
private readonly Thread _thread;
private readonly TaskCompletionSource<object> _threadTcs = new TaskCompletionSource<object>();
private readonly UvLoopHandle _loop;
private readonly UvAsyncHandle _post;
private readonly UvTimerHandle _heartbeatTimer;
private Queue<Work> _workAdding = new Queue<Work>(1024);
private Queue<Work> _workRunning = new Queue<Work>(1024);
private Queue<CloseHandle> _closeHandleAdding = new Queue<CloseHandle>(256);
@ -57,6 +61,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal
_post = new UvAsyncHandle(_log);
_thread = new Thread(ThreadStart);
_thread.Name = "KestrelThread - libuv";
_heartbeatTimer = new UvTimerHandle(_log);
#if !DEBUG
// Mark the thread as being as unimportant to keeping the process alive.
// Don't do this for debug builds, so we know if the thread isn't terminating.
@ -176,9 +181,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal
}
}
private void AllowStop()
{
_heartbeatTimer.Stop();
_post.Unreference();
}
@ -274,6 +279,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal
{
_loop.Init(_engine.Libuv);
_post.Init(_loop, OnPost, EnqueueCloseHandle);
_heartbeatTimer.Init(_loop, EnqueueCloseHandle);
_heartbeatTimer.Start(OnHeartbeat, timeout: 1000, repeat: 1000);
_initCompleted = true;
tcs.SetResult(0);
}
@ -296,6 +303,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal
// run the loop one more time to delete the open handles
_post.Reference();
_post.Dispose();
_heartbeatTimer.Dispose();
// Ensure the Dispose operations complete in the event loop.
_loop.Run();
@ -327,6 +335,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal
} while (wasWork && loopsRemaining > 0);
}
private void OnHeartbeat(UvTimerHandle timer)
{
Walk(ptr =>
{
var handle = UvMemory.FromIntPtr<UvHandle>(ptr);
(handle as UvStreamHandle)?.Connection?.Tick();
});
}
private bool DoPostWork()
{
Queue<Work> queue;

View File

@ -53,6 +53,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Networking
_uv_tcp_getpeername = NativeMethods.uv_tcp_getpeername;
_uv_tcp_getsockname = NativeMethods.uv_tcp_getsockname;
_uv_walk = NativeMethods.uv_walk;
_uv_timer_init = NativeMethods.uv_timer_init;
_uv_timer_start = NativeMethods.uv_timer_start;
_uv_timer_stop = NativeMethods.uv_timer_stop;
}
// Second ctor that doesn't set any fields only to be used by MockLibuv
@ -407,6 +410,30 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Networking
_uv_walk(loop, walk_cb, arg);
}
protected Func<UvLoopHandle, UvTimerHandle, int> _uv_timer_init;
unsafe public void timer_init(UvLoopHandle loop, UvTimerHandle handle)
{
loop.Validate();
handle.Validate();
ThrowIfErrored(_uv_timer_init(loop, handle));
}
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void uv_timer_cb(IntPtr handle);
protected Func<UvTimerHandle, uv_timer_cb, long, long, int> _uv_timer_start;
unsafe public void timer_start(UvTimerHandle handle, uv_timer_cb cb, long timeout, long repeat)
{
handle.Validate();
ThrowIfErrored(_uv_timer_start(handle, cb, timeout, repeat));
}
protected Func<UvTimerHandle, int> _uv_timer_stop;
unsafe public void timer_stop(UvTimerHandle handle)
{
handle.Validate();
ThrowIfErrored(_uv_timer_stop(handle));
}
public delegate int uv_tcp_getsockname_func(UvTcpHandle handle, out SockAddr addr, ref int namelen);
protected uv_tcp_getsockname_func _uv_tcp_getsockname;
public void tcp_getsockname(UvTcpHandle handle, out SockAddr addr, ref int namelen)
@ -604,6 +631,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Networking
[DllImport("libuv", CallingConvention = CallingConvention.Cdecl)]
public static extern int uv_walk(UvLoopHandle loop, uv_walk_cb walk_cb, IntPtr arg);
[DllImport("libuv", CallingConvention = CallingConvention.Cdecl)]
unsafe public static extern int uv_timer_init(UvLoopHandle loop, UvTimerHandle handle);
[DllImport("libuv", CallingConvention = CallingConvention.Cdecl)]
unsafe public static extern int uv_timer_start(UvTimerHandle handle, uv_timer_cb cb, long timeout, long repeat);
[DllImport("libuv", CallingConvention = CallingConvention.Cdecl)]
unsafe public static extern int uv_timer_stop(UvTimerHandle handle);
[DllImport("WS2_32.dll", CallingConvention = CallingConvention.Winapi)]
unsafe public static extern int WSAIoctl(
IntPtr socket,

View File

@ -0,0 +1,57 @@
// 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 Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Networking
{
public class UvTimerHandle : UvHandle
{
private readonly static Libuv.uv_timer_cb _uv_timer_cb = UvTimerCb;
private Action<UvTimerHandle> _callback;
public UvTimerHandle(IKestrelTrace logger) : base(logger)
{
}
public void Init(UvLoopHandle loop, Action<Action<IntPtr>, IntPtr> queueCloseHandle)
{
CreateHandle(
loop.Libuv,
loop.ThreadId,
loop.Libuv.handle_size(Libuv.HandleType.TIMER),
queueCloseHandle);
_uv.timer_init(loop, this);
}
public void Start(Action<UvTimerHandle> callback, long timeout, long repeat)
{
_callback = callback;
_uv.timer_start(this, _uv_timer_cb, timeout, repeat);
}
public void Stop()
{
_uv.timer_stop(this);
}
private static void UvTimerCb(IntPtr handle)
{
var timer = FromIntPtr<UvTimerHandle>(handle);
try
{
timer._callback(timer);
}
catch (Exception ex)
{
timer._log.LogError(0, ex, nameof(UvTimerCb));
throw;
}
}
}
}

View File

@ -23,6 +23,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel
// Matches the default LimitRequestFields in Apache httpd.
private int _maxRequestHeaderCount = 100;
// Matches the default http.sys keep-alive timouet.
private TimeSpan _keepAliveTimeout = TimeSpan.FromMinutes(2);
/// <summary>
/// Gets or sets the maximum size of the response buffer before write
/// calls begin to block or return tasks that don't complete until the
@ -138,5 +141,23 @@ namespace Microsoft.AspNetCore.Server.Kestrel
_maxRequestHeaderCount = value;
}
}
/// <summary>
/// Gets or sets the keep-alive timeout.
/// </summary>
/// <remarks>
/// Defaults to 2 minutes. Timeout granularity is in seconds. Sub-second values will be rounded to the next second.
/// </remarks>
public TimeSpan KeepAliveTimeout
{
get
{
return _keepAliveTimeout;
}
set
{
_keepAliveTimeout = TimeSpan.FromSeconds(Math.Ceiling(value.TotalSeconds));
}
}
}
}

View File

@ -0,0 +1,209 @@
// 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.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Testing;
using Xunit;
namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
{
public class KeepAliveTimeoutTests
{
private static readonly TimeSpan KeepAliveTimeout = TimeSpan.FromSeconds(10);
private static readonly int LongDelay = (int)TimeSpan.FromSeconds(30).TotalMilliseconds;
private static readonly int ShortDelay = LongDelay / 10;
[Fact]
public async Task TestKeepAliveTimeout()
{
using (var server = CreateServer())
{
var tasks = new[]
{
ConnectionClosedWhenKeepAliveTimeoutExpires(server),
ConnectionClosedWhenKeepAliveTimeoutExpiresAfterChunkedRequest(server),
KeepAliveTimeoutResetsBetweenContentLengthRequests(server),
KeepAliveTimeoutResetsBetweenChunkedRequests(server),
KeepAliveTimeoutNotTriggeredMidContentLengthRequest(server),
KeepAliveTimeoutNotTriggeredMidChunkedRequest(server),
ConnectionTimesOutWhenOpenedButNoRequestSent(server)
};
await Task.WhenAll(tasks);
}
}
private async Task ConnectionClosedWhenKeepAliveTimeoutExpires(TestServer server)
{
using (var connection = new TestConnection(server.Port))
{
await connection.Send(
"GET / HTTP/1.1",
"",
"");
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 ConnectionClosedWhenKeepAliveTimeoutExpiresAfterChunkedRequest(TestServer server)
{
using (var connection = new TestConnection(server.Port))
{
await connection.Send(
"POST / HTTP/1.1",
"Transfer-Encoding: chunked",
"",
"5", "hello",
"6", " world",
"0",
"",
"");
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)
{
using (var connection = new TestConnection(server.Port))
{
for (var i = 0; i < 10; i++)
{
await connection.Send(
"GET / HTTP/1.1",
"",
"");
await ReceiveResponse(connection, server.Context);
await Task.Delay(ShortDelay);
}
}
}
private async Task KeepAliveTimeoutResetsBetweenChunkedRequests(TestServer server)
{
using (var connection = new TestConnection(server.Port))
{
for (var i = 0; i < 5; i++)
{
await connection.Send(
"POST / HTTP/1.1",
"Transfer-Encoding: chunked",
"",
"5", "hello",
"6", " world",
"0",
"",
"");
await ReceiveResponse(connection, server.Context);
await Task.Delay(ShortDelay);
}
}
}
private async Task KeepAliveTimeoutNotTriggeredMidContentLengthRequest(TestServer server)
{
using (var connection = new TestConnection(server.Port))
{
await connection.Send(
"POST / HTTP/1.1",
"Content-Length: 8",
"",
"a");
await Task.Delay(LongDelay);
await connection.Send("bcdefgh");
await ReceiveResponse(connection, server.Context);
}
}
private async Task KeepAliveTimeoutNotTriggeredMidChunkedRequest(TestServer server)
{
using (var connection = new TestConnection(server.Port))
{
await connection.Send(
"POST / HTTP/1.1",
"Transfer-Encoding: chunked",
"",
"5", "hello",
"");
await Task.Delay(LongDelay);
await connection.Send(
"6", " world",
"0",
"",
"");
await ReceiveResponse(connection, server.Context);
}
}
private async Task ConnectionTimesOutWhenOpenedButNoRequestSent(TestServer server)
{
using (var connection = new TestConnection(server.Port))
{
await Task.Delay(LongDelay);
await Assert.ThrowsAsync<IOException>(async () =>
{
await connection.Send(
"GET / HTTP/1.1",
"",
"");
});
}
}
private TestServer CreateServer()
{
return new TestServer(App, new TestServiceContext
{
ServerOptions = new KestrelServerOptions
{
AddServerHeader = false,
Limits =
{
KeepAliveTimeout = KeepAliveTimeout
}
}
});
}
private async Task App(HttpContext httpContext)
{
const string response = "hello, world";
httpContext.Response.ContentLength = response.Length;
await httpContext.Response.WriteAsync(response);
}
private async Task ReceiveResponse(TestConnection connection, TestServiceContext testServiceContext)
{
await connection.Receive(
"HTTP/1.1 200 OK",
$"Date: {testServiceContext.DateHeaderValue}",
"Content-Length: 12",
"",
"hello, world");
}
}
}

View File

@ -122,7 +122,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
}
[Fact]
public void MaxRequestHeadersDefault()
public void MaxRequestHeaderCountDefault()
{
Assert.Equal(100, (new KestrelServerLimits()).MaxRequestHeaderCount);
}
@ -131,7 +131,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
[InlineData(int.MinValue)]
[InlineData(-1)]
[InlineData(0)]
public void MaxRequestHeadersInvalid(int value)
public void MaxRequestHeaderCountInvalid(int value)
{
Assert.Throws<ArgumentOutOfRangeException>(() =>
{
@ -142,11 +142,30 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
[Theory]
[InlineData(1)]
[InlineData(int.MaxValue)]
public void MaxRequestHeadersValid(int value)
public void MaxRequestHeaderCountValid(int value)
{
var o = new KestrelServerLimits();
o.MaxRequestHeaderCount = value;
Assert.Equal(value, o.MaxRequestHeaderCount);
}
[Fact]
public void KeepAliveTimeoutDefault()
{
Assert.Equal(TimeSpan.FromMinutes(2), new KestrelServerLimits().KeepAliveTimeout);
}
[Theory]
[InlineData(0)]
[InlineData(0.5)]
[InlineData(2.1)]
[InlineData(2.5)]
[InlineData(2.9)]
public void KeepAliveTimeoutIsRoundedToTheNextSecond(double seconds)
{
var o = new KestrelServerLimits();
o.KeepAliveTimeout = TimeSpan.FromSeconds(seconds);
Assert.Equal(Math.Ceiling(seconds), o.KeepAliveTimeout.TotalSeconds);
}
}
}

View File

@ -126,6 +126,10 @@ namespace Microsoft.AspNetCore.Server.KestrelTests.TestHelpers
{
throw new Exception($"Why is this getting called?{Environment.NewLine}{_stackTrace}");
};
_uv_timer_init = (loop, handle) => 0;
_uv_timer_start = (handle, callback, timeout, repeat) => 0;
_uv_timer_stop = handle => 0;
}
public Func<UvStreamHandle, int, Action<int>, int> OnWrite { get; set; }

View File

@ -58,6 +58,14 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
{
}
public void End(ProduceEndType endType)
{
}
public void Stop()
{
}
public void Abort()
{
}
@ -65,9 +73,6 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
public void Write(ArraySegment<byte> data, Action<Exception, object> callback, object state)
{
}
public void End(ProduceEndType endType)
{
}
void IFrameControl.ProduceContinue()
{

View File

@ -0,0 +1,72 @@
// 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.Internal.Networking;
using Microsoft.AspNetCore.Testing;
using Xunit;
namespace Microsoft.AspNetCore.Server.KestrelTests
{
public class UvTimerHandleTests
{
[Fact]
public void TestTimeout()
{
var trace = new TestKestrelTrace();
var loop = new UvLoopHandle(trace);
loop.Init(new Libuv());
var timer = new UvTimerHandle(trace);
timer.Init(loop, (a, b) => { });
var callbackInvoked = false;
timer.Start(_ =>
{
callbackInvoked = true;
}, 1, 0);
loop.Run();
timer.Dispose();
loop.Run();
loop.Dispose();
Assert.True(callbackInvoked);
}
[Fact]
public void TestRepeat()
{
var trace = new TestKestrelTrace();
var loop = new UvLoopHandle(trace);
loop.Init(new Libuv());
var timer = new UvTimerHandle(trace);
timer.Init(loop, (callback, handle) => { });
var callbackCount = 0;
timer.Start(_ =>
{
if (callbackCount < 2)
{
callbackCount++;
}
else
{
timer.Stop();
}
}, 1, 1);
loop.Run();
timer.Dispose();
loop.Run();
loop.Dispose();
Assert.Equal(2, callbackCount);
}
}
}

View File

@ -34,6 +34,7 @@ namespace Microsoft.AspNetCore.Testing
_stream = new NetworkStream(_socket, false);
_reader = new StreamReader(_stream, Encoding.ASCII);
}
public void Dispose()
{
_stream.Dispose();
@ -97,7 +98,7 @@ namespace Microsoft.AspNetCore.Testing
var task = _reader.ReadAsync(actual, offset, actual.Length - offset);
if (!Debugger.IsAttached)
{
Assert.True(await Task.WhenAny(task, Task.Delay(10000)) == task, "TestConnection.Receive timed out.");
Assert.True(await Task.WhenAny(task, Task.Delay(TimeSpan.FromMinutes(1))) == task, "TestConnection.Receive timed out.");
}
var count = await task;
if (count == 0)