diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Connection.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Connection.cs index 84e70e1811..7494362185 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Connection.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Connection.cs @@ -2,6 +2,7 @@ // 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; @@ -38,6 +39,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http private TaskCompletionSource _socketClosedTcs = new TaskCompletionSource(); private BufferSizeControl _bufferSizeControl; + private long _lastTimestamp; + private long _timeoutTimestamp = long.MaxValue; + public Connection(ListenerContext context, UvStreamHandle socket) : base(context) { _socket = socket; @@ -62,6 +66,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http } _frame = FrameFactory(this); + + _lastTimestamp = Thread.Loop.Now(); } // Internal for testing @@ -156,9 +162,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http } // 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() @@ -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) diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Frame.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Frame.cs index c065abba57..2e17305777 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Frame.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Frame.cs @@ -69,7 +69,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http private int _remainingRequestHeadersBytesAllowed; private int _requestHeadersParsed; - private int _secondsSinceLastRequest; + protected readonly long _keepAliveMilliseconds; public Frame(ConnectionContext context) : base(context) @@ -77,6 +77,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http _pathBase = context.ServerAddress.PathBase; FrameControl = this; + _keepAliveMilliseconds = (long)ServerOptions.Limits.KeepAliveTimeout.TotalMilliseconds; } public string ConnectionIdFeature { get; set; } @@ -805,6 +806,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http return RequestLineStatus.Empty; } + ConnectionControl.CancelTimeout(); + _requestProcessingStatus = RequestProcessingStatus.RequestStarted; int bytesScanned; @@ -1269,25 +1272,6 @@ 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; - } - public enum RequestLineStatus { Empty, diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/FrameOfT.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/FrameOfT.cs index 85a0832467..99d56f1fbf 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/FrameOfT.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/FrameOfT.cs @@ -32,6 +32,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http { while (!_requestProcessingStopping) { + ConnectionControl.SetTimeout(_keepAliveMilliseconds); + while (!_requestProcessingStopping && TakeStartLine(SocketInput) != RequestLineStatus.Done) { if (SocketInput.CheckFinOrThrow()) diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/IConnectionControl.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/IConnectionControl.cs index 0c174ca86a..f7de78dfcf 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/IConnectionControl.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/IConnectionControl.cs @@ -8,6 +8,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http void Pause(); void Resume(); void End(ProduceEndType endType); - void Stop(); + void SetTimeout(long milliseconds); + void CancelTimeout(); } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/MessageBody.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/MessageBody.cs index 584b6d79ec..65bbe9a808 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/MessageBody.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/MessageBody.cs @@ -172,7 +172,6 @@ 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(0); } @@ -189,11 +188,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http _context.RejectRequest(RequestRejectionReason.UnexpectedEndOfRequestContent); } - if (_inputLength == 0) - { - _context.RequestFinished(); - } - return new ValueTask(actual); } else @@ -212,11 +206,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http _context.RejectRequest(RequestRejectionReason.UnexpectedEndOfRequestContent); } - if (_inputLength == 0) - { - _context.RequestFinished(); - } - return actual; } } @@ -368,8 +357,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http _mode = Mode.Complete; } - _context.RequestFinished(); - return 0; } diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Infrastructure/KestrelThread.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Infrastructure/KestrelThread.cs index 08e5773083..ec8394be20 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Infrastructure/KestrelThread.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Infrastructure/KestrelThread.cs @@ -19,6 +19,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal /// public class KestrelThread { + public const long HeartbeatMilliseconds = 1000; + private static readonly Action _postCallbackAdapter = (callback, state) => ((Action)callback).Invoke(state); private static readonly Action _postAsyncCallbackAdapter = (callback, state) => ((Action)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 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; @@ -280,7 +279,7 @@ 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); + _heartbeatTimer.Start(OnHeartbeat, timeout: HeartbeatMilliseconds, repeat: HeartbeatMilliseconds); _initCompleted = true; tcs.SetResult(0); } @@ -337,10 +336,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal private void OnHeartbeat(UvTimerHandle timer) { + var now = Loop.Now(); + Walk(ptr => { var handle = UvMemory.FromIntPtr(ptr); - (handle as UvStreamHandle)?.Connection?.Tick(); + (handle as UvStreamHandle)?.Connection?.Tick(now); }); } diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Networking/Libuv.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Networking/Libuv.cs index 9b1fada8f5..aaf1a00eba 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Networking/Libuv.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Networking/Libuv.cs @@ -56,6 +56,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Networking _uv_timer_init = NativeMethods.uv_timer_init; _uv_timer_start = NativeMethods.uv_timer_start; _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 @@ -434,6 +435,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Networking ThrowIfErrored(_uv_timer_stop(handle)); } + protected Func _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); protected uv_tcp_getsockname_func _uv_tcp_getsockname; 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)] 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)] unsafe public static extern int WSAIoctl( IntPtr socket, diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Networking/UvLoopHandle.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Networking/UvLoopHandle.cs index 25bb3701b0..cbb654ccd1 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Networking/UvLoopHandle.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Networking/UvLoopHandle.cs @@ -33,6 +33,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Networking _uv.stop(this); } + public long Now() + { + return _uv.now(this); + } + unsafe protected override bool ReleaseHandle() { var memory = handle; diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/KestrelServerLimits.cs b/src/Microsoft.AspNetCore.Server.Kestrel/KestrelServerLimits.cs index 7682e015ae..57aaa031b9 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/KestrelServerLimits.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/KestrelServerLimits.cs @@ -23,8 +23,8 @@ 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); + // Matches the default http.sys connection timeout. + private TimeSpan _connectionTimeout = TimeSpan.FromMinutes(2); /// /// 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. /// /// - /// Defaults to 2 minutes. Timeout granularity is in seconds. Sub-second values will be rounded to the next second. + /// Defaults to 2 minutes. /// public TimeSpan KeepAliveTimeout { get { - return _keepAliveTimeout; + return _connectionTimeout; } set { - _keepAliveTimeout = TimeSpan.FromSeconds(Math.Ceiling(value.TotalSeconds)); + _connectionTimeout = value; } } } diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/KeepAliveTimeoutTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/KeepAliveTimeoutTests.cs index 60be92f163..f5c202a4dd 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/KeepAliveTimeoutTests.cs +++ b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/KeepAliveTimeoutTests.cs @@ -3,8 +3,11 @@ using System; using System.IO; +using System.Text; +using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Testing; using Xunit; @@ -13,23 +16,25 @@ 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; + private static readonly TimeSpan LongDelay = TimeSpan.FromSeconds(30); + private static readonly TimeSpan ShortDelay = TimeSpan.FromSeconds(LongDelay.TotalSeconds / 10); [Fact] 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[] { ConnectionClosedWhenKeepAliveTimeoutExpires(server), - ConnectionClosedWhenKeepAliveTimeoutExpiresAfterChunkedRequest(server), - KeepAliveTimeoutResetsBetweenContentLengthRequests(server), - KeepAliveTimeoutResetsBetweenChunkedRequests(server), - KeepAliveTimeoutNotTriggeredMidContentLengthRequest(server), - KeepAliveTimeoutNotTriggeredMidChunkedRequest(server), - ConnectionTimesOutWhenOpenedButNoRequestSent(server) + ConnectionKeptAliveBetweenRequests(server), + ConnectionNotTimedOutWhileRequestBeingSent(server), + ConnectionNotTimedOutWhileAppIsRunning(server, longRunningCancellationTokenSource), + ConnectionTimesOutWhenOpenedButNoRequestSent(server), + KeepAliveTimeoutDoesNotApplyToUpgradedConnections(server, upgradeCancellationTokenSource) }; 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)) { + 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( "POST / HTTP/1.1", "Transfer-Encoding: chunked", "", - "5", "hello", - "6", " world", + ""); + + while (!cts.IsCancellationRequested) + { + await connection.Send( + "1", + "a", + ""); + } + + await connection.Send( "0", - "", - ""); + "", + ""); await ReceiveResponse(connection, server.Context); - - await Task.Delay(LongDelay); - - await Assert.ThrowsAsync(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 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) + private async Task ConnectionNotTimedOutWhileAppIsRunning(TestServer server, CancellationTokenSource cts) { using (var connection = new TestConnection(server.Port)) { await connection.Send( - "POST / HTTP/1.1", - "Content-Length: 8", + "GET /longrunning HTTP/1.1", "", - "a"); - await Task.Delay(LongDelay); - await connection.Send("bcdefgh"); - await ReceiveResponse(connection, server.Context); - } - } + ""); + cts.CancelAfter(LongDelay); + + 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( - "POST / HTTP/1.1", - "Transfer-Encoding: chunked", - "", - "5", "hello", - ""); - await Task.Delay(LongDelay); - await connection.Send( - "6", " world", - "0", - "", - ""); + "GET / HTTP/1.1", + "", + ""); 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 { @@ -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"; - httpContext.Response.ContentLength = response.Length; - await httpContext.Response.WriteAsync(response); + var ct = httpContext.RequestAborted; + + 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().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) @@ -209,9 +230,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests await connection.Receive( "HTTP/1.1 200 OK", $"Date: {testServiceContext.DateHeaderValue}", - "Content-Length: 12", + "Transfer-Encoding: chunked", "", - "hello, world"); + "c", + "hello, world", + "0", + "", + ""); } } } diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/ConnectionTests.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/ConnectionTests.cs index 59f8c012a9..0d42df12f0 100644 --- a/test/Microsoft.AspNetCore.Server.KestrelTests/ConnectionTests.cs +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/ConnectionTests.cs @@ -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.Server.Kestrel; using Microsoft.AspNetCore.Server.Kestrel.Internal; @@ -14,7 +18,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests public class ConnectionTests { [Fact] - public void DoesNotEndConnectionOnZeroRead() + public async Task DoesNotEndConnectionOnZeroRead() { var mockLibuv = new MockLibuv(); @@ -30,14 +34,19 @@ namespace Microsoft.AspNetCore.Server.KestrelTests ServerAddress = ServerAddress.FromUrl("http://127.0.0.1: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; - mockLibuv.AllocCallback(socket.InternalGetHandle(), 2048, out ignored); - mockLibuv.ReadCallback(socket.InternalGetHandle(), 0, ref ignored); - Assert.False(connection.SocketInput.CheckFinOrThrow()); + Connection connection = null; + await context.Thread.PostAsync(_ => + { + 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); } diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/FrameTests.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/FrameTests.cs index 18c0039860..66c0941c2b 100644 --- a/test/Microsoft.AspNetCore.Server.KestrelTests/FrameTests.cs +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/FrameTests.cs @@ -12,6 +12,7 @@ using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure; using Microsoft.AspNetCore.Server.KestrelTests.TestHelpers; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Internal; +using Moq; using Xunit; namespace Microsoft.AspNetCore.Server.KestrelTests @@ -724,6 +725,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests { var connectionContext = new ConnectionContext() { + ConnectionControl = new Mock().Object, DateHeaderValueManager = new DateHeaderValueManager(), ServerAddress = ServerAddress.FromUrl("http://localhost:5000"), ServerOptions = new KestrelServerOptions(), @@ -770,6 +772,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests { var connectionContext = new ConnectionContext() { + ConnectionControl = new Mock().Object, DateHeaderValueManager = new DateHeaderValueManager(), ServerAddress = ServerAddress.FromUrl("http://localhost:5000"), 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(); + 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(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(); + 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(application: null, context: connectionContext); + frame.Reset(); + + frame.TakeStartLine(socketInput); + connectionControl.Verify(cc => cc.CancelTimeout(), Times.Never); + } + } + [Fact] public void TakeMessageHeadersCallsConsumingCompleteWithFurthestExamined() { @@ -864,5 +920,35 @@ namespace Microsoft.AspNetCore.Server.KestrelTests 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(); + 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(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(); + } + } } } diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/KestrelServerLimitsTests.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/KestrelServerLimitsTests.cs index 9edfd5d3c9..8481fc258d 100644 --- a/test/Microsoft.AspNetCore.Server.KestrelTests/KestrelServerLimitsTests.cs +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/KestrelServerLimitsTests.cs @@ -161,11 +161,11 @@ namespace Microsoft.AspNetCore.Server.KestrelTests [InlineData(2.1)] [InlineData(2.5)] [InlineData(2.9)] - public void KeepAliveTimeoutIsRoundedToTheNextSecond(double seconds) + public void KeepAliveTimeoutValid(double seconds) { var o = new KestrelServerLimits(); o.KeepAliveTimeout = TimeSpan.FromSeconds(seconds); - Assert.Equal(Math.Ceiling(seconds), o.KeepAliveTimeout.TotalSeconds); + Assert.Equal(seconds, o.KeepAliveTimeout.TotalSeconds); } } } diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/TestHelpers/MockConnection.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/TestHelpers/MockConnection.cs index 930c1ec6d8..7a095cfcff 100644 --- a/test/Microsoft.AspNetCore.Server.KestrelTests/TestHelpers/MockConnection.cs +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/TestHelpers/MockConnection.cs @@ -15,6 +15,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests.TestHelpers public MockConnection(KestrelServerOptions options) { + ConnectionControl = this; RequestAbortedSource = new CancellationTokenSource(); ServerOptions = options; } diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/TestHelpers/MockLibuv.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/TestHelpers/MockLibuv.cs index 80584f65be..55ad9161d4 100644 --- a/test/Microsoft.AspNetCore.Server.KestrelTests/TestHelpers/MockLibuv.cs +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/TestHelpers/MockLibuv.cs @@ -129,6 +129,7 @@ namespace Microsoft.AspNetCore.Server.KestrelTests.TestHelpers _uv_timer_init = (loop, handle) => 0; _uv_timer_start = (handle, callback, timeout, repeat) => 0; _uv_timer_stop = handle => 0; + _uv_now = (loop) => DateTime.UtcNow.Ticks / TimeSpan.TicksPerMillisecond; } public Func, int> OnWrite { get; set; } diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/TestInput.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/TestInput.cs index 5bb230ab4a..a9b357d139 100644 --- a/test/Microsoft.AspNetCore.Server.KestrelTests/TestInput.cs +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/TestInput.cs @@ -22,7 +22,12 @@ namespace Microsoft.AspNetCore.Server.KestrelTests { var trace = new KestrelTrace(new TestKestrelTrace()); var ltp = new LoggingThreadPool(trace); - var context = new Frame(null, new ConnectionContext() { ServerAddress = new ServerAddress() }) + var connectionContext = new ConnectionContext() + { + ServerAddress = new ServerAddress(), + ServerOptions = new KestrelServerOptions() + }; + var context = new Frame(null, connectionContext) { DateHeaderValueManager = new DateHeaderValueManager(), 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() { }