diff --git a/test/Kestrel.Core.Tests/HttpConnectionTests.cs b/test/Kestrel.Core.Tests/HttpConnectionTests.cs index 091b3cadeaa072bb73d9fffea32f3c2fd0bc475a..c8dbf28ba0c7ed3decb6b9a9ece9486c7d88020b 100644 --- a/test/Kestrel.Core.Tests/HttpConnectionTests.cs +++ b/test/Kestrel.Core.Tests/HttpConnectionTests.cs @@ -418,10 +418,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests } [Fact] - public void WriteTimingAbortsConnectionWhenWriteDoesNotCompleteWithMinimumDataRate() + public async Task WriteTimingAbortsConnectionWhenWriteDoesNotCompleteWithMinimumDataRate() { var systemClock = new MockSystemClock(); - var aborted = new ManualResetEventSlim(); + var aborted = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously); _httpConnectionContext.ServiceContext.ServerOptions.Limits.MinResponseDataRate = new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(2)); @@ -434,7 +434,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests _httpConnection.Http1Connection.Reset(); _httpConnection.Http1Connection.RequestAborted.Register(() => { - aborted.Set(); + aborted.SetResult(null); }); // Initialize timestamp @@ -448,15 +448,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests _httpConnection.Tick(systemClock.UtcNow); Assert.True(_httpConnection.RequestTimedOut); - Assert.True(aborted.Wait(TimeSpan.FromSeconds(10))); + await aborted.Task.DefaultTimeout(); } [Fact] - public void WriteTimingAbortsConnectionWhenSmallWriteDoesNotCompleteWithinGracePeriod() + public async Task WriteTimingAbortsConnectionWhenSmallWriteDoesNotCompleteWithinGracePeriod() { var systemClock = new MockSystemClock(); var minResponseDataRate = new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(5)); - var aborted = new ManualResetEventSlim(); + var aborted = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously); _httpConnectionContext.ServiceContext.ServerOptions.Limits.MinResponseDataRate = minResponseDataRate; _httpConnectionContext.ServiceContext.SystemClock = systemClock; @@ -468,7 +468,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests _httpConnection.Http1Connection.Reset(); _httpConnection.Http1Connection.RequestAborted.Register(() => { - aborted.Set(); + aborted.SetResult(null); }); // Initialize timestamp @@ -490,14 +490,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests _httpConnection.Tick(systemClock.UtcNow); Assert.True(_httpConnection.RequestTimedOut); - Assert.True(aborted.Wait(TimeSpan.FromSeconds(10))); + await aborted.Task.DefaultTimeout(); } [Fact] - public void WriteTimingTimeoutPushedOnConcurrentWrite() + public async Task WriteTimingTimeoutPushedOnConcurrentWrite() { var systemClock = new MockSystemClock(); - var aborted = new ManualResetEventSlim(); + var aborted = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously); _httpConnectionContext.ServiceContext.ServerOptions.Limits.MinResponseDataRate = new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(2)); @@ -510,7 +510,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests _httpConnection.Http1Connection.Reset(); _httpConnection.Http1Connection.RequestAborted.Register(() => { - aborted.Set(); + aborted.SetResult(null); }); // Initialize timestamp @@ -537,7 +537,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests _httpConnection.Tick(systemClock.UtcNow); Assert.True(_httpConnection.RequestTimedOut); - Assert.True(aborted.Wait(TimeSpan.FromSeconds(10))); + await aborted.Task.DefaultTimeout(); } [Fact] @@ -547,7 +547,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var minResponseDataRate = new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(5)); var numWrites = 5; var writeSize = 100; - var aborted = new TaskCompletionSource<object>(); + var aborted = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously); _httpConnectionContext.ServiceContext.ServerOptions.Limits.MinResponseDataRate = minResponseDataRate; _httpConnectionContext.ServiceContext.SystemClock = systemClock; diff --git a/test/Kestrel.Core.Tests/MessageBodyTests.cs b/test/Kestrel.Core.Tests/MessageBodyTests.cs index 703808a400d58f8e94421f24168115a46b64dd34..5f006b12e3cc96ce0183c2d6199b880de5fa9ec4 100644 --- a/test/Kestrel.Core.Tests/MessageBodyTests.cs +++ b/test/Kestrel.Core.Tests/MessageBodyTests.cs @@ -687,11 +687,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests { using (var input = new TestInput()) { - var logEvent = new ManualResetEventSlim(); + var logEvent = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously); var mockLogger = new Mock<IKestrelTrace>(); mockLogger .Setup(logger => logger.RequestBodyDone("ConnectionId", "RequestId")) - .Callback(() => logEvent.Set()); + .Callback(() => logEvent.SetResult(null)); input.Http1Connection.ServiceContext.Log = mockLogger.Object; input.Http1Connection.ConnectionIdFeature = "ConnectionId"; input.Http1Connection.TraceIdentifier = "RequestId"; @@ -706,7 +706,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests input.Fin(); - Assert.True(logEvent.Wait(TestConstants.DefaultTimeout)); + await logEvent.Task.DefaultTimeout(); await body.StopAsync(); } diff --git a/test/Kestrel.FunctionalTests/ChunkedResponseTests.cs b/test/Kestrel.FunctionalTests/ChunkedResponseTests.cs index c9a7e70747a1a5faedb546cdd840d07578d4dbda..6cb1a9e439284d5b31117595e4e3f347721287bc 100644 --- a/test/Kestrel.FunctionalTests/ChunkedResponseTests.cs +++ b/test/Kestrel.FunctionalTests/ChunkedResponseTests.cs @@ -350,7 +350,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests { var testContext = new TestServiceContext(LoggerFactory); - var flushWh = new ManualResetEventSlim(); + var flushWh = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously); using (var server = new TestServer(async httpContext => { @@ -358,7 +358,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello "), 0, 6); // Don't complete response until client has received the first chunk. - flushWh.Wait(); + await flushWh.Task.DefaultTimeout(); await response.Body.WriteAsync(Encoding.ASCII.GetBytes("World!"), 0, 6); }, testContext, listenOptions)) @@ -379,7 +379,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests "Hello ", ""); - flushWh.Set(); + flushWh.SetResult(null); await connection.ReceiveEnd( "6", diff --git a/test/Kestrel.FunctionalTests/RequestBodyTimeoutTests.cs b/test/Kestrel.FunctionalTests/RequestBodyTimeoutTests.cs index 0ae0e9f0ce9d6ff3d347124531c9b859f88dd410..9d292e53fce717e2207fd53a299b173b214b9ee2 100644 --- a/test/Kestrel.FunctionalTests/RequestBodyTimeoutTests.cs +++ b/test/Kestrel.FunctionalTests/RequestBodyTimeoutTests.cs @@ -30,15 +30,39 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests DateHeaderValueManager = new DateHeaderValueManager(systemClock) }; - var appRunningEvent = new ManualResetEventSlim(); + var appRunningEvent = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously); using (var server = new TestServer(context => { context.Features.Get<IHttpMinRequestBodyDataRateFeature>().MinDataRate = new MinDataRate(bytesPerSecond: 1, gracePeriod: gracePeriod); - appRunningEvent.Set(); - return context.Request.Body.ReadAsync(new byte[1], 0, 1); + // The server must call Request.Body.ReadAsync() *before* the test sets systemClock.UtcNow (which is triggered by the + // server calling appRunningEvent.SetResult(null)). If systemClock.UtcNow is set first, it's possible for the test to fail + // due to the following race condition: + // + // 1. [test] systemClock.UtcNow += gracePeriod + TimeSpan.FromSeconds(1); + // 2. [server] Heartbeat._timer is triggered, which calls HttpConnection.Tick() + // 3. [server] HttpConnection.Tick() calls HttpConnection.CheckForReadDataRateTimeout() + // 4. [server] HttpConnection.CheckForReadDataRateTimeout() is a no-op, since _readTimingEnabled is false, + // since Request.Body.ReadAsync() has not been called yet + // 5. [server] HttpConnection.Tick() sets _lastTimestamp = timestamp + // 6. [server] Request.Body.ReadAsync() is called + // 6. [test] systemClock.UtcNow is never updated again, so server timestamp is never updated, + // so HttpConnection.CheckForReadDataRateTimeout() is always a no-op until test fails + // + // This is a pretty tight race, since the once-per-second Heartbeat._timer needs to fire between the test updating + // systemClock.UtcNow and the server calling Request.Body.ReadAsync(). But it happened often enough to cause + // test flakiness in our CI (https://github.com/aspnet/KestrelHttpServer/issues/2539). + // + // For verification, I was able to induce the race by adding a sleep in the RequestDelegate: + // appRunningEvent.SetResult(null); + // Thread.Sleep(5000); + // return context.Request.Body.ReadAsync(new byte[1], 0, 1); + + var readTask = context.Request.Body.ReadAsync(new byte[1], 0, 1); + appRunningEvent.SetResult(null); + return readTask; }, serviceContext)) { using (var connection = server.CreateConnection()) @@ -50,7 +74,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests "", ""); - Assert.True(appRunningEvent.Wait(TestConstants.DefaultTimeout)); + await appRunningEvent.Task.DefaultTimeout(); systemClock.UtcNow += gracePeriod + TimeSpan.FromSeconds(1); await connection.Receive( @@ -77,13 +101,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests DateHeaderValueManager = new DateHeaderValueManager(systemClock), }; - var appRunningEvent = new ManualResetEventSlim(); + var appRunningEvent = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously); using (var server = new TestServer(context => { context.Features.Get<IHttpMinRequestBodyDataRateFeature>().MinDataRate = null; - appRunningEvent.Set(); + appRunningEvent.SetResult(null); return Task.CompletedTask; }, serviceContext)) { @@ -96,7 +120,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests "", ""); - Assert.True(appRunningEvent.Wait(TestConstants.DefaultTimeout)); + await appRunningEvent.Task.DefaultTimeout(); await connection.Receive( "HTTP/1.1 200 OK", @@ -115,7 +139,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests Assert.Contains(TestSink.Writes, w => w.EventId.Id == 33 && w.LogLevel == LogLevel.Information); } - [Fact(Skip="https://github.com/aspnet/KestrelHttpServer/issues/2464")] + [Fact] public async Task ConnectionClosedEvenIfAppSwallowsException() { var gracePeriod = TimeSpan.FromSeconds(5); @@ -126,23 +150,30 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests DateHeaderValueManager = new DateHeaderValueManager(systemClock) }; - var appRunningEvent = new ManualResetEventSlim(); - var exceptionSwallowedEvent = new ManualResetEventSlim(); + var appRunningTcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously); + var exceptionSwallowedTcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously); using (var server = new TestServer(async context => { context.Features.Get<IHttpMinRequestBodyDataRateFeature>().MinDataRate = new MinDataRate(bytesPerSecond: 1, gracePeriod: gracePeriod); - appRunningEvent.Set(); + // See comment in RequestTimesOutWhenRequestBodyNotReceivedAtSpecifiedMinimumRate for + // why we call ReadAsync before setting the appRunningEvent. + var readTask = context.Request.Body.ReadAsync(new byte[1], 0, 1); + appRunningTcs.SetResult(null); try { - await context.Request.Body.ReadAsync(new byte[1], 0, 1); + await readTask; } catch (BadHttpRequestException ex) when (ex.StatusCode == 408) { - exceptionSwallowedEvent.Set(); + exceptionSwallowedTcs.SetResult(null); + } + catch (Exception ex) + { + exceptionSwallowedTcs.SetException(ex); } var response = "hello, world"; @@ -159,9 +190,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests "", ""); - Assert.True(appRunningEvent.Wait(TestConstants.DefaultTimeout), "AppRunningEvent timed out."); + await appRunningTcs.Task.DefaultTimeout(); systemClock.UtcNow += gracePeriod + TimeSpan.FromSeconds(1); - Assert.True(exceptionSwallowedEvent.Wait(TestConstants.DefaultTimeout), "ExceptionSwallowedEvent timed out."); + await exceptionSwallowedTcs.Task.DefaultTimeout(); await connection.Receive( "HTTP/1.1 200 OK", diff --git a/test/Kestrel.FunctionalTests/RequestTests.cs b/test/Kestrel.FunctionalTests/RequestTests.cs index eac3aaa369dff23a0b0220e3546cfbd9574bb1f3..32dfac74fcf5000aa0a43a58e93819ad7bfc43b5 100644 --- a/test/Kestrel.FunctionalTests/RequestTests.cs +++ b/test/Kestrel.FunctionalTests/RequestTests.cs @@ -1696,8 +1696,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests [Fact] public async Task DoesNotEnforceRequestBodyMinimumDataRateOnUpgradedRequest() { - var appEvent = new ManualResetEventSlim(); - var delayEvent = new ManualResetEventSlim(); + var appEvent = new TaskCompletionSource<object>(); + var delayEvent = new TaskCompletionSource<object>(); var serviceContext = new TestServiceContext(LoggerFactory) { SystemClock = new SystemClock() @@ -1710,12 +1710,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests using (var stream = await context.Features.Get<IHttpUpgradeFeature>().UpgradeAsync()) { - appEvent.Set(); + appEvent.SetResult(null); // Read once to go through one set of TryPauseTimingReads()/TryResumeTimingReads() calls await stream.ReadAsync(new byte[1], 0, 1); - delayEvent.Wait(); + await delayEvent.Task.DefaultTimeout(); // Read again to check that the connection is still alive await stream.ReadAsync(new byte[1], 0, 1); @@ -1735,11 +1735,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests "", "a"); - Assert.True(appEvent.Wait(TestConstants.DefaultTimeout)); + await appEvent.Task.DefaultTimeout(); await Task.Delay(TimeSpan.FromSeconds(5)); - delayEvent.Set(); + delayEvent.SetResult(null); await connection.Send("b"); diff --git a/test/Kestrel.FunctionalTests/ResponseTests.cs b/test/Kestrel.FunctionalTests/ResponseTests.cs index 9660dca2f3ed3f59af94ce9c6c43235549d2ce2a..2f22bc1f675ace222aa2bd592d3cb1a1d08f52fa 100644 --- a/test/Kestrel.FunctionalTests/ResponseTests.cs +++ b/test/Kestrel.FunctionalTests/ResponseTests.cs @@ -1116,12 +1116,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests var flushed = new SemaphoreSlim(0, 1); var serviceContext = new TestServiceContext(LoggerFactory) { ServerOptions = { AllowSynchronousIO = true } }; - using (var server = new TestServer(httpContext => + using (var server = new TestServer(async httpContext => { httpContext.Response.ContentLength = 12; httpContext.Response.Body.Write(Encoding.ASCII.GetBytes("hello, world"), 0, 12); - flushed.Wait(); - return Task.CompletedTask; + await flushed.WaitAsync(); }, serviceContext)) { using (var connection = server.CreateConnection()) @@ -1152,7 +1151,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests { httpContext.Response.ContentLength = 12; await httpContext.Response.WriteAsync(""); - flushed.Wait(); + await flushed.WaitAsync(); await httpContext.Response.WriteAsync("hello, world"); }, new TestServiceContext(LoggerFactory))) { @@ -1180,23 +1179,23 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests [Fact] public async Task WriteAfterConnectionCloseNoops() { - var connectionClosed = new ManualResetEventSlim(); - var requestStarted = new ManualResetEventSlim(); - var tcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously); + var connectionClosed = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously); + var requestStarted = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously); + var appCompleted = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously); using (var server = new TestServer(async httpContext => { try { - requestStarted.Set(); - connectionClosed.Wait(); + requestStarted.SetResult(null); + await connectionClosed.Task.DefaultTimeout(); httpContext.Response.ContentLength = 12; await httpContext.Response.WriteAsync("hello, world"); - tcs.TrySetResult(null); + appCompleted.TrySetResult(null); } catch (Exception ex) { - tcs.TrySetException(ex); + appCompleted.TrySetException(ex); } }, new TestServiceContext(LoggerFactory))) { @@ -1208,14 +1207,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests "", ""); - requestStarted.Wait(); + await requestStarted.Task.DefaultTimeout(); connection.Shutdown(SocketShutdown.Send); await connection.WaitForConnectionClose().DefaultTimeout(); } - connectionClosed.Set(); + connectionClosed.SetResult(null); - await tcs.Task.DefaultTimeout(); + await appCompleted.Task.DefaultTimeout(); } } @@ -2280,19 +2279,19 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests var largeString = new string('a', maxBytesPreCompleted + 1); var writeTcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously); - var requestAbortedWh = new ManualResetEventSlim(); - var requestStartWh = new ManualResetEventSlim(); + var requestAbortedWh = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously); + var requestStartWh = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously); using (var server = new TestServer(async httpContext => { - requestStartWh.Set(); + requestStartWh.SetResult(null); var response = httpContext.Response; var request = httpContext.Request; var lifetime = httpContext.Features.Get<IHttpRequestLifetimeFeature>(); - lifetime.RequestAborted.Register(() => requestAbortedWh.Set()); - Assert.True(requestAbortedWh.Wait(TestConstants.DefaultTimeout)); + lifetime.RequestAborted.Register(() => requestAbortedWh.SetResult(null)); + await requestAbortedWh.Task.DefaultTimeout(); try { @@ -2316,15 +2315,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests "", ""); - Assert.True(requestStartWh.Wait(TestConstants.DefaultTimeout)); + await requestStartWh.Task.DefaultTimeout(); } // Write failed - can throw TaskCanceledException or OperationCanceledException, - // dependending on how far the canceled write goes. + // depending on how far the canceled write goes. await Assert.ThrowsAnyAsync<OperationCanceledException>(async () => await writeTcs.Task).DefaultTimeout(); // RequestAborted tripped - Assert.True(requestAbortedWh.Wait(TestConstants.DefaultTimeout)); + await requestAbortedWh.Task.DefaultTimeout(); } }