diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Connection.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Connection.cs index 53de8bb857..84e70e1811 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Connection.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Connection.cs @@ -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 diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Frame.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Frame.cs index 7cc160f0ff..ea969d1225 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Frame.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Frame.cs @@ -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, diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/IConnectionControl.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/IConnectionControl.cs index fbc181bce3..0c174ca86a 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/IConnectionControl.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/IConnectionControl.cs @@ -8,5 +8,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http void Pause(); void Resume(); void End(ProduceEndType endType); + void Stop(); } } \ 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 752ddcffb8..1e760bdb5b 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/MessageBody.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/MessageBody.cs @@ -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(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(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; } diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Infrastructure/KestrelThread.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Infrastructure/KestrelThread.cs index 6d46aba889..08e5773083 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Infrastructure/KestrelThread.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Infrastructure/KestrelThread.cs @@ -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 _threadTcs = new TaskCompletionSource(); private readonly UvLoopHandle _loop; private readonly UvAsyncHandle _post; + private readonly UvTimerHandle _heartbeatTimer; private Queue _workAdding = new Queue(1024); private Queue _workRunning = new Queue(1024); private Queue _closeHandleAdding = new Queue(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(ptr); + (handle as UvStreamHandle)?.Connection?.Tick(); + }); + } + private bool DoPostWork() { Queue queue; diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Networking/Libuv.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Networking/Libuv.cs index 8901af40e2..9b1fada8f5 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Networking/Libuv.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Networking/Libuv.cs @@ -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 _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 _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 _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, diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Networking/UvTimerHandle.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Networking/UvTimerHandle.cs new file mode 100644 index 0000000000..efed9ab59c --- /dev/null +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Networking/UvTimerHandle.cs @@ -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 _callback; + + public UvTimerHandle(IKestrelTrace logger) : base(logger) + { + } + + public void Init(UvLoopHandle loop, Action, IntPtr> queueCloseHandle) + { + CreateHandle( + loop.Libuv, + loop.ThreadId, + loop.Libuv.handle_size(Libuv.HandleType.TIMER), + queueCloseHandle); + + _uv.timer_init(loop, this); + } + + public void Start(Action 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(handle); + + try + { + timer._callback(timer); + } + catch (Exception ex) + { + timer._log.LogError(0, ex, nameof(UvTimerCb)); + throw; + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/KestrelServerLimits.cs b/src/Microsoft.AspNetCore.Server.Kestrel/KestrelServerLimits.cs index 6cfac04635..7682e015ae 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/KestrelServerLimits.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/KestrelServerLimits.cs @@ -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); + /// /// 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; } } + + /// + /// 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. + /// + public TimeSpan KeepAliveTimeout + { + get + { + return _keepAliveTimeout; + } + set + { + _keepAliveTimeout = TimeSpan.FromSeconds(Math.Ceiling(value.TotalSeconds)); + } + } } } diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/KeepAliveTimeoutTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/KeepAliveTimeoutTests.cs new file mode 100644 index 0000000000..f4bf8ba357 --- /dev/null +++ b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/KeepAliveTimeoutTests.cs @@ -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(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(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(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"); + } + } +} diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/KestrelServerLimitsTests.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/KestrelServerLimitsTests.cs index 5199a16941..9edfd5d3c9 100644 --- a/test/Microsoft.AspNetCore.Server.KestrelTests/KestrelServerLimitsTests.cs +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/KestrelServerLimitsTests.cs @@ -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(() => { @@ -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); + } } } diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/TestHelpers/MockLibuv.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/TestHelpers/MockLibuv.cs index fcdf349003..c56dc3c491 100644 --- a/test/Microsoft.AspNetCore.Server.KestrelTests/TestHelpers/MockLibuv.cs +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/TestHelpers/MockLibuv.cs @@ -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, int> OnWrite { get; set; } diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/TestInput.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/TestInput.cs index de0edf5020..81c03056ac 100644 --- a/test/Microsoft.AspNetCore.Server.KestrelTests/TestInput.cs +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/TestInput.cs @@ -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 data, Action callback, object state) { } - public void End(ProduceEndType endType) - { - } void IFrameControl.ProduceContinue() { diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/UvTimerHandleTests.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/UvTimerHandleTests.cs new file mode 100644 index 0000000000..b81d14d89a --- /dev/null +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/UvTimerHandleTests.cs @@ -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); + } + } +} diff --git a/test/shared/TestConnection.cs b/test/shared/TestConnection.cs index 4accbe0ed1..a382cac84d 100644 --- a/test/shared/TestConnection.cs +++ b/test/shared/TestConnection.cs @@ -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)