diff --git a/src/Identity/test/Identity.FunctionalTests/MapIdentityTests.cs b/src/Identity/test/Identity.FunctionalTests/MapIdentityTests.cs index 4c58725dfd6a..42cad245feb0 100644 --- a/src/Identity/test/Identity.FunctionalTests/MapIdentityTests.cs +++ b/src/Identity/test/Identity.FunctionalTests/MapIdentityTests.cs @@ -70,7 +70,7 @@ public async Task CanLoginWithBearerToken(string addIdentityMode) [Fact] public async Task CanCustomizeBearerTokenExpiration() { - var clock = new TestTimeProvider(); + var clock = new MockTimeProvider(); var expireTimeSpan = TimeSpan.FromSeconds(42); await using var app = await CreateAppAsync(services => diff --git a/src/Identity/test/Identity.FunctionalTests/Microsoft.AspNetCore.Identity.FunctionalTests.csproj b/src/Identity/test/Identity.FunctionalTests/Microsoft.AspNetCore.Identity.FunctionalTests.csproj index 0b19674121ee..0504b59f1121 100644 --- a/src/Identity/test/Identity.FunctionalTests/Microsoft.AspNetCore.Identity.FunctionalTests.csproj +++ b/src/Identity/test/Identity.FunctionalTests/Microsoft.AspNetCore.Identity.FunctionalTests.csproj @@ -10,7 +10,7 @@ - + diff --git a/src/Identity/test/Identity.Test/Microsoft.AspNetCore.Identity.Test.csproj b/src/Identity/test/Identity.Test/Microsoft.AspNetCore.Identity.Test.csproj index 197835a8d54f..894af599ff4b 100644 --- a/src/Identity/test/Identity.Test/Microsoft.AspNetCore.Identity.Test.csproj +++ b/src/Identity/test/Identity.Test/Microsoft.AspNetCore.Identity.Test.csproj @@ -5,7 +5,8 @@ - + + diff --git a/src/Identity/test/Identity.Test/SecurityStampValidatorTest.cs b/src/Identity/test/Identity.Test/SecurityStampValidatorTest.cs index 13934e5a608b..facd6a3711a0 100644 --- a/src/Identity/test/Identity.Test/SecurityStampValidatorTest.cs +++ b/src/Identity/test/Identity.Test/SecurityStampValidatorTest.cs @@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -270,7 +271,7 @@ public async Task OnValidateIdentityDoesNotRejectsWhenNotExpired() public async Task OnValidateIdentityDoesNotExtendExpirationWhenSlidingIsDisabled() { var user = new PocoUser("test"); - var timeProvider = new TestTimeProvider(new DateTimeOffset(2013, 6, 11, 12, 34, 56, 0, TimeSpan.Zero)); + var timeProvider = new MockTimeProvider(); var httpContext = new Mock(); var userManager = MockHelpers.MockUserManager(); var identityOptions = new Mock>(); @@ -308,8 +309,10 @@ public async Task OnValidateIdentityDoesNotExtendExpirationWhenSlidingIsDisabled await SecurityStampValidator.ValidatePrincipalAsync(context); // Issued is moved forward, expires is not. - Assert.Equal(timeProvider.GetUtcNow(), context.Properties.IssuedUtc); - Assert.Equal(timeProvider.GetUtcNow() + TimeSpan.FromDays(1), context.Properties.ExpiresUtc); + var now = timeProvider.GetUtcNow(); + now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, now.Offset); // Truncate to the nearest second. + Assert.Equal(now, context.Properties.IssuedUtc); + Assert.Equal(now + TimeSpan.FromDays(1), context.Properties.ExpiresUtc); Assert.NotNull(context.Principal); } diff --git a/src/Identity/test/InMemory.Test/FunctionalTest.cs b/src/Identity/test/InMemory.Test/FunctionalTest.cs index f458f5d11e07..98aa844dddae 100644 --- a/src/Identity/test/InMemory.Test/FunctionalTest.cs +++ b/src/Identity/test/InMemory.Test/FunctionalTest.cs @@ -14,6 +14,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity.Test; using Microsoft.AspNetCore.TestHost; +using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Net.Http.Headers; @@ -66,7 +67,7 @@ public async Task CookieContainsRoleClaim() [InlineData(false)] public async Task CanCreateMeLoginAndCookieStopsWorkingAfterExpiration(bool testCore) { - var timeProvider = new TestTimeProvider(); + var timeProvider = new MockTimeProvider(); var server = await CreateServer(services => { services.ConfigureApplicationCookie(options => @@ -114,10 +115,10 @@ public async Task CanCreateMeLoginAndCookieStopsWorkingAfterExpiration(bool test [InlineData(false, false)] public async Task CanCreateMeLoginAndSecurityStampExtendsExpiration(bool rememberMe, bool testCore) { - var timeProvider = new TestTimeProvider(); + var timeProvider = new MockTimeProvider(); var server = await CreateServer(services => { - services.Configure(o => o.TimeProvider = timeProvider); + services.AddSingleton(timeProvider); }, testCore: testCore); var transaction1 = await SendAsync(server, "http://example.com/createMe"); @@ -163,12 +164,12 @@ public async Task CanCreateMeLoginAndSecurityStampExtendsExpiration(bool remembe [InlineData(false)] public async Task CanAccessOldPrincipalDuringSecurityStampReplacement(bool testCore) { - var timeProvider = new TestTimeProvider(); + var timeProvider = new MockTimeProvider(); var server = await CreateServer(services => { + services.AddSingleton(timeProvider); services.Configure(options => { - options.TimeProvider = timeProvider; options.OnRefreshingPrincipal = c => { var newId = new ClaimsIdentity(); @@ -216,7 +217,7 @@ public async Task CanAccessOldPrincipalDuringSecurityStampReplacement(bool testC [InlineData(false)] public async Task TwoFactorRememberCookieVerification(bool testCore) { - var timeProvider = new TestTimeProvider(); + var timeProvider = new MockTimeProvider(); var server = await CreateServer(services => services.AddSingleton(timeProvider), testCore: testCore); var transaction1 = await SendAsync(server, "http://example.com/createMe"); @@ -245,7 +246,7 @@ public async Task TwoFactorRememberCookieVerification(bool testCore) [InlineData(false)] public async Task TwoFactorRememberCookieClearedBySecurityStampChange(bool testCore) { - var timeProvider = new TestTimeProvider(); + var timeProvider = new MockTimeProvider(); var server = await CreateServer(services => services.AddSingleton(timeProvider), testCore: testCore); var transaction1 = await SendAsync(server, "http://example.com/createMe"); diff --git a/src/Identity/test/InMemory.Test/Microsoft.AspNetCore.Identity.InMemory.Test.csproj b/src/Identity/test/InMemory.Test/Microsoft.AspNetCore.Identity.InMemory.Test.csproj index 3acb0ea06c24..87ad230d34de 100644 --- a/src/Identity/test/InMemory.Test/Microsoft.AspNetCore.Identity.InMemory.Test.csproj +++ b/src/Identity/test/InMemory.Test/Microsoft.AspNetCore.Identity.InMemory.Test.csproj @@ -5,7 +5,8 @@ - + + @@ -18,3 +19,4 @@ + \ No newline at end of file diff --git a/src/Identity/test/Shared/TestTimeProvider.cs b/src/Identity/test/Shared/TestTimeProvider.cs deleted file mode 100644 index 13d8d529159f..000000000000 --- a/src/Identity/test/Shared/TestTimeProvider.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.AspNetCore.Identity.Test; - -internal class TestTimeProvider : TimeProvider -{ - private DateTimeOffset _current; - - public TestTimeProvider() : this(DateTimeOffset.UtcNow) { } - - public TestTimeProvider(DateTimeOffset current) - { - _current = current; - } - - public override DateTimeOffset GetUtcNow() => _current; - - public void Advance(TimeSpan timeSpan) - { - _current += timeSpan; - } -} diff --git a/src/Middleware/OutputCaching/test/Microsoft.AspNetCore.OutputCaching.Tests.csproj b/src/Middleware/OutputCaching/test/Microsoft.AspNetCore.OutputCaching.Tests.csproj index f3a12d8a3c3d..036adf5982bb 100644 --- a/src/Middleware/OutputCaching/test/Microsoft.AspNetCore.OutputCaching.Tests.csproj +++ b/src/Middleware/OutputCaching/test/Microsoft.AspNetCore.OutputCaching.Tests.csproj @@ -10,6 +10,10 @@ + + + + diff --git a/src/Middleware/OutputCaching/test/OutputCacheMiddlewareTests.cs b/src/Middleware/OutputCaching/test/OutputCacheMiddlewareTests.cs index d2623feea6eb..a353a200a577 100644 --- a/src/Middleware/OutputCaching/test/OutputCacheMiddlewareTests.cs +++ b/src/Middleware/OutputCaching/test/OutputCacheMiddlewareTests.cs @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.OutputCaching.Memory; +using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging.Testing; using Microsoft.Extensions.Primitives; @@ -334,7 +335,7 @@ public void ContentIsNotModified_IfNoneMatch_MatchesAtLeastOneValue_True() [Fact] public void StartResponseAsync_IfAllowResponseCaptureIsTrue_SetsResponseTime() { - var timeProvider = new TestTimeProvider(); + var timeProvider = new MockTimeProvider(); var middleware = TestUtils.CreateTestMiddleware(options: new OutputCacheOptions { TimeProvider = timeProvider @@ -350,7 +351,7 @@ public void StartResponseAsync_IfAllowResponseCaptureIsTrue_SetsResponseTime() [Fact] public void StartResponseAsync_IfAllowResponseCaptureIsTrue_SetsResponseTimeOnlyOnce() { - var timeProvider = new TestTimeProvider(); + var timeProvider = new MockTimeProvider(); var middleware = TestUtils.CreateTestMiddleware(options: new OutputCacheOptions { TimeProvider = timeProvider @@ -387,7 +388,7 @@ public void FinalizeCacheHeadersAsync_ResponseValidity_IgnoresExpiryIfAvailable( { // The Expires header should not be used when set in the response - var timeProvider = new TestTimeProvider(DateTimeOffset.MinValue); + var timeProvider = new MockTimeProvider(); var options = new OutputCacheOptions { TimeProvider = timeProvider @@ -410,7 +411,7 @@ public void FinalizeCacheHeadersAsync_ResponseValidity_UseMaxAgeIfAvailable() { // The MaxAge header should not be used if set in the response - var timeProvider = new TestTimeProvider(); + var timeProvider = new MockTimeProvider(); var sink = new TestSink(); var options = new OutputCacheOptions { @@ -436,7 +437,7 @@ public void FinalizeCacheHeadersAsync_ResponseValidity_UseMaxAgeIfAvailable() [Fact] public void FinalizeCacheHeadersAsync_ResponseValidity_UseSharedMaxAgeIfAvailable() { - var timeProvider = new TestTimeProvider(); + var timeProvider = new MockTimeProvider(); var sink = new TestSink(); var options = new OutputCacheOptions { diff --git a/src/Middleware/OutputCaching/test/TestUtils.cs b/src/Middleware/OutputCaching/test/TestUtils.cs index 615cec55e535..b745adae97dc 100644 --- a/src/Middleware/OutputCaching/test/TestUtils.cs +++ b/src/Middleware/OutputCaching/test/TestUtils.cs @@ -347,25 +347,6 @@ public ValueTask SetAsync(string key, byte[] entry, string[]? tags, TimeSpan val } } -internal class TestTimeProvider : TimeProvider -{ - private DateTimeOffset _current; - - public TestTimeProvider() : this(DateTimeOffset.UtcNow) { } - - public TestTimeProvider(DateTimeOffset current) - { - _current = current; - } - - public override DateTimeOffset GetUtcNow() => _current; - - public void Advance(TimeSpan timeSpan) - { - _current += timeSpan; - } -} - internal class AllowTestPolicy : IOutputCachePolicy { public ValueTask CacheRequestAsync(OutputCacheContext context, CancellationToken cancellationToken) diff --git a/src/Middleware/ResponseCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests.csproj b/src/Middleware/ResponseCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests.csproj index dd7bcb50d1df..f07480e34325 100644 --- a/src/Middleware/ResponseCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests.csproj +++ b/src/Middleware/ResponseCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests.csproj @@ -10,6 +10,10 @@ + + + + diff --git a/src/Middleware/ResponseCaching/test/ResponseCachingMiddlewareTests.cs b/src/Middleware/ResponseCaching/test/ResponseCachingMiddlewareTests.cs index 212c77caf4fb..65451ba8dbd7 100644 --- a/src/Middleware/ResponseCaching/test/ResponseCachingMiddlewareTests.cs +++ b/src/Middleware/ResponseCaching/test/ResponseCachingMiddlewareTests.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging.Testing; using Microsoft.Extensions.Primitives; @@ -356,7 +357,7 @@ public void ContentIsNotModified_IfNoneMatch_MatchesAtLeastOneValue_True() [Fact] public void StartResponseAsync_IfAllowResponseCaptureIsTrue_SetsResponseTime() { - var timeProvider = new TestTimeProvider(); + var timeProvider = new MockTimeProvider(); var middleware = TestUtils.CreateTestMiddleware(options: new ResponseCachingOptions { TimeProvider = timeProvider @@ -372,7 +373,7 @@ public void StartResponseAsync_IfAllowResponseCaptureIsTrue_SetsResponseTime() [Fact] public void StartResponseAsync_IfAllowResponseCaptureIsTrue_SetsResponseTimeOnlyOnce() { - var timeProvider = new TestTimeProvider(); + var timeProvider = new MockTimeProvider(); var middleware = TestUtils.CreateTestMiddleware(options: new ResponseCachingOptions { TimeProvider = timeProvider @@ -442,7 +443,10 @@ public void FinalizeCacheHeadersAsync_DefaultResponseValidity_Is10Seconds() [Fact] public void FinalizeCacheHeadersAsync_ResponseValidity_UseExpiryIfAvailable() { - var timeProvider = new TestTimeProvider(DateTimeOffset.MinValue); + var timeProvider = new MockTimeProvider(); + var now = timeProvider.GetUtcNow(); + now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second + 1, now.Offset); // Round up to seconds. + timeProvider.AdvanceTo(now); var sink = new TestSink(); var middleware = TestUtils.CreateTestMiddleware(testSink: sink, options: new ResponseCachingOptions { @@ -451,7 +455,7 @@ public void FinalizeCacheHeadersAsync_ResponseValidity_UseExpiryIfAvailable() var context = TestUtils.CreateTestContext(); context.ResponseTime = timeProvider.GetUtcNow(); - context.HttpContext.Response.Headers.Expires = HeaderUtilities.FormatDate(timeProvider.GetUtcNow() + TimeSpan.FromSeconds(11)); + context.HttpContext.Response.Headers.Expires = HeaderUtilities.FormatDate(now + TimeSpan.FromSeconds(11)); middleware.FinalizeCacheHeaders(context); @@ -462,7 +466,7 @@ public void FinalizeCacheHeadersAsync_ResponseValidity_UseExpiryIfAvailable() [Fact] public void FinalizeCacheHeadersAsync_ResponseValidity_UseMaxAgeIfAvailable() { - var timeProvider = new TestTimeProvider(); + var timeProvider = new MockTimeProvider(); var sink = new TestSink(); var middleware = TestUtils.CreateTestMiddleware(testSink: sink, options: new ResponseCachingOptions { @@ -487,7 +491,7 @@ public void FinalizeCacheHeadersAsync_ResponseValidity_UseMaxAgeIfAvailable() [Fact] public void FinalizeCacheHeadersAsync_ResponseValidity_UseSharedMaxAgeIfAvailable() { - var timeProvider = new TestTimeProvider(); + var timeProvider = new MockTimeProvider(); var sink = new TestSink(); var middleware = TestUtils.CreateTestMiddleware(testSink: sink, options: new ResponseCachingOptions { diff --git a/src/Middleware/ResponseCaching/test/TestUtils.cs b/src/Middleware/ResponseCaching/test/TestUtils.cs index 14b5c211cb34..a3a64267e550 100644 --- a/src/Middleware/ResponseCaching/test/TestUtils.cs +++ b/src/Middleware/ResponseCaching/test/TestUtils.cs @@ -389,22 +389,3 @@ public void Set(string key, IResponseCacheEntry entry, TimeSpan validFor) _storage[key] = entry; } } - -internal class TestTimeProvider : TimeProvider -{ - private DateTimeOffset _current; - - public TestTimeProvider() : this(DateTimeOffset.UtcNow) { } - - public TestTimeProvider(DateTimeOffset current) - { - _current = current; - } - - public override DateTimeOffset GetUtcNow() => _current; - - public void Advance(TimeSpan timeSpan) - { - _current += timeSpan; - } -} diff --git a/src/Security/Authentication/test/CertificateTests.cs b/src/Security/Authentication/test/CertificateTests.cs index 32fb14180fb7..f7b7d09c8b21 100644 --- a/src/Security/Authentication/test/CertificateTests.cs +++ b/src/Security/Authentication/test/CertificateTests.cs @@ -688,7 +688,8 @@ public async Task VerifyValidationResultNeverCachedAfter30Min(bool cache) { const string Expected = "John Doe"; var validationCount = 0; - var timeProvider = new TestTimeProvider(); + // The test certs are generated based off UtcNow. + var timeProvider = new MockTimeProvider(TimeProvider.System.GetUtcNow()); using var host = await CreateHost( new CertificateAuthenticationOptions diff --git a/src/Security/Authentication/test/CookieTests.cs b/src/Security/Authentication/test/CookieTests.cs index 2a0e76a631e4..e9f89dd6e04d 100644 --- a/src/Security/Authentication/test/CookieTests.cs +++ b/src/Security/Authentication/test/CookieTests.cs @@ -21,7 +21,7 @@ namespace Microsoft.AspNetCore.Authentication.Cookies; public class CookieTests : SharedAuthenticationTests { - private readonly TestTimeProvider _timeProvider = new(); + private readonly MockTimeProvider _timeProvider = new(); protected override string DefaultScheme => CookieAuthenticationDefaults.AuthenticationScheme; protected override Type HandlerType => typeof(CookieAuthenticationHandler); diff --git a/src/Security/Authentication/test/Microsoft.AspNetCore.Authentication.Test.csproj b/src/Security/Authentication/test/Microsoft.AspNetCore.Authentication.Test.csproj index 2d5f2deb7350..6cc293b1b3da 100644 --- a/src/Security/Authentication/test/Microsoft.AspNetCore.Authentication.Test.csproj +++ b/src/Security/Authentication/test/Microsoft.AspNetCore.Authentication.Test.csproj @@ -14,6 +14,7 @@ + PreserveNewest diff --git a/src/Security/Authentication/test/SharedAuthenticationTests.cs b/src/Security/Authentication/test/SharedAuthenticationTests.cs index 1dbdcdad4291..28ca03bc68df 100644 --- a/src/Security/Authentication/test/SharedAuthenticationTests.cs +++ b/src/Security/Authentication/test/SharedAuthenticationTests.cs @@ -4,13 +4,14 @@ using System.Security.Claims; using Microsoft.AspNetCore.Authentication.Tests; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.DependencyInjection; namespace Microsoft.AspNetCore.Authentication; public abstract class SharedAuthenticationTests where TOptions : AuthenticationSchemeOptions { - protected TestTimeProvider TimeProvider { get; } = new(); + protected MockTimeProvider TimeProvider { get; } = new(); protected abstract string DefaultScheme { get; } protected virtual string DisplayName { get; } diff --git a/src/Security/Authentication/test/TestTimeProvider.cs b/src/Security/Authentication/test/TestTimeProvider.cs deleted file mode 100644 index aa168482b93d..000000000000 --- a/src/Security/Authentication/test/TestTimeProvider.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.AspNetCore.Authentication; - -public class TestTimeProvider : TimeProvider -{ - private DateTimeOffset _current; - - public TestTimeProvider() : this(DateTimeOffset.UtcNow) { } - - public TestTimeProvider(DateTimeOffset current) - { - _current = current; - } - - public override DateTimeOffset GetUtcNow() => _current; - - public void Advance(TimeSpan timeSpan) - { - _current += timeSpan; - } -} diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/DateHeaderValueManager.cs b/src/Servers/Kestrel/Core/src/Internal/Http/DateHeaderValueManager.cs index 31ad48241a1c..f3d9eb3b7d95 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/DateHeaderValueManager.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/DateHeaderValueManager.cs @@ -15,8 +15,15 @@ internal sealed class DateHeaderValueManager : IHeartbeatHandler // This uses C# compiler's ability to refer to static data directly. For more information see https://vcsjones.dev/2019/02/01/csharp-readonly-span-bytes-static private static ReadOnlySpan DatePreambleBytes => "\r\nDate: "u8; + public TimeProvider _timeProvider; + private DateHeaderValues? _dateValues; + public DateHeaderValueManager(TimeProvider timeProvider) + { + _timeProvider = timeProvider; + } + /// /// Returns a value representing the current server date/time for use in the HTTP "Date" response header /// in accordance with http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.18 @@ -25,17 +32,17 @@ internal sealed class DateHeaderValueManager : IHeartbeatHandler public DateHeaderValues GetDateHeaderValues() => _dateValues!; // Called by the Timer (background) thread - public void OnHeartbeat(DateTimeOffset now) + public void OnHeartbeat() { - SetDateValues(now); + SetDateValues(); } /// /// Sets date values from a provided ticks value /// - /// A DateTimeOffset value - private void SetDateValues(DateTimeOffset value) + private void SetDateValues() { + var value = _timeProvider.GetUtcNow(); var dateValue = HeaderUtilities.FormatDate(value); var dateBytes = new byte[DatePreambleBytes.Length + dateValue.Length]; DatePreambleBytes.CopyTo(dateBytes); diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs b/src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs index 373c8f5b8774..3a86d96e9a57 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs @@ -28,8 +28,6 @@ internal partial class Http1Connection : HttpProtocol, IRequestProcessor, IHttpO private readonly HttpConnectionContext _context; private readonly IHttpParser _parser; private readonly Http1OutputProducer _http1Output; - protected readonly long _keepAliveTicks; - private readonly long _requestHeadersTimeoutTicks; private volatile bool _requestTimedOut; private uint _requestCount; @@ -55,8 +53,6 @@ public Http1Connection(HttpConnectionContext context) _context = context; _parser = ServiceContext.HttpParser; - _keepAliveTicks = ServerOptions.Limits.KeepAliveTimeout.Ticks; - _requestHeadersTimeoutTicks = ServerOptions.Limits.RequestHeadersTimeout.Ticks; _http1Output = new Http1OutputProducer( _context.Transport.Output, @@ -166,7 +162,7 @@ public bool ParseRequest(ref SequenceReader reader) break; } - TimeoutControl.ResetTimeout(_requestHeadersTimeoutTicks, TimeoutReason.RequestHeaders); + TimeoutControl.ResetTimeout(ServerOptions.Limits.RequestHeadersTimeout, TimeoutReason.RequestHeaders); _requestProcessingStatus = RequestProcessingStatus.ParsingRequestLine; goto case RequestProcessingStatus.ParsingRequestLine; @@ -671,7 +667,7 @@ protected override void BeginRequestProcessing() // Reset the features and timeout. Reset(); _requestCount++; - TimeoutControl.SetTimeout(_keepAliveTicks, TimeoutReason.KeepAlive); + TimeoutControl.SetTimeout(ServerOptions.Limits.KeepAliveTimeout, TimeoutReason.KeepAlive); } protected override bool BeginRead(out ValueTask awaitable) @@ -790,5 +786,5 @@ protected override Task TryProduceInvalidRequestResponse() return base.TryProduceInvalidRequestResponse(); } - void IRequestProcessor.Tick(DateTimeOffset now) { } + void IRequestProcessor.Tick(long timestamp) { } } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/Http1MessageBody.cs b/src/Servers/Kestrel/Core/src/Internal/Http/Http1MessageBody.cs index a8c2916c0247..f0d34685e004 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/Http1MessageBody.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/Http1MessageBody.cs @@ -83,7 +83,7 @@ protected async Task OnConsumeAsyncAwaited() { Log.RequestBodyNotEntirelyRead(_context.ConnectionIdFeature, _context.TraceIdentifier); - _context.TimeoutControl.SetTimeout(Constants.RequestBodyDrainTimeout.Ticks, TimeoutReason.RequestBodyDrain); + _context.TimeoutControl.SetTimeout(Constants.RequestBodyDrainTimeout, TimeoutReason.RequestBodyDrain); try { diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs index a7ee8515a09d..32a323e09b10 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs @@ -44,7 +44,7 @@ internal sealed partial class Http2Connection : IHttp2StreamLifetimeHandler, IHt private const int InitialStreamPoolSize = 5; private const int MaxStreamPoolSize = 100; - private const long StreamPoolExpiryTicks = TimeSpan.TicksPerSecond * 5; + private readonly TimeSpan StreamPoolExpiry = TimeSpan.FromSeconds(5); private readonly HttpConnectionContext _context; private readonly ConnectionMetricsContext _metricsContext; @@ -113,7 +113,7 @@ public Http2Connection(HttpConnectionContext context) _keepAlive = new Http2KeepAlive( http2Limits.KeepAlivePingDelay, http2Limits.KeepAlivePingTimeout, - context.ServiceContext.SystemClock); + context.ServiceContext.TimeProvider); } _serverSettings.MaxConcurrentStreams = (uint)http2Limits.MaxStreamsPerConnection; @@ -147,7 +147,7 @@ public Http2Connection(HttpConnectionContext context) public KestrelTrace Log => _context.ServiceContext.Log; public IFeatureCollection ConnectionFeatures => _context.ConnectionFeatures; - public ISystemClock SystemClock => _context.ServiceContext.SystemClock; + public TimeProvider TimeProvider => _context.ServiceContext.TimeProvider; public ITimeoutControl TimeoutControl => _context.TimeoutControl; public KestrelServerLimits Limits => _context.ServiceContext.ServerOptions.Limits; @@ -211,7 +211,7 @@ public async Task ProcessRequestsAsync(IHttpApplication appl ValidateTlsRequirements(); TimeoutControl.InitializeHttp2(_inputFlowControl); - TimeoutControl.SetTimeout(Limits.KeepAliveTimeout.Ticks, TimeoutReason.KeepAlive); + TimeoutControl.SetTimeout(Limits.KeepAliveTimeout, TimeoutReason.KeepAlive); if (!await TryReadPrefaceAsync()) { @@ -242,7 +242,7 @@ public async Task ProcessRequestsAsync(IHttpApplication appl if (result.IsCanceled) { // Heartbeat will cancel ReadAsync and trigger expiring unused streams from pool. - StreamPool.RemoveExpired(SystemClock.UtcNowTicks); + StreamPool.RemoveExpired(TimeProvider.GetTimestamp()); } try @@ -731,7 +731,7 @@ private Task ProcessHeadersFrameAsync(IHttpApplication appli if (!_incomingFrame.HeadersEndHeaders) { - TimeoutControl.SetTimeout(Limits.RequestHeadersTimeout.Ticks, TimeoutReason.RequestHeaders); + TimeoutControl.SetTimeout(Limits.RequestHeadersTimeout, TimeoutReason.RequestHeaders); } // Start a new stream @@ -947,7 +947,7 @@ private Task ProcessPingFrameAsync(in ReadOnlySequence payload) // Incoming ping resets connection keep alive timeout if (TimeoutControl.TimerReason == TimeoutReason.KeepAlive) { - TimeoutControl.ResetTimeout(Limits.KeepAliveTimeout.Ticks, TimeoutReason.KeepAlive); + TimeoutControl.ResetTimeout(Limits.KeepAliveTimeout, TimeoutReason.KeepAlive); } if (_incomingFrame.PingAck) @@ -1244,7 +1244,7 @@ private void AbortStream(int streamId, IOException error) } } - void IRequestProcessor.Tick(DateTimeOffset now) + void IRequestProcessor.Tick(long timestamp) { Input.CancelPendingRead(); } @@ -1258,7 +1258,7 @@ void IHttp2StreamLifetimeHandler.OnStreamCompleted(Http2Stream stream) private void UpdateCompletedStreams() { Http2Stream? firstRequedStream = null; - var now = SystemClock.UtcNowTicks; + var timestamp = TimeProvider.GetTimestamp(); while (_completedStreams.TryDequeue(out var stream)) { @@ -1270,13 +1270,13 @@ private void UpdateCompletedStreams() break; } - if (stream.DrainExpirationTicks == default) + if (stream.DrainExpirationTimestamp == default) { _serverActiveStreamCount--; - stream.DrainExpirationTicks = now + Constants.RequestBodyDrainTimeout.Ticks; + stream.DrainExpirationTimestamp = TimeProvider.GetTimestamp(timestamp, Constants.RequestBodyDrainTimeout); } - if (stream.EndStreamReceived || stream.RstStreamReceived || stream.DrainExpirationTicks < now) + if (stream.EndStreamReceived || stream.RstStreamReceived || stream.DrainExpirationTimestamp < timestamp) { if (stream == _currentHeadersStream) { @@ -1307,7 +1307,7 @@ private void RemoveStream(Http2Stream stream) // Pool and reuse the stream if it finished in a graceful state and there is space in the pool. // This property is used to remove unused streams from the pool - stream.DrainExpirationTicks = SystemClock.UtcNowTicks + StreamPoolExpiryTicks; + stream.DrainExpirationTimestamp = TimeProvider.GetTimestamp(StreamPoolExpiry); StreamPool.Push(stream); } @@ -1325,7 +1325,7 @@ private void MakeSpaceInDrainQueue() // If we're tracking too many streams, discard the oldest. while (_streams.Count >= maxStreams && _completedStreams.TryDequeue(out var stream)) { - if (stream.DrainExpirationTicks == default) + if (stream.DrainExpirationTimestamp == default) { _serverActiveStreamCount--; } @@ -1366,7 +1366,7 @@ private void UpdateConnectionState() { if (TimeoutControl.TimerReason == TimeoutReason.None) { - TimeoutControl.SetTimeout(Limits.KeepAliveTimeout.Ticks, TimeoutReason.KeepAlive); + TimeoutControl.SetTimeout(Limits.KeepAliveTimeout, TimeoutReason.KeepAlive); } // If we're awaiting headers, either a new stream will be started, or there will be a connection diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2KeepAlive.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2KeepAlive.cs index 03ae6468568d..166412f394da 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2KeepAlive.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2KeepAlive.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Buffers; -using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2; @@ -19,31 +18,31 @@ internal sealed class Http2KeepAlive // An empty ping payload internal static readonly ReadOnlySequence PingPayload = new ReadOnlySequence(new byte[8]); - private readonly TimeSpan _keepAliveInterval; - private readonly TimeSpan _keepAliveTimeout; - private readonly ISystemClock _systemClock; + private readonly long _keepAliveInterval; + private readonly long _keepAliveTimeout; + private readonly TimeProvider _timeProvider; private long _lastFrameReceivedTimestamp; private long _pingSentTimestamp; // Internal for testing internal KeepAliveState _state; - public Http2KeepAlive(TimeSpan keepAliveInterval, TimeSpan keepAliveTimeout, ISystemClock systemClock) + public Http2KeepAlive(TimeSpan keepAliveInterval, TimeSpan keepAliveTimeout, TimeProvider timeProvider) { - _keepAliveInterval = keepAliveInterval; - _keepAliveTimeout = keepAliveTimeout; - _systemClock = systemClock; + _keepAliveInterval = keepAliveInterval.ToTicks(timeProvider); + _keepAliveTimeout = keepAliveTimeout == TimeSpan.MaxValue ? long.MaxValue + : keepAliveTimeout.ToTicks(timeProvider); + _timeProvider = timeProvider; } public KeepAliveState ProcessKeepAlive(bool frameReceived) { - var timestamp = _systemClock.UtcNowTicks; + var timestamp = _timeProvider.GetTimestamp(); if (frameReceived) { - // System clock only has 1 second of precision, so the clock could be up to 1 second in the past. - // To err on the side of caution, add a second to the clock when calculating the ping sent time. - _lastFrameReceivedTimestamp = timestamp + TimeSpan.TicksPerSecond; + // To err on the side of caution, add a second to the time when calculating the ping sent time. + _lastFrameReceivedTimestamp = timestamp + _timeProvider.TimestampFrequency; // Any frame received after the keep alive interval is exceeded resets the state back to none. if (_state == KeepAliveState.PingSent) @@ -58,23 +57,22 @@ public KeepAliveState ProcessKeepAlive(bool frameReceived) { case KeepAliveState.None: // Check whether keep alive interval has passed since last frame received - if (timestamp > (_lastFrameReceivedTimestamp + _keepAliveInterval.Ticks)) + if (timestamp > (_lastFrameReceivedTimestamp + _keepAliveInterval)) { // Ping will be sent immeditely after this method finishes. // Set the status directly to ping sent and set the timestamp _state = KeepAliveState.PingSent; - // System clock only has 1 second of precision, so the clock could be up to 1 second in the past. - // To err on the side of caution, add a second to the clock when calculating the ping sent time. - _pingSentTimestamp = _systemClock.UtcNowTicks + TimeSpan.TicksPerSecond; + // To err on the side of caution, add a second to the time when calculating the ping sent time. + _pingSentTimestamp = timestamp + _timeProvider.TimestampFrequency; // Indicate that the ping needs to be sent. This is only returned once return KeepAliveState.SendPing; } break; case KeepAliveState.PingSent: - if (_keepAliveTimeout != TimeSpan.MaxValue) + if (_keepAliveTimeout != long.MaxValue) { - if (timestamp > (_pingSentTimestamp + _keepAliveTimeout.Ticks)) + if (timestamp > (_pingSentTimestamp + _keepAliveTimeout)) { _state = KeepAliveState.Timeout; } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs index 9a10e29d58d7..5ee7fc967785 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs @@ -29,7 +29,7 @@ internal abstract partial class Http2Stream : HttpProtocol, IThreadPoolWorkItem, public Pipe RequestBodyPipe { get; private set; } = default!; - internal long DrainExpirationTicks { get; set; } + internal long DrainExpirationTimestamp { get; set; } private StreamCompletionFlags _completionState; private readonly object _completionLock = new object(); @@ -42,7 +42,7 @@ public void Initialize(Http2StreamContext context) _completionState = StreamCompletionFlags.None; InputRemaining = null; RequestBodyStarted = false; - DrainExpirationTicks = 0; + DrainExpirationTimestamp = 0; TotalParsedHeaderSize = 0; // Allow up to 2x during parsing, enforce the hard limit after when we can preserve the connection. _eagerRequestHeadersParsedLimit = ServerOptions.Limits.MaxRequestHeaderCount * 2; @@ -725,5 +725,5 @@ void IPooledStream.DisposeCore() Dispose(); } - long IPooledStream.PoolExpirationTicks => DrainExpirationTicks; + long IPooledStream.PoolExpirationTimestamp => DrainExpirationTimestamp; } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs index 8597cadfd7df..6f032ffb07b9 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs @@ -192,7 +192,7 @@ public void Abort(ConnectionAbortedException ex, Http3ErrorCode errorCode) } } - public void Tick(DateTimeOffset now) + public void Tick(long timestamp) { if (_aborted) { @@ -201,14 +201,12 @@ public void Tick(DateTimeOffset now) return; } - ValidateOpenControlStreams(now); - UpdateStreamTimeouts(now); + ValidateOpenControlStreams(timestamp); + UpdateStreamTimeouts(timestamp); } - private void ValidateOpenControlStreams(DateTimeOffset now) + private void ValidateOpenControlStreams(long timestamp) { - var ticks = now.Ticks; - // This method validates that a connnection's control streams are open. // // They're checked on a delayed timer because when a connection is aborted or timed out, notifications are sent to open streams @@ -218,24 +216,24 @@ private void ValidateOpenControlStreams(DateTimeOffset now) // // Realistically, control streams are never closed except when the connection is. A small delay in aborting the connection in the // unlikely situation where a control stream is incorrectly closed should be fine. - ValidateOpenControlStream(OutboundControlStream, this, ticks); - ValidateOpenControlStream(ControlStream, this, ticks); - ValidateOpenControlStream(EncoderStream, this, ticks); - ValidateOpenControlStream(DecoderStream, this, ticks); + ValidateOpenControlStream(OutboundControlStream, this, timestamp); + ValidateOpenControlStream(ControlStream, this, timestamp); + ValidateOpenControlStream(EncoderStream, this, timestamp); + ValidateOpenControlStream(DecoderStream, this, timestamp); - static void ValidateOpenControlStream(Http3ControlStream? stream, Http3Connection connection, long ticks) + static void ValidateOpenControlStream(Http3ControlStream? stream, Http3Connection connection, long timestamp) { if (stream != null) { if (stream.IsCompleted || stream.IsAborted || stream.EndStreamReceived) { // If a control stream is no longer active then set a timeout so that the connection is aborted next tick. - if (stream.StreamTimeoutTicks == default) + if (stream.StreamTimeoutTimestamp == default) { - stream.StreamTimeoutTicks = ticks; + stream.StreamTimeoutTimestamp = timestamp; } - if (stream.StreamTimeoutTicks < ticks) + if (stream.StreamTimeoutTimestamp < timestamp) { connection.OnStreamConnectionError(new Http3ConnectionErrorException("A control stream used by the connection was closed or reset.", Http3ErrorCode.ClosedCriticalStream)); } @@ -244,28 +242,29 @@ static void ValidateOpenControlStream(Http3ControlStream? stream, Http3Connectio } } - private void UpdateStreamTimeouts(DateTimeOffset now) + private void UpdateStreamTimeouts(long timestamp) { // This method checks for timeouts: // 1. When a stream first starts and waits to receive headers. // Uses RequestHeadersTimeout. // 2. When a stream finished and is waiting for underlying transport to drain. // Uses MinResponseDataRate. - - var ticks = now.Ticks; + var serviceContext = _context.ServiceContext; + var requestHeadersTimeout = serviceContext.ServerOptions.Limits.RequestHeadersTimeout.ToTicks( + serviceContext.TimeProvider); lock (_unidentifiedStreams) { foreach (var stream in _unidentifiedStreams.Values) { - if (stream.StreamTimeoutTicks == default) + if (stream.StreamTimeoutTimestamp == default) { // On expiration overflow, use max value. - var expirationTicks = ticks + _context.ServiceContext.ServerOptions.Limits.RequestHeadersTimeout.Ticks; - stream.StreamTimeoutTicks = expirationTicks >= 0 ? expirationTicks : long.MaxValue; + var expiration = timestamp + requestHeadersTimeout; + stream.StreamTimeoutTimestamp = expiration >= 0 ? expiration : long.MaxValue; } - if (stream.StreamTimeoutTicks < ticks) + if (stream.StreamTimeoutTimestamp < timestamp) { stream.Abort(new("Stream timed out before its type was determined.")); } @@ -278,14 +277,14 @@ private void UpdateStreamTimeouts(DateTimeOffset now) { if (stream.IsReceivingHeader) { - if (stream.StreamTimeoutTicks == default) + if (stream.StreamTimeoutTimestamp == default) { // On expiration overflow, use max value. - var expirationTicks = ticks + _context.ServiceContext.ServerOptions.Limits.RequestHeadersTimeout.Ticks; - stream.StreamTimeoutTicks = expirationTicks >= 0 ? expirationTicks : long.MaxValue; + var expiration = timestamp + requestHeadersTimeout; + stream.StreamTimeoutTimestamp = expiration >= 0 ? expiration : long.MaxValue; } - if (stream.StreamTimeoutTicks < ticks) + if (stream.StreamTimeoutTimestamp < timestamp) { if (stream.IsRequestStream) { @@ -305,12 +304,12 @@ private void UpdateStreamTimeouts(DateTimeOffset now) continue; } - if (stream.StreamTimeoutTicks == default) + if (stream.StreamTimeoutTimestamp == default) { - stream.StreamTimeoutTicks = TimeoutControl.GetResponseDrainDeadline(ticks, minDataRate); + stream.StreamTimeoutTimestamp = TimeoutControl.GetResponseDrainDeadline(timestamp, minDataRate); } - if (stream.StreamTimeoutTicks < ticks) + if (stream.StreamTimeoutTimestamp < timestamp) { // Cancel connection to be consistent with other data rate limits. Log.ResponseMinimumDataRateNotSatisfied(_context.ConnectionId, stream.TraceIdentifier); @@ -350,7 +349,7 @@ public async Task ProcessRequestsAsync(IHttpApplication appl outboundControlStreamTask = ProcessOutboundControlStreamAsync(outboundControlStream); // Close the connection if we don't receive any request streams - TimeoutControl.SetTimeout(Limits.KeepAliveTimeout.Ticks, TimeoutReason.KeepAlive); + TimeoutControl.SetTimeout(Limits.KeepAliveTimeout, TimeoutReason.KeepAlive); while (_stoppedAcceptingStreams == 0) { @@ -822,7 +821,7 @@ void IHttp3StreamLifetimeHandler.OnStreamCompleted(IHttp3Stream stream) if (_activeRequestCount == 0) { - TimeoutControl.SetTimeout(Limits.KeepAliveTimeout.Ticks, TimeoutReason.KeepAlive); + TimeoutControl.SetTimeout(Limits.KeepAliveTimeout, TimeoutReason.KeepAlive); } } _streams.Remove(stream.StreamId); diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ControlStream.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ControlStream.cs index fa34f9b6829b..599a55f50212 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ControlStream.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ControlStream.cs @@ -69,7 +69,7 @@ private void OnStreamClosed() public PipeReader Input => _context.Transport.Input; public KestrelTrace Log => _context.ServiceContext.Log; - public long StreamTimeoutTicks { get; set; } + public long StreamTimeoutTimestamp { get; set; } public bool IsReceivingHeader => _headerType == -1; public bool IsDraining => false; public bool IsRequestStream => false; diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3PendingStream.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3PendingStream.cs index af813249434e..b6db0bb810db 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3PendingStream.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3PendingStream.cs @@ -13,12 +13,12 @@ internal sealed class Http3PendingStream internal readonly Http3StreamContext Context; internal readonly long StreamId; - internal long StreamTimeoutTicks; + internal long StreamTimeoutTimestamp; public Http3PendingStream(Http3StreamContext context, long id) { Context = context; - StreamTimeoutTicks = default; + StreamTimeoutTimestamp = default; StreamId = id; } @@ -95,7 +95,7 @@ public async ValueTask ReadNextStreamHeaderAsync(Http3StreamContext contex } } - StreamTimeoutTicks = default; + StreamTimeoutTimestamp = default; } return -1L; diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs index 7e032302d5ce..e0810f4ffd62 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs @@ -70,10 +70,9 @@ internal abstract partial class Http3Stream : HttpProtocol, IHttp3Stream, IHttpS public QPackDecoder QPackDecoder { get; private set; } = default!; public PipeReader Input => _context.Transport.Input; - public ISystemClock SystemClock => _context.ServiceContext.SystemClock; public KestrelServerLimits Limits => _context.ServiceContext.ServerOptions.Limits; public long StreamId => _streamIdFeature.StreamId; - public long StreamTimeoutTicks { get; set; } + public long StreamTimeoutTimestamp { get; set; } public bool IsReceivingHeader => _requestHeaderParsingState <= RequestHeaderParsingState.Headers; // Assigned once headers are received public bool IsDraining => _appCompletedTaskSource.GetStatus() != ValueTaskSourceStatus.Pending; // Draining starts once app is complete public bool IsRequestStream => true; @@ -102,7 +101,7 @@ public void Initialize(Http3StreamContext context) _eagerRequestHeadersParsedLimit = ServerOptions.Limits.MaxRequestHeaderCount * 2; _isMethodConnect = false; _completionState = default; - StreamTimeoutTicks = 0; + StreamTimeoutTimestamp = 0; if (_frameWriter == null) { @@ -859,7 +858,7 @@ private async Task ProcessHeadersFrameAsync(IHttpApplication } _requestHeaderParsingState = RequestHeaderParsingState.Body; - StreamTimeoutTicks = default; + StreamTimeoutTimestamp = default; _context.StreamLifetimeHandler.OnStreamHeaderReceived(this); ThreadPool.UnsafeQueueUserWorkItem(this, preferLocal: false); diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/IHttp3Stream.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/IHttp3Stream.cs index 379b5e98444a..24a61c4b7885 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/IHttp3Stream.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/IHttp3Stream.cs @@ -20,7 +20,7 @@ internal interface IHttp3Stream /// 2. Between when the request delegate is complete and the transport draining. /// Value is driven by . /// - long StreamTimeoutTicks { get; set; } + long StreamTimeoutTimestamp { get; set; } /// /// The stream is receiving the header frame. diff --git a/src/Servers/Kestrel/Core/src/Internal/HttpConnection.cs b/src/Servers/Kestrel/Core/src/Internal/HttpConnection.cs index 89a28d23e494..889b311f30c0 100644 --- a/src/Servers/Kestrel/Core/src/Internal/HttpConnection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/HttpConnection.cs @@ -21,7 +21,7 @@ internal sealed class HttpConnection : ITimeoutHandler private static ReadOnlySpan Http2Id => "h2"u8; private readonly BaseHttpConnectionContext _context; - private readonly ISystemClock _systemClock; + private readonly TimeProvider _timeProvider; private readonly TimeoutControl _timeoutControl; private readonly object _protocolSelectionLock = new object(); @@ -34,9 +34,9 @@ internal sealed class HttpConnection : ITimeoutHandler public HttpConnection(BaseHttpConnectionContext context) { _context = context; - _systemClock = _context.ServiceContext.SystemClock; + _timeProvider = _context.ServiceContext.TimeProvider; - _timeoutControl = new TimeoutControl(this); + _timeoutControl = new TimeoutControl(this, _timeProvider); // Tests override the timeout control sometimes _context.TimeoutControl ??= _timeoutControl; @@ -49,7 +49,7 @@ public async Task ProcessRequestsAsync(IHttpApplication http try { // Ensure TimeoutControl._lastTimestamp is initialized before anything that could set timeouts runs. - _timeoutControl.Initialize(_systemClock.UtcNowTicks); + _timeoutControl.Initialize(); IRequestProcessor? requestProcessor = null; @@ -236,10 +236,9 @@ private void Tick() return; } - // It's safe to use UtcNowUnsynchronized since Tick is called by the Heartbeat. - var now = _systemClock.UtcNowUnsynchronized; - _timeoutControl.Tick(now); - _requestProcessor!.Tick(now); + var timestamp = _timeProvider.GetTimestamp(); + _timeoutControl.Tick(timestamp); + _requestProcessor!.Tick(timestamp); } public void OnTimeout(TimeoutReason reason) diff --git a/src/Servers/Kestrel/Core/src/Internal/IRequestProcessor.cs b/src/Servers/Kestrel/Core/src/Internal/IRequestProcessor.cs index 217b5d501cab..b1b5cfddbb14 100644 --- a/src/Servers/Kestrel/Core/src/Internal/IRequestProcessor.cs +++ b/src/Servers/Kestrel/Core/src/Internal/IRequestProcessor.cs @@ -13,6 +13,6 @@ internal interface IRequestProcessor void HandleRequestHeadersTimeout(); void HandleReadDataRateTimeout(); void OnInputOrOutputCompleted(); - void Tick(DateTimeOffset now); + void Tick(long timestamp); void Abort(ConnectionAbortedException ex); } diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/ConnectionManager.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/ConnectionManager.cs index 9b27d4d8bcdc..fd43496090b6 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/ConnectionManager.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/ConnectionManager.cs @@ -5,8 +5,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; -internal sealed class ConnectionManager +internal sealed class ConnectionManager : IHeartbeatHandler { + private readonly Action _walkCallback; + private long _lastConnectionId = long.MinValue; private readonly ConcurrentDictionary _connectionReferences = new ConcurrentDictionary(); @@ -21,6 +23,7 @@ public ConnectionManager(KestrelTrace trace, ResourceCounter upgradedConnections { UpgradedConnectionCount = upgradedConnections; _trace = trace; + _walkCallback = WalkCallback; } public long GetNewConnectionId() => Interlocked.Increment(ref _lastConnectionId); @@ -30,6 +33,16 @@ public ConnectionManager(KestrelTrace trace, ResourceCounter upgradedConnections /// public ResourceCounter UpgradedConnectionCount { get; } + public void OnHeartbeat() + { + Walk(_walkCallback); + } + + private void WalkCallback(KestrelConnection connection) + { + connection.TickHeartbeat(); + } + public void AddConnection(long id, ConnectionReference connectionReference) { if (!_connectionReferences.TryAdd(id, connectionReference)) diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/Heartbeat.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/Heartbeat.cs index 0895a760e384..b42f66b6a55d 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/Heartbeat.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/Heartbeat.cs @@ -11,17 +11,17 @@ internal sealed class Heartbeat : IDisposable public static readonly TimeSpan Interval = TimeSpan.FromSeconds(1); private readonly IHeartbeatHandler[] _callbacks; - private readonly ISystemClock _systemClock; + private readonly TimeProvider _timeProvider; private readonly IDebugger _debugger; private readonly KestrelTrace _trace; private readonly TimeSpan _interval; private readonly Thread _timerThread; private readonly ManualResetEventSlim _stopEvent; - public Heartbeat(IHeartbeatHandler[] callbacks, ISystemClock systemClock, IDebugger debugger, KestrelTrace trace, TimeSpan interval) + public Heartbeat(IHeartbeatHandler[] callbacks, TimeProvider timeProvider, IDebugger debugger, KestrelTrace trace, TimeSpan interval) { _callbacks = callbacks; - _systemClock = systemClock; + _timeProvider = timeProvider; _debugger = debugger; _trace = trace; _interval = interval; @@ -42,24 +42,22 @@ public void Start() internal void OnHeartbeat() { - var now = _systemClock.UtcNow; + var now = _timeProvider.GetTimestamp(); try { foreach (var callback in _callbacks) { - callback.OnHeartbeat(now); + callback.OnHeartbeat(); } if (!_debugger.IsAttached) { - var after = _systemClock.UtcNow; - - var duration = TimeSpan.FromTicks(after.Ticks - now.Ticks); + var duration = _timeProvider.GetElapsedTime(now); if (duration > _interval) { - _trace.HeartbeatSlow(duration, _interval, now); + _trace.HeartbeatSlow(duration, _interval, _timeProvider.GetUtcNow()); } } } diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/HeartbeatManager.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/HeartbeatManager.cs deleted file mode 100644 index 27239e5b403c..000000000000 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/HeartbeatManager.cs +++ /dev/null @@ -1,37 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; - -internal sealed class HeartbeatManager : IHeartbeatHandler, ISystemClock -{ - private readonly ConnectionManager _connectionManager; - private readonly Action _walkCallback; - private DateTimeOffset _now; - private long _nowTicks; - - public HeartbeatManager(ConnectionManager connectionManager) - { - _connectionManager = connectionManager; - _walkCallback = WalkCallback; - } - - public DateTimeOffset UtcNow => new DateTimeOffset(UtcNowTicks, TimeSpan.Zero); - - public long UtcNowTicks => Volatile.Read(ref _nowTicks); - - public DateTimeOffset UtcNowUnsynchronized => _now; - - public void OnHeartbeat(DateTimeOffset now) - { - _now = now; - Volatile.Write(ref _nowTicks, now.Ticks); - - _connectionManager.Walk(_walkCallback); - } - - private void WalkCallback(KestrelConnection connection) - { - connection.TickHeartbeat(); - } -} diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/IHeartbeatHandler.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/IHeartbeatHandler.cs index 1ef557ab73fb..74e8e723a036 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/IHeartbeatHandler.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/IHeartbeatHandler.cs @@ -5,5 +5,5 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; internal interface IHeartbeatHandler { - void OnHeartbeat(DateTimeOffset now); + void OnHeartbeat(); } diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/ISystemClock.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/ISystemClock.cs deleted file mode 100644 index 24985259f316..000000000000 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/ISystemClock.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; - -/// -/// Abstracts the system clock to facilitate testing. -/// -internal interface ISystemClock -{ - /// - /// Retrieves the current UTC system time. - /// - DateTimeOffset UtcNow { get; } - - /// - /// Retrieves ticks for the current UTC system time. - /// - long UtcNowTicks { get; } - - /// - /// Retrieves the current UTC system time. - /// This is only safe to use from code called by the . - /// - DateTimeOffset UtcNowUnsynchronized { get; } -} diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/ITimeoutControl.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/ITimeoutControl.cs index aba73fe6d688..24cae41baf96 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/ITimeoutControl.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/ITimeoutControl.cs @@ -9,12 +9,12 @@ internal interface ITimeoutControl { TimeoutReason TimerReason { get; } - void SetTimeout(long ticks, TimeoutReason timeoutReason); - void ResetTimeout(long ticks, TimeoutReason timeoutReason); + void SetTimeout(TimeSpan timeout, TimeoutReason timeoutReason); + void ResetTimeout(TimeSpan timeout, TimeoutReason timeoutReason); void CancelTimeout(); void InitializeHttp2(InputFlowControl connectionInputFlowControl); - void Tick(DateTimeOffset now); + void Tick(long timestamp); void StartRequestBody(MinDataRate minRate); void StopRequestBody(); @@ -25,5 +25,5 @@ internal interface ITimeoutControl void StartTimingWrite(); void StopTimingWrite(); void BytesWrittenToBuffer(MinDataRate minRate, long count); - long GetResponseDrainDeadline(long ticks, MinDataRate minRate); + long GetResponseDrainDeadline(long timestamp, MinDataRate minRate); } diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/SystemClock.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/SystemClock.cs deleted file mode 100644 index 662038772a9f..000000000000 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/SystemClock.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; - -/// -/// Provides access to the normal system clock. -/// -internal sealed class SystemClock : ISystemClock -{ - /// - /// Retrieves the current UTC system time. - /// - public DateTimeOffset UtcNow => DateTimeOffset.UtcNow; - - /// - /// Retrieves ticks for the current UTC system time. - /// - public long UtcNowTicks => DateTimeOffset.UtcNow.Ticks; - - /// - /// Retrieves the current UTC system time. - /// - public DateTimeOffset UtcNowUnsynchronized => DateTimeOffset.UtcNow; -} diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/TimeExtensions.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/TimeExtensions.cs new file mode 100644 index 000000000000..94dbba5af700 --- /dev/null +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/TimeExtensions.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System; + +internal static class TimeExtensions +{ + public static long ToTicks(this TimeSpan timeSpan, TimeProvider timeProvider) + => timeSpan.ToTicks(timeProvider.TimestampFrequency); + + public static long ToTicks(this TimeSpan timeSpan, long tickFrequency) + { + if (timeSpan < TimeSpan.Zero) + { + throw new ArgumentOutOfRangeException(nameof(timeSpan), timeSpan, string.Empty); + } + if (timeSpan == TimeSpan.MaxValue) + { + return long.MaxValue; + } + if (tickFrequency == TimeSpan.TicksPerSecond) + { + return timeSpan.Ticks; + } + checked + { + return (long)(timeSpan.Ticks * ((double)tickFrequency / TimeSpan.TicksPerSecond)); + } + } + + public static long GetTimestamp(this TimeProvider timeProvider, TimeSpan timeSpan) + { + return timeProvider.GetTimestamp(timeProvider.GetTimestamp(), timeSpan); + } + + public static long GetTimestamp(this TimeProvider timeProvider, long timeStamp, TimeSpan timeSpan) + { + return timeStamp + timeSpan.ToTicks(timeProvider); + } +} diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/TimeoutControl.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/TimeoutControl.cs index 7ad002a1fd6e..ba907abd7c5b 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/TimeoutControl.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/TimeoutControl.cs @@ -10,12 +10,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; internal sealed class TimeoutControl : ITimeoutControl, IConnectionTimeoutFeature { private readonly ITimeoutHandler _timeoutHandler; + private readonly TimeProvider _timeProvider; + private readonly long _heartbeatIntervalTicks; private long _lastTimestamp; private long _timeoutTimestamp = long.MaxValue; private readonly object _readTimingLock = new object(); private MinDataRate? _minReadRate; + private long _minReadRateGracePeriodTicks; private bool _readTimingEnabled; private bool _readTimingPauseRequested; private long _readTimingElapsedTicks; @@ -29,24 +32,24 @@ internal sealed class TimeoutControl : ITimeoutControl, IConnectionTimeoutFeatur private int _concurrentAwaitingWrites; private long _writeTimingTimeoutTimestamp; - public TimeoutControl(ITimeoutHandler timeoutHandler) + public TimeoutControl(ITimeoutHandler timeoutHandler, TimeProvider timeProvider) { _timeoutHandler = timeoutHandler; + _timeProvider = timeProvider; + _heartbeatIntervalTicks = Heartbeat.Interval.ToTicks(_timeProvider); } public TimeoutReason TimerReason { get; private set; } internal IDebugger Debugger { get; set; } = DebuggerWrapper.Singleton; - internal void Initialize(long nowTicks) + internal void Initialize() { - _lastTimestamp = nowTicks; + Interlocked.Exchange(ref _lastTimestamp, _timeProvider.GetTimestamp()); } - public void Tick(DateTimeOffset now) + public void Tick(long timestamp) { - var timestamp = now.Ticks; - CheckForTimeout(timestamp); CheckForReadDataRateTimeout(timestamp); CheckForWriteDataRateTimeout(timestamp); @@ -108,13 +111,13 @@ private void CheckForReadDataRateTimeout(long timestamp) // Assume overly long tick intervals are the result of server resource starvation. // Don't count extra time between ticks against the rate limit. - _readTimingElapsedTicks += Math.Min(timestamp - _lastTimestamp, Heartbeat.Interval.Ticks); + _readTimingElapsedTicks += Math.Min(timestamp - _lastTimestamp, _heartbeatIntervalTicks); Debug.Assert(_minReadRate != null); - if (_minReadRate.BytesPerSecond > 0 && _readTimingElapsedTicks > _minReadRate.GracePeriod.Ticks) + if (_minReadRate.BytesPerSecond > 0 && _readTimingElapsedTicks > _minReadRateGracePeriodTicks) { - var elapsedSeconds = (double)_readTimingElapsedTicks / TimeSpan.TicksPerSecond; + var elapsedSeconds = (double)_readTimingElapsedTicks / _timeProvider.TimestampFrequency; var rate = _readTimingBytesRead / elapsedSeconds; timeout = rate < _minReadRate.BytesPerSecond && !Debugger.IsAttached; @@ -145,7 +148,7 @@ private void CheckForWriteDataRateTimeout(long timestamp) { // Assume overly long tick intervals are the result of server resource starvation. // Don't count extra time between ticks against the rate limit. - var extraTimeForTick = timestamp - _lastTimestamp - Heartbeat.Interval.Ticks; + var extraTimeForTick = timestamp - _lastTimestamp - _heartbeatIntervalTicks; if (extraTimeForTick > 0) { @@ -162,16 +165,16 @@ private void CheckForWriteDataRateTimeout(long timestamp) } } - public void SetTimeout(long ticks, TimeoutReason timeoutReason) + public void SetTimeout(TimeSpan timeout, TimeoutReason timeoutReason) { Debug.Assert(_timeoutTimestamp == long.MaxValue, "Concurrent timeouts are not supported."); - AssignTimeout(ticks, timeoutReason); + AssignTimeout(timeout, timeoutReason); } - public void ResetTimeout(long ticks, TimeoutReason timeoutReason) + public void ResetTimeout(TimeSpan timeout, TimeoutReason timeoutReason) { - AssignTimeout(ticks, timeoutReason); + AssignTimeout(timeout, timeoutReason); } public void CancelTimeout() @@ -181,12 +184,13 @@ public void CancelTimeout() TimerReason = TimeoutReason.None; } - private void AssignTimeout(long ticks, TimeoutReason timeoutReason) + private void AssignTimeout(TimeSpan timeout, TimeoutReason timeoutReason) { TimerReason = timeoutReason; // Add Heartbeat.Interval since this can be called right before the next heartbeat. - Interlocked.Exchange(ref _timeoutTimestamp, Interlocked.Read(ref _lastTimestamp) + ticks + Heartbeat.Interval.Ticks); + var timeoutTicks = timeout.ToTicks(_timeProvider); + Interlocked.Exchange(ref _timeoutTimestamp, Interlocked.Read(ref _lastTimestamp) + timeoutTicks + _heartbeatIntervalTicks); } public void InitializeHttp2(InputFlowControl connectionInputFlowControl) @@ -202,6 +206,7 @@ public void StartRequestBody(MinDataRate minRate) Debug.Assert(_concurrentIncompleteRequestBodies == 0 || minRate == _minReadRate, "Multiple simultaneous read data rates are not supported."); _minReadRate = minRate; + _minReadRateGracePeriodTicks = minRate.GracePeriod.ToTicks(_timeProvider); _concurrentIncompleteRequestBodies++; if (_concurrentIncompleteRequestBodies == 1) @@ -282,14 +287,14 @@ public void BytesWrittenToBuffer(MinDataRate minRate, long count) lock (_writeTimingLock) { // Add Heartbeat.Interval since this can be called right before the next heartbeat. - var currentTimeUpperBound = Interlocked.Read(ref _lastTimestamp) + Heartbeat.Interval.Ticks; - var ticksToCompleteWriteAtMinRate = TimeSpan.FromSeconds(count / minRate.BytesPerSecond).Ticks; + var currentTimeUpperBound = Interlocked.Read(ref _lastTimestamp) + _heartbeatIntervalTicks; + var ticksToCompleteWriteAtMinRate = TimeSpan.FromSeconds(count / minRate.BytesPerSecond).ToTicks(_timeProvider); // If ticksToCompleteWriteAtMinRate is less than the configured grace period, // allow that write to take up to the grace period to complete. Only add the grace period // to the current time and not to any accumulated timeout. var singleWriteTimeoutTimestamp = currentTimeUpperBound + Math.Max( - minRate.GracePeriod.Ticks, + minRate.GracePeriod.ToTicks(_timeProvider), ticksToCompleteWriteAtMinRate); // Don't penalize a connection for completing previous writes more quickly than required. @@ -316,7 +321,7 @@ void IConnectionTimeoutFeature.SetTimeout(TimeSpan timeSpan) throw new InvalidOperationException(CoreStrings.ConcurrentTimeoutsNotSupported); } - SetTimeout(timeSpan.Ticks, TimeoutReason.TimeoutFeature); + SetTimeout(timeSpan, TimeoutReason.TimeoutFeature); } void IConnectionTimeoutFeature.ResetTimeout(TimeSpan timeSpan) @@ -326,13 +331,13 @@ void IConnectionTimeoutFeature.ResetTimeout(TimeSpan timeSpan) throw new ArgumentException(CoreStrings.PositiveFiniteTimeSpanRequired, nameof(timeSpan)); } - ResetTimeout(timeSpan.Ticks, TimeoutReason.TimeoutFeature); + ResetTimeout(timeSpan, TimeoutReason.TimeoutFeature); } - public long GetResponseDrainDeadline(long ticks, MinDataRate minRate) + public long GetResponseDrainDeadline(long timestamp, MinDataRate minRate) { // On grace period overflow, use max value. - var gracePeriod = ticks + minRate.GracePeriod.Ticks; + var gracePeriod = timestamp + minRate.GracePeriod.ToTicks(_timeProvider); gracePeriod = gracePeriod >= 0 ? gracePeriod : long.MaxValue; return Math.Max(_writeTimingTimeoutTimestamp, gracePeriod); diff --git a/src/Servers/Kestrel/Core/src/Internal/KestrelServerImpl.cs b/src/Servers/Kestrel/Core/src/Internal/KestrelServerImpl.cs index 3fe65ebb4101..e749d52f2016 100644 --- a/src/Servers/Kestrel/Core/src/Internal/KestrelServerImpl.cs +++ b/src/Servers/Kestrel/Core/src/Internal/KestrelServerImpl.cs @@ -83,12 +83,11 @@ private static ServiceContext CreateServiceContext(IOptions(trace.IsEnabled(LogLevel.Information), serverOptions.DisableHttp1LineFeedTerminators), - SystemClock = heartbeatManager, + TimeProvider = TimeProvider.System, DateHeaderValueManager = dateHeaderValueManager, ConnectionManager = connectionManager, Heartbeat = heartbeat, diff --git a/src/Servers/Kestrel/Core/src/Internal/ServiceContext.cs b/src/Servers/Kestrel/Core/src/Internal/ServiceContext.cs index fb7021187c76..66421c4b519e 100644 --- a/src/Servers/Kestrel/Core/src/Internal/ServiceContext.cs +++ b/src/Servers/Kestrel/Core/src/Internal/ServiceContext.cs @@ -21,7 +21,7 @@ internal class ServiceContext public IHttpParser HttpParser { get; set; } = default!; - public ISystemClock SystemClock { get; set; } = default!; + public TimeProvider TimeProvider { get; set; } = default!; public DateHeaderValueManager DateHeaderValueManager { get; set; } = default!; diff --git a/src/Servers/Kestrel/Core/test/DateHeaderValueManagerTests.cs b/src/Servers/Kestrel/Core/test/DateHeaderValueManagerTests.cs index 9c39a580d756..4c0ef56a4314 100644 --- a/src/Servers/Kestrel/Core/test/DateHeaderValueManagerTests.cs +++ b/src/Servers/Kestrel/Core/test/DateHeaderValueManagerTests.cs @@ -1,13 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Logging.Abstractions; using Moq; -using Xunit; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests; @@ -24,10 +22,11 @@ public class DateHeaderValueManagerTests [Fact] public void GetDateHeaderValue_ReturnsDateValueInRFC1123Format() { - var now = DateTimeOffset.UtcNow; + var timeProvider = new MockTimeProvider(); + var now = timeProvider.GetUtcNow(); - var dateHeaderValueManager = new DateHeaderValueManager(); - dateHeaderValueManager.OnHeartbeat(now); + var dateHeaderValueManager = new DateHeaderValueManager(timeProvider); + dateHeaderValueManager.OnHeartbeat(); Assert.Equal(now.ToString(Rfc1123DateFormat), dateHeaderValueManager.GetDateHeaderValues().String); } @@ -35,83 +34,72 @@ public void GetDateHeaderValue_ReturnsDateValueInRFC1123Format() [Fact] public void GetDateHeaderValue_ReturnsCachedValueBetweenTimerTicks() { - var now = DateTimeOffset.UtcNow; - var future = now.AddSeconds(10); - var systemClock = new MockSystemClock - { - UtcNow = now - }; + var timeProvider = new MockTimeProvider(); + var now = timeProvider.GetUtcNow(); - var dateHeaderValueManager = new DateHeaderValueManager(); - dateHeaderValueManager.OnHeartbeat(now); + var dateHeaderValueManager = new DateHeaderValueManager(timeProvider); + dateHeaderValueManager.OnHeartbeat(); var testKestrelTrace = new KestrelTrace(NullLoggerFactory.Instance); - using (var heartbeat = new Heartbeat(new IHeartbeatHandler[] { dateHeaderValueManager }, systemClock, DebuggerWrapper.Singleton, testKestrelTrace, Heartbeat.Interval)) + using (var heartbeat = new Heartbeat(new IHeartbeatHandler[] { dateHeaderValueManager }, timeProvider, DebuggerWrapper.Singleton, testKestrelTrace, Heartbeat.Interval)) { Assert.Equal(now.ToString(Rfc1123DateFormat), dateHeaderValueManager.GetDateHeaderValues().String); - systemClock.UtcNow = future; + timeProvider.Advance(TimeSpan.FromSeconds(10)); Assert.Equal(now.ToString(Rfc1123DateFormat), dateHeaderValueManager.GetDateHeaderValues().String); } - Assert.Equal(0, systemClock.UtcNowCalled); + Assert.Equal(2, timeProvider.UtcNowCalled); } [Fact] public void GetDateHeaderValue_ReturnsUpdatedValueAfterHeartbeat() { - var now = DateTimeOffset.UtcNow; + var timeProvider = new MockTimeProvider(); + var now = timeProvider.GetUtcNow(); var future = now.AddSeconds(10); - var systemClock = new MockSystemClock - { - UtcNow = now - }; - var dateHeaderValueManager = new DateHeaderValueManager(); - dateHeaderValueManager.OnHeartbeat(now); + var dateHeaderValueManager = new DateHeaderValueManager(timeProvider); + dateHeaderValueManager.OnHeartbeat(); var testKestrelTrace = new KestrelTrace(NullLoggerFactory.Instance); var mockHeartbeatHandler = new Mock(); - using (var heartbeat = new Heartbeat(new[] { dateHeaderValueManager, mockHeartbeatHandler.Object }, systemClock, DebuggerWrapper.Singleton, testKestrelTrace, Heartbeat.Interval)) + using (var heartbeat = new Heartbeat(new[] { dateHeaderValueManager, mockHeartbeatHandler.Object }, timeProvider, DebuggerWrapper.Singleton, testKestrelTrace, Heartbeat.Interval)) { heartbeat.OnHeartbeat(); Assert.Equal(now.ToString(Rfc1123DateFormat), dateHeaderValueManager.GetDateHeaderValues().String); // Wait for the next heartbeat before verifying GetDateHeaderValues picks up new time. - systemClock.UtcNow = future; + timeProvider.AdvanceTo(future); heartbeat.OnHeartbeat(); Assert.Equal(future.ToString(Rfc1123DateFormat), dateHeaderValueManager.GetDateHeaderValues().String); - Assert.Equal(4, systemClock.UtcNowCalled); + Assert.Equal(4, timeProvider.UtcNowCalled); } } [Fact] public void GetDateHeaderValue_ReturnsLastDateValueAfterHeartbeatDisposed() { - var now = DateTimeOffset.UtcNow; - var future = now.AddSeconds(10); - var systemClock = new MockSystemClock - { - UtcNow = now - }; + var timeProvider = new MockTimeProvider(); + var now = timeProvider.GetUtcNow(); - var dateHeaderValueManager = new DateHeaderValueManager(); - dateHeaderValueManager.OnHeartbeat(now); + var dateHeaderValueManager = new DateHeaderValueManager(timeProvider); + dateHeaderValueManager.OnHeartbeat(); var testKestrelTrace = new KestrelTrace(NullLoggerFactory.Instance); - using (var heartbeat = new Heartbeat(new IHeartbeatHandler[] { dateHeaderValueManager }, systemClock, DebuggerWrapper.Singleton, testKestrelTrace, Heartbeat.Interval)) + using (var heartbeat = new Heartbeat(new IHeartbeatHandler[] { dateHeaderValueManager }, timeProvider, DebuggerWrapper.Singleton, testKestrelTrace, Heartbeat.Interval)) { heartbeat.OnHeartbeat(); Assert.Equal(now.ToString(Rfc1123DateFormat), dateHeaderValueManager.GetDateHeaderValues().String); } - systemClock.UtcNow = future; + timeProvider.Advance(TimeSpan.FromSeconds(10)); Assert.Equal(now.ToString(Rfc1123DateFormat), dateHeaderValueManager.GetDateHeaderValues().String); } } diff --git a/src/Servers/Kestrel/Core/test/HeartbeatTests.cs b/src/Servers/Kestrel/Core/test/HeartbeatTests.cs index 07f5416c3a01..65d46db9ce29 100644 --- a/src/Servers/Kestrel/Core/test/HeartbeatTests.cs +++ b/src/Servers/Kestrel/Core/test/HeartbeatTests.cs @@ -28,15 +28,14 @@ public async void HeartbeatLoopRunsWithSpecifiedInterval() { var heartbeatCallCount = 0; var tcs = new TaskCompletionSource(); - var systemClock = new MockSystemClock(); + var timeProvider = new MockTimeProvider(); var heartbeatHandler = new Mock(); var debugger = new Mock(); var kestrelTrace = new KestrelTrace(LoggerFactory); - var now = systemClock.UtcNow; var splits = new List(); Stopwatch sw = null; - heartbeatHandler.Setup(h => h.OnHeartbeat(now)).Callback(() => + heartbeatHandler.Setup(h => h.OnHeartbeat()).Callback(() => { heartbeatCallCount++; if (sw == null) @@ -62,7 +61,7 @@ public async void HeartbeatLoopRunsWithSpecifiedInterval() var intervalMs = 300; - using (var heartbeat = new Heartbeat(new[] { heartbeatHandler.Object }, systemClock, debugger.Object, kestrelTrace, TimeSpan.FromMilliseconds(intervalMs))) + using (var heartbeat = new Heartbeat(new[] { heartbeatHandler.Object }, timeProvider, debugger.Object, kestrelTrace, TimeSpan.FromMilliseconds(intervalMs))) { heartbeat.Start(); @@ -99,16 +98,15 @@ static void AssertApproxEqual(double intervalMs, double actualMs) [Fact] public async Task HeartbeatTakingLongerThanIntervalIsLoggedAsWarning() { - var systemClock = new MockSystemClock(); + var timeProvider = new MockTimeProvider(); var heartbeatHandler = new Mock(); var debugger = new Mock(); var kestrelTrace = new KestrelTrace(LoggerFactory); var handlerMre = new ManualResetEventSlim(); var handlerStartedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var now = systemClock.UtcNow; var heartbeatDuration = TimeSpan.FromSeconds(2); - heartbeatHandler.Setup(h => h.OnHeartbeat(now)).Callback(() => + heartbeatHandler.Setup(h => h.OnHeartbeat()).Callback(() => { handlerStartedTcs.SetResult(); handlerMre.Wait(); @@ -117,7 +115,7 @@ public async Task HeartbeatTakingLongerThanIntervalIsLoggedAsWarning() Task blockedHeartbeatTask; - using (var heartbeat = new Heartbeat(new[] { heartbeatHandler.Object }, systemClock, debugger.Object, kestrelTrace, Heartbeat.Interval)) + using (var heartbeat = new Heartbeat(new[] { heartbeatHandler.Object }, timeProvider, debugger.Object, kestrelTrace, Heartbeat.Interval)) { blockedHeartbeatTask = Task.Run(() => heartbeat.OnHeartbeat()); @@ -125,16 +123,16 @@ public async Task HeartbeatTakingLongerThanIntervalIsLoggedAsWarning() } // 2 seconds passes... - systemClock.UtcNow = systemClock.UtcNow.AddSeconds(2); + timeProvider.Advance(TimeSpan.FromSeconds(2)); handlerMre.Set(); await blockedHeartbeatTask.DefaultTimeout(); - heartbeatHandler.Verify(h => h.OnHeartbeat(now), Times.Once()); + heartbeatHandler.Verify(h => h.OnHeartbeat(), Times.Once()); var warningMessage = TestSink.Writes.Single(message => message.LogLevel == LogLevel.Warning).Message; - Assert.Equal($"As of \"{now.ToString(CultureInfo.InvariantCulture)}\", the heartbeat has been running for " + Assert.Equal($"As of \"{timeProvider.GetUtcNow().ToString(CultureInfo.InvariantCulture)}\", the heartbeat has been running for " + $"\"{heartbeatDuration.ToString("c", CultureInfo.InvariantCulture)}\" which is longer than " + $"\"{Heartbeat.Interval.ToString("c", CultureInfo.InvariantCulture)}\". " + "This could be caused by thread pool starvation.", warningMessage); @@ -143,15 +141,14 @@ public async Task HeartbeatTakingLongerThanIntervalIsLoggedAsWarning() [Fact] public async Task HeartbeatTakingLongerThanIntervalIsNotLoggedIfDebuggerAttached() { - var systemClock = new MockSystemClock(); + var timeProvider = new MockTimeProvider(); var heartbeatHandler = new Mock(); var debugger = new Mock(); var kestrelTrace = new KestrelTrace(LoggerFactory); var handlerMre = new ManualResetEventSlim(); var handlerStartedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var now = systemClock.UtcNow; - heartbeatHandler.Setup(h => h.OnHeartbeat(now)).Callback(() => + heartbeatHandler.Setup(h => h.OnHeartbeat()).Callback(() => { handlerStartedTcs.SetResult(); handlerMre.Wait(); @@ -161,7 +158,7 @@ public async Task HeartbeatTakingLongerThanIntervalIsNotLoggedIfDebuggerAttached Task blockedHeartbeatTask; - using (var heartbeat = new Heartbeat(new[] { heartbeatHandler.Object }, systemClock, debugger.Object, kestrelTrace, Heartbeat.Interval)) + using (var heartbeat = new Heartbeat(new[] { heartbeatHandler.Object }, timeProvider, debugger.Object, kestrelTrace, Heartbeat.Interval)) { blockedHeartbeatTask = Task.Run(() => heartbeat.OnHeartbeat()); @@ -169,13 +166,13 @@ public async Task HeartbeatTakingLongerThanIntervalIsNotLoggedIfDebuggerAttached } // 2 seconds passes... - systemClock.UtcNow = systemClock.UtcNow.AddSeconds(2); + timeProvider.Advance(TimeSpan.FromSeconds(2)); handlerMre.Set(); await blockedHeartbeatTask.DefaultTimeout(); - heartbeatHandler.Verify(h => h.OnHeartbeat(now), Times.Once()); + heartbeatHandler.Verify(h => h.OnHeartbeat(), Times.Once()); Assert.Empty(TestSink.Writes.Where(w => w.EventId.Name == "HeartbeatSlow")); } @@ -183,14 +180,14 @@ public async Task HeartbeatTakingLongerThanIntervalIsNotLoggedIfDebuggerAttached [Fact] public void ExceptionFromHeartbeatHandlerIsLoggedAsError() { - var systemClock = new MockSystemClock(); + var timeProvider = new MockTimeProvider(); var heartbeatHandler = new Mock(); var kestrelTrace = new KestrelTrace(LoggerFactory); var ex = new Exception(); - heartbeatHandler.Setup(h => h.OnHeartbeat(systemClock.UtcNow)).Throws(ex); + heartbeatHandler.Setup(h => h.OnHeartbeat()).Throws(ex); - using (var heartbeat = new Heartbeat(new[] { heartbeatHandler.Object }, systemClock, DebuggerWrapper.Singleton, kestrelTrace, Heartbeat.Interval)) + using (var heartbeat = new Heartbeat(new[] { heartbeatHandler.Object }, timeProvider, DebuggerWrapper.Singleton, kestrelTrace, Heartbeat.Interval)) { heartbeat.OnHeartbeat(); } diff --git a/src/Servers/Kestrel/Core/test/Http1/Http1ConnectionTests.cs b/src/Servers/Kestrel/Core/test/Http1/Http1ConnectionTests.cs index cd1cc5b208bd..0a0be038a8ac 100644 --- a/src/Servers/Kestrel/Core/test/Http1/Http1ConnectionTests.cs +++ b/src/Servers/Kestrel/Core/test/Http1/Http1ConnectionTests.cs @@ -450,8 +450,7 @@ public async Task ParseRequestStartsRequestHeadersTimeoutOnFirstByteAvailable() ParseRequest((await _transport.Input.ReadAsync()).Buffer, out _consumed, out _examined); _transport.Input.AdvanceTo(_consumed, _examined); - var expectedRequestHeadersTimeout = _serviceContext.ServerOptions.Limits.RequestHeadersTimeout.Ticks; - _timeoutControl.Verify(cc => cc.ResetTimeout(expectedRequestHeadersTimeout, TimeoutReason.RequestHeaders)); + _timeoutControl.Verify(cc => cc.ResetTimeout(_serviceContext.ServerOptions.Limits.RequestHeadersTimeout, TimeoutReason.RequestHeaders)); } [Fact] @@ -570,7 +569,7 @@ public async Task ProcessRequestsAsyncEnablesKeepAliveTimeout() { var requestProcessingTask = _http1Connection.ProcessRequestsAsync(null); - var expectedKeepAliveTimeout = _serviceContext.ServerOptions.Limits.KeepAliveTimeout.Ticks; + var expectedKeepAliveTimeout = _serviceContext.ServerOptions.Limits.KeepAliveTimeout; _timeoutControl.Verify(cc => cc.SetTimeout(expectedKeepAliveTimeout, TimeoutReason.KeepAlive)); _http1Connection.StopProcessingNextRequest(); diff --git a/src/Servers/Kestrel/Core/test/KestrelServerTests.cs b/src/Servers/Kestrel/Core/test/KestrelServerTests.cs index a6415f735e77..39eea7cbb2cc 100644 --- a/src/Servers/Kestrel/Core/test/KestrelServerTests.cs +++ b/src/Servers/Kestrel/Core/test/KestrelServerTests.cs @@ -711,6 +711,7 @@ public async Task StopAsyncDispatchesSubsequentStopAsyncContinuations() [Fact] public void StartingServerInitializesHeartbeat() { + var timeProvider = new MockTimeProvider(); var testContext = new TestServiceContext { ServerOptions = @@ -720,12 +721,14 @@ public void StartingServerInitializesHeartbeat() new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)) } }, - DateHeaderValueManager = new DateHeaderValueManager() + MockTimeProvider = timeProvider, + TimeProvider = timeProvider, + DateHeaderValueManager = new DateHeaderValueManager(timeProvider) }; testContext.Heartbeat = new Heartbeat( new IHeartbeatHandler[] { testContext.DateHeaderValueManager }, - testContext.MockSystemClock, + timeProvider, DebuggerWrapper.Singleton, testContext.Log, Heartbeat.Interval); @@ -736,11 +739,11 @@ public void StartingServerInitializesHeartbeat() // Ensure KestrelServer is started at a different time than when it was constructed, since we're // verifying the heartbeat is initialized during KestrelServer.StartAsync(). - testContext.MockSystemClock.UtcNow += TimeSpan.FromDays(1); + testContext.MockTimeProvider.Advance(TimeSpan.FromDays(1)); StartDummyApplication(server); - Assert.Equal(HeaderUtilities.FormatDate(testContext.MockSystemClock.UtcNow), + Assert.Equal(HeaderUtilities.FormatDate(testContext.MockTimeProvider.GetUtcNow()), testContext.DateHeaderValueManager.GetDateHeaderValues().String); } } diff --git a/src/Servers/Kestrel/Core/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests.csproj b/src/Servers/Kestrel/Core/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests.csproj index 8ab8fb711da1..1bc7490209d6 100644 --- a/src/Servers/Kestrel/Core/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests.csproj +++ b/src/Servers/Kestrel/Core/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests.csproj @@ -10,6 +10,7 @@ + diff --git a/src/Servers/Kestrel/Core/test/PooledStreamStackTests.cs b/src/Servers/Kestrel/Core/test/PooledStreamStackTests.cs index 087c4c8c9b88..a679399b1854 100644 --- a/src/Servers/Kestrel/Core/test/PooledStreamStackTests.cs +++ b/src/Servers/Kestrel/Core/test/PooledStreamStackTests.cs @@ -25,7 +25,7 @@ public void RemoveExpired_Empty_NoOp() public void RemoveExpired_NoneExpired_NoOp() { var streams = new PooledStreamStack(10); - streams.Push(CreateStream(streamId: 1, expirationTicks: 200)); + streams.Push(CreateStream(streamId: 1, expirationTimestamp: 200)); streams.RemoveExpired(100); @@ -37,7 +37,7 @@ public void RemoveExpired_NoneExpired_NoOp() public void RemoveExpired_OneExpired_ExpiredStreamRemoved() { var streams = new PooledStreamStack(10); - streams.Push(CreateStream(streamId: 1, expirationTicks: 200)); + streams.Push(CreateStream(streamId: 1, expirationTimestamp: 200)); streams.RemoveExpired(300); @@ -49,8 +49,8 @@ public void RemoveExpired_OneExpired_ExpiredStreamRemoved() public void RemoveExpired_MultipleExpired_ExpiredStreamsRemoved() { var streams = new PooledStreamStack(10); - streams.Push(CreateStream(streamId: 1, expirationTicks: 200)); - streams.Push(CreateStream(streamId: 2, expirationTicks: 250)); + streams.Push(CreateStream(streamId: 1, expirationTimestamp: 200)); + streams.Push(CreateStream(streamId: 2, expirationTimestamp: 250)); streams.RemoveExpired(300); @@ -63,8 +63,8 @@ public void RemoveExpired_MultipleExpired_ExpiredStreamsRemoved() public void RemoveExpired_OneExpiredAndOneValid_ExpiredStreamRemoved() { var streams = new PooledStreamStack(10); - streams.Push(CreateStream(streamId: 1, expirationTicks: 200)); - streams.Push(CreateStream(streamId: 2, expirationTicks: 400)); + streams.Push(CreateStream(streamId: 1, expirationTimestamp: 200)); + streams.Push(CreateStream(streamId: 2, expirationTimestamp: 400)); streams.RemoveExpired(300); @@ -77,11 +77,11 @@ public void RemoveExpired_OneExpiredAndOneValid_ExpiredStreamRemoved() public void RemoveExpired_AllExpired_ExpiredStreamRemoved() { var streams = new PooledStreamStack(5); - streams.Push(CreateStream(streamId: 1, expirationTicks: 200)); - streams.Push(CreateStream(streamId: 2, expirationTicks: 200)); - streams.Push(CreateStream(streamId: 3, expirationTicks: 200)); - streams.Push(CreateStream(streamId: 4, expirationTicks: 200)); - streams.Push(CreateStream(streamId: 5, expirationTicks: 200)); + streams.Push(CreateStream(streamId: 1, expirationTimestamp: 200)); + streams.Push(CreateStream(streamId: 2, expirationTimestamp: 200)); + streams.Push(CreateStream(streamId: 3, expirationTimestamp: 200)); + streams.Push(CreateStream(streamId: 4, expirationTimestamp: 200)); + streams.Push(CreateStream(streamId: 5, expirationTimestamp: 200)); streams.RemoveExpired(300); @@ -94,13 +94,13 @@ public void RemoveExpired_AllExpired_ExpiredStreamRemoved() Assert.Equal(default, streams._array[4]); } - private static Http2Stream CreateStream(int streamId, long expirationTicks) + private static Http2Stream CreateStream(int streamId, long expirationTimestamp) { var context = TestContextFactory.CreateHttp2StreamContext(connectionId: "TestConnectionId", streamId: streamId); return new Http2Stream(new DummyApplication(), context) { - DrainExpirationTicks = expirationTicks + DrainExpirationTimestamp = expirationTimestamp }; } } diff --git a/src/Servers/Kestrel/Core/test/StartLineTests.cs b/src/Servers/Kestrel/Core/test/StartLineTests.cs index 701561f367df..feb3ea2580e0 100644 --- a/src/Servers/Kestrel/Core/test/StartLineTests.cs +++ b/src/Servers/Kestrel/Core/test/StartLineTests.cs @@ -524,15 +524,17 @@ public StartLineTests() var pair = DuplexPipe.CreateConnectionPair(options, options); Transport = pair.Transport; + var timeProvider = new MockTimeProvider(); var serviceContext = TestContextFactory.CreateServiceContext( serverOptions: new KestrelServerOptions(), + timeProvider: timeProvider, httpParser: new HttpParser()); var connectionContext = TestContextFactory.CreateHttpConnectionContext( serviceContext: serviceContext, connectionContext: Mock.Of(), transport: Transport, - timeoutControl: new TimeoutControl(timeoutHandler: null), + timeoutControl: new TimeoutControl(timeoutHandler: null, timeProvider), memoryPool: MemoryPool, connectionFeatures: new FeatureCollection()); diff --git a/src/Servers/Kestrel/Core/test/TimeoutControlTests.cs b/src/Servers/Kestrel/Core/test/TimeoutControlTests.cs index 4d40887d8a7c..3562700e30bf 100644 --- a/src/Servers/Kestrel/Core/test/TimeoutControlTests.cs +++ b/src/Servers/Kestrel/Core/test/TimeoutControlTests.cs @@ -1,12 +1,10 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.FlowControl; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Testing; using Moq; -using Xunit; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests; @@ -14,13 +12,13 @@ public class TimeoutControlTests { private readonly Mock _mockTimeoutHandler; private readonly TimeoutControl _timeoutControl; - private readonly MockSystemClock _systemClock; + private readonly MockTimeProvider _timeProvider; public TimeoutControlTests() { _mockTimeoutHandler = new Mock(); - _timeoutControl = new TimeoutControl(_mockTimeoutHandler.Object); - _systemClock = new MockSystemClock(); + _timeProvider = new MockTimeProvider(); + _timeoutControl = new TimeoutControl(_mockTimeoutHandler.Object, _timeProvider); } [Fact] @@ -30,10 +28,10 @@ public void DoesNotTimeOutWhenDebuggerIsAttached() mockDebugger.SetupGet(g => g.IsAttached).Returns(true); _timeoutControl.Debugger = mockDebugger.Object; - var now = DateTimeOffset.Now; - _timeoutControl.Initialize(now.Ticks); - _timeoutControl.SetTimeout(1, TimeoutReason.RequestHeaders); - _timeoutControl.Tick(now.AddTicks(2).Add(Heartbeat.Interval)); + var now = _timeProvider.GetTimestamp(); + _timeoutControl.Initialize(); + _timeoutControl.SetTimeout(TimeSpan.FromTicks(1), TimeoutReason.RequestHeaders); + _timeoutControl.Tick(_timeProvider.GetTimestamp(now + 2, Heartbeat.Interval)); _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); } @@ -65,14 +63,14 @@ public void RequestBodyMinimumDataRateNotEnforcedDuringGracePeriod() var minRate = new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(2)); // Initialize timestamp - var now = DateTimeOffset.UtcNow; - _timeoutControl.Initialize(now.Ticks); + var now = _timeProvider.GetTimestamp(); + _timeoutControl.Initialize(); _timeoutControl.StartRequestBody(minRate); _timeoutControl.StartTimingRead(); // Tick during grace period w/ low data rate - now += TimeSpan.FromSeconds(1); + now += _timeProvider.TimestampFrequency; _timeoutControl.BytesRead(10); _timeoutControl.Tick(now); @@ -80,9 +78,9 @@ public void RequestBodyMinimumDataRateNotEnforcedDuringGracePeriod() _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); // Tick after grace period w/ low data rate - now += TimeSpan.FromSeconds(1); + now += _timeProvider.TimestampFrequency; _timeoutControl.Tick(now); - now += TimeSpan.FromSeconds(1); + now += _timeProvider.TimestampFrequency; _timeoutControl.BytesRead(10); _timeoutControl.Tick(now); @@ -97,21 +95,21 @@ public void RequestBodyDataRateIsAveragedOverTimeSpentReadingRequestBody() var minRate = new MinDataRate(bytesPerSecond: 100, gracePeriod: gracePeriod); // Initialize timestamp - var now = DateTimeOffset.UtcNow; - _timeoutControl.Initialize(now.Ticks); + var now = _timeProvider.GetTimestamp(); + _timeoutControl.Initialize(); _timeoutControl.StartRequestBody(minRate); _timeoutControl.StartTimingRead(); // Set base data rate to 200 bytes/second - now += TimeSpan.FromSeconds(1); + now += _timeProvider.TimestampFrequency; _timeoutControl.Tick(now); - now += TimeSpan.FromSeconds(1); + now += _timeProvider.TimestampFrequency; _timeoutControl.BytesRead(400); _timeoutControl.Tick(now); // Data rate: 200 bytes/second - now += TimeSpan.FromSeconds(1); + now += _timeProvider.TimestampFrequency; _timeoutControl.BytesRead(200); _timeoutControl.Tick(now); @@ -119,7 +117,7 @@ public void RequestBodyDataRateIsAveragedOverTimeSpentReadingRequestBody() _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); // Data rate: 150 bytes/second - now += TimeSpan.FromSeconds(1); + now += _timeProvider.TimestampFrequency; _timeoutControl.BytesRead(0); _timeoutControl.Tick(now); @@ -127,7 +125,7 @@ public void RequestBodyDataRateIsAveragedOverTimeSpentReadingRequestBody() _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); // Data rate: 120 bytes/second - now += TimeSpan.FromSeconds(1); + now += _timeProvider.TimestampFrequency; _timeoutControl.BytesRead(0); _timeoutControl.Tick(now); @@ -135,7 +133,7 @@ public void RequestBodyDataRateIsAveragedOverTimeSpentReadingRequestBody() _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); // Data rate: 100 bytes/second - now += TimeSpan.FromSeconds(1); + now += _timeProvider.TimestampFrequency; _timeoutControl.BytesRead(0); _timeoutControl.Tick(now); @@ -143,7 +141,7 @@ public void RequestBodyDataRateIsAveragedOverTimeSpentReadingRequestBody() _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); // Data rate: ~85 bytes/second - now += TimeSpan.FromSeconds(1); + now += _timeProvider.TimestampFrequency; _timeoutControl.BytesRead(0); _timeoutControl.Tick(now); @@ -157,51 +155,51 @@ public void RequestBodyDataRateNotComputedOnPausedTime() var minRate = new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(2)); // Initialize timestamp - _timeoutControl.Initialize(_systemClock.UtcNow.Ticks); + _timeoutControl.Initialize(); _timeoutControl.StartRequestBody(minRate); _timeoutControl.StartTimingRead(); // Tick at 3s, expected counted time is 3s, expected data rate is 200 bytes/second - _systemClock.UtcNow += TimeSpan.FromSeconds(1); - _timeoutControl.Tick(_systemClock.UtcNow); - _systemClock.UtcNow += TimeSpan.FromSeconds(1); - _timeoutControl.Tick(_systemClock.UtcNow); - _systemClock.UtcNow += TimeSpan.FromSeconds(1); + _timeProvider.Advance(TimeSpan.FromSeconds(1)); + _timeoutControl.Tick(_timeProvider.GetTimestamp()); + _timeProvider.Advance(TimeSpan.FromSeconds(1)); + _timeoutControl.Tick(_timeProvider.GetTimestamp()); + _timeProvider.Advance(TimeSpan.FromSeconds(1)); _timeoutControl.BytesRead(600); - _timeoutControl.Tick(_systemClock.UtcNow); + _timeoutControl.Tick(_timeProvider.GetTimestamp()); // Pause at 3.5s - _systemClock.UtcNow += TimeSpan.FromSeconds(0.5); + _timeProvider.Advance(TimeSpan.FromSeconds(0.5)); _timeoutControl.StopTimingRead(); // Tick at 4s, expected counted time is 4s (first tick after pause goes through), expected data rate is 150 bytes/second - _systemClock.UtcNow += TimeSpan.FromSeconds(0.5); - _timeoutControl.Tick(_systemClock.UtcNow); + _timeProvider.Advance(TimeSpan.FromSeconds(0.5)); + _timeoutControl.Tick(_timeProvider.GetTimestamp()); // Tick at 6s, expected counted time is 4s, expected data rate is 150 bytes/second - _systemClock.UtcNow += TimeSpan.FromSeconds(2); - _timeoutControl.Tick(_systemClock.UtcNow); + _timeProvider.Advance(TimeSpan.FromSeconds(2)); + _timeoutControl.Tick(_timeProvider.GetTimestamp()); // Not timed out _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); // Resume at 6.5s - _systemClock.UtcNow += TimeSpan.FromSeconds(0.5); + _timeProvider.Advance(TimeSpan.FromSeconds(0.5)); _timeoutControl.StartTimingRead(); // Tick at 9s, expected counted time is 6s, expected data rate is 100 bytes/second - _systemClock.UtcNow += TimeSpan.FromSeconds(1.0); - _timeoutControl.Tick(_systemClock.UtcNow); - _systemClock.UtcNow += TimeSpan.FromSeconds(.5); - _timeoutControl.Tick(_systemClock.UtcNow); + _timeProvider.Advance(TimeSpan.FromSeconds(1.0)); + _timeoutControl.Tick(_timeProvider.GetTimestamp()); + _timeProvider.Advance(TimeSpan.FromSeconds(.5)); + _timeoutControl.Tick(_timeProvider.GetTimestamp()); // Not timed out _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); // Tick at 10s, expected counted time is 7s, expected data rate drops below 100 bytes/second - _systemClock.UtcNow += TimeSpan.FromSeconds(1); - _timeoutControl.Tick(_systemClock.UtcNow); + _timeProvider.Advance(TimeSpan.FromSeconds(1)); + _timeoutControl.Tick(_timeProvider.GetTimestamp()); // Timed out _mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.ReadDataRate), Times.Once); @@ -213,40 +211,40 @@ public void ReadTimingNotPausedWhenResumeCalledBeforeNextTick() var minRate = new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(2)); // Initialize timestamp - _timeoutControl.Initialize(_systemClock.UtcNow.Ticks); + _timeoutControl.Initialize(); _timeoutControl.StartRequestBody(minRate); _timeoutControl.StartTimingRead(); // Tick at 2s, expected counted time is 2s, expected data rate is 100 bytes/second - _systemClock.UtcNow += TimeSpan.FromSeconds(1); - _timeoutControl.Tick(_systemClock.UtcNow); - _systemClock.UtcNow += TimeSpan.FromSeconds(1); - _timeoutControl.Tick(_systemClock.UtcNow); + _timeProvider.Advance(TimeSpan.FromSeconds(1)); + _timeoutControl.Tick(_timeProvider.GetTimestamp()); + _timeProvider.Advance(TimeSpan.FromSeconds(1)); + _timeoutControl.Tick(_timeProvider.GetTimestamp()); _timeoutControl.BytesRead(200); // Not timed out _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); // Pause at 2.25s - _systemClock.UtcNow += TimeSpan.FromSeconds(0.25); + _timeProvider.Advance(TimeSpan.FromSeconds(0.25)); _timeoutControl.StopTimingRead(); // Resume at 2.5s - _systemClock.UtcNow += TimeSpan.FromSeconds(0.25); + _timeProvider.Advance(TimeSpan.FromSeconds(0.25)); _timeoutControl.StartTimingRead(); // Tick at 3s, expected counted time is 3s, expected data rate is 100 bytes/second - _systemClock.UtcNow += TimeSpan.FromSeconds(0.5); + _timeProvider.Advance(TimeSpan.FromSeconds(0.5)); _timeoutControl.BytesRead(100); - _timeoutControl.Tick(_systemClock.UtcNow); + _timeoutControl.Tick(_timeProvider.GetTimestamp()); // Not timed out _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); // Tick at 4s, expected counted time is 4s, expected data rate drops below 100 bytes/second - _systemClock.UtcNow += TimeSpan.FromSeconds(1); - _timeoutControl.Tick(_systemClock.UtcNow); + _timeProvider.Advance(TimeSpan.FromSeconds(1)); + _timeoutControl.Tick(_timeProvider.GetTimestamp()); // Timed out _mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.ReadDataRate), Times.Once); @@ -258,27 +256,25 @@ public void ReadTimingNotEnforcedWhenTimeoutIsSet() var timeout = TimeSpan.FromSeconds(5); var minRate = new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(2)); - var startTime = _systemClock.UtcNow; - // Initialize timestamp - _timeoutControl.Initialize(startTime.Ticks); + _timeoutControl.Initialize(); _timeoutControl.StartRequestBody(minRate); _timeoutControl.StartTimingRead(); - _timeoutControl.SetTimeout(timeout.Ticks, TimeoutReason.RequestBodyDrain); + _timeoutControl.SetTimeout(timeout, TimeoutReason.RequestBodyDrain); // Tick beyond grace period with low data rate - _systemClock.UtcNow += TimeSpan.FromSeconds(3); + _timeProvider.Advance(TimeSpan.FromSeconds(3)); _timeoutControl.BytesRead(1); - _timeoutControl.Tick(_systemClock.UtcNow); + _timeoutControl.Tick(_timeProvider.GetTimestamp()); // Not timed out _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); // Tick just past timeout period, adjusted by Heartbeat.Interval - _systemClock.UtcNow = startTime + timeout + Heartbeat.Interval + TimeSpan.FromTicks(1); - _timeoutControl.Tick(_systemClock.UtcNow); + _timeProvider.Advance(TimeSpan.FromSeconds(2) + Heartbeat.Interval + TimeSpan.FromTicks(1)); + _timeoutControl.Tick(_timeProvider.GetTimestamp()); // Timed out _mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.RequestBodyDrain), Times.Once); @@ -292,18 +288,18 @@ public void ReadTimingNotEnforcedWhenLowConnectionInputFlowControlAvailability() var flowControl = new InputFlowControl(initialWindowSize: 2, minWindowSizeIncrement: 1); // Initialize timestamp - var now = DateTimeOffset.UtcNow; - _timeoutControl.Initialize(now.Ticks); + var now = _timeProvider.GetTimestamp(); + _timeoutControl.Initialize(); _timeoutControl.InitializeHttp2(flowControl); _timeoutControl.StartRequestBody(minRate); _timeoutControl.StartTimingRead(); // Tick past grace period - now += TimeSpan.FromSeconds(1); + now += _timeProvider.TimestampFrequency; _timeoutControl.BytesRead(100); _timeoutControl.Tick(now); - now += TimeSpan.FromSeconds(1); + now += _timeProvider.TimestampFrequency; _timeoutControl.BytesRead(100); _timeoutControl.Tick(now); @@ -311,7 +307,7 @@ public void ReadTimingNotEnforcedWhenLowConnectionInputFlowControlAvailability() flowControl.TryAdvance(2); // Read 0 bytes in 1 second - now += TimeSpan.FromSeconds(1); + now += _timeProvider.TimestampFrequency; _timeoutControl.Tick(now); // Not timed out @@ -325,7 +321,7 @@ public void ReadTimingNotEnforcedWhenLowConnectionInputFlowControlAvailability() _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); // Read 0 bytes in 1 second - now += TimeSpan.FromSeconds(1); + now += _timeProvider.TimestampFrequency; _timeoutControl.Tick(now); // Timed out @@ -338,22 +334,22 @@ public void ReadTimingOnlyCountsUpToOneHeartbeatIntervalPerTick() var minRate = new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(2)); // Initialize timestamp - var now = DateTimeOffset.UtcNow; - _timeoutControl.Initialize(now.Ticks); + var now = _timeProvider.GetTimestamp(); + _timeoutControl.Initialize(); _timeoutControl.StartRequestBody(minRate); _timeoutControl.StartTimingRead(); // Tick past grace period - now += TimeSpan.FromSeconds(1); + now += _timeProvider.TimestampFrequency; _timeoutControl.BytesRead(100); _timeoutControl.Tick(now); - now += TimeSpan.FromSeconds(1); + now += _timeProvider.TimestampFrequency; _timeoutControl.BytesRead(100); _timeoutControl.Tick(now); // Read 100 bytes in 2 seconds with a single tick - now += TimeSpan.FromSeconds(2); + now += _timeProvider.TimestampFrequency * 2; _timeoutControl.BytesRead(100); _timeoutControl.Tick(now); @@ -361,10 +357,10 @@ public void ReadTimingOnlyCountsUpToOneHeartbeatIntervalPerTick() _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); // Read 100 bytes in 2 seconds in two ticks - now += TimeSpan.FromSeconds(1); + now += _timeProvider.TimestampFrequency; _timeoutControl.BytesRead(100); _timeoutControl.Tick(now); - now += TimeSpan.FromSeconds(1); + now += _timeProvider.TimestampFrequency; _timeoutControl.Tick(now); // Timed out @@ -377,7 +373,7 @@ public void WriteTimingAbortsConnectionWhenWriteDoesNotCompleteWithMinimumDataRa var minRate = new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(2)); // Initialize timestamp - _timeoutControl.Initialize(_systemClock.UtcNow.Ticks); + _timeoutControl.Initialize(); // Should complete within 4 seconds, but the timeout is adjusted by adding Heartbeat.Interval _timeoutControl.BytesWrittenToBuffer(minRate, 400); @@ -395,8 +391,7 @@ public void WriteTimingAbortsConnectionWhenSmallWriteDoesNotCompleteWithinGraceP var minRate = new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(5)); // Initialize timestamp - var startTime = _systemClock.UtcNow; - _timeoutControl.Initialize(startTime.Ticks); + _timeoutControl.Initialize(); // Should complete within 1 second, but the timeout is adjusted by adding Heartbeat.Interval _timeoutControl.BytesWrittenToBuffer(minRate, 100); @@ -420,7 +415,7 @@ public void WriteTimingTimeoutPushedOnConcurrentWrite() var minRate = new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(2)); // Initialize timestamp - _timeoutControl.Initialize(_systemClock.UtcNow.Ticks); + _timeoutControl.Initialize(); // Should complete within 5 seconds, but the timeout is adjusted by adding Heartbeat.Interval _timeoutControl.BytesWrittenToBuffer(minRate, 500); @@ -453,8 +448,7 @@ public void WriteTimingAbortsConnectionWhenRepeatedSmallWritesDoNotCompleteWithM var writeSize = 100; // Initialize timestamp - var startTime = _systemClock.UtcNow; - _timeoutControl.Initialize(startTime.Ticks); + _timeoutControl.Initialize(); // 5 consecutive 100 byte writes. for (var i = 0; i < numWrites - 1; i++) @@ -473,8 +467,8 @@ public void WriteTimingAbortsConnectionWhenRepeatedSmallWritesDoNotCompleteWithM _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); // On more tick forward triggers the timeout. - _systemClock.UtcNow += TimeSpan.FromTicks(1); - _timeoutControl.Tick(_systemClock.UtcNow); + _timeProvider.Advance(TimeSpan.FromTicks(1)); + _timeoutControl.Tick(_timeProvider.GetTimestamp()); _mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.WriteDataRate), Times.Once); } @@ -485,15 +479,15 @@ public void WriteTimingOnlyCountsUpToOneHeartbeatIntervalPerTick() var minRate = new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(2)); // Initialize timestamp - _timeoutControl.Initialize(_systemClock.UtcNow.Ticks); + _timeoutControl.Initialize(); // Should complete within 4 seconds, but the timeout is adjusted by adding Heartbeat.Interval _timeoutControl.BytesWrittenToBuffer(minRate, 400); _timeoutControl.StartTimingWrite(); // Tick just past 4s plus Heartbeat.Interval at once - _systemClock.UtcNow += TimeSpan.FromSeconds(4) + Heartbeat.Interval + TimeSpan.FromTicks(1); - _timeoutControl.Tick(_systemClock.UtcNow); + _timeProvider.Advance(TimeSpan.FromSeconds(4) + Heartbeat.Interval + TimeSpan.FromTicks(1)); + _timeoutControl.Tick(_timeProvider.GetTimestamp()); _mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.WriteDataRate), Times.Never); @@ -510,7 +504,7 @@ private void TickBodyWithMinimumDataRate(int bytesPerSecond) var minRate = new MinDataRate(bytesPerSecond, gracePeriod); // Initialize timestamp - _timeoutControl.Initialize(_systemClock.UtcNow.Ticks); + _timeoutControl.Initialize(); _timeoutControl.StartRequestBody(minRate); _timeoutControl.StartTimingRead(); @@ -518,22 +512,22 @@ private void TickBodyWithMinimumDataRate(int bytesPerSecond) AdvanceClock(gracePeriod); // Tick after grace period w/ low data rate - _systemClock.UtcNow += TimeSpan.FromSeconds(1); + _timeProvider.Advance(TimeSpan.FromSeconds(1)); _timeoutControl.BytesRead(1); - _timeoutControl.Tick(_systemClock.UtcNow); + _timeoutControl.Tick(_timeProvider.GetTimestamp()); } private void AdvanceClock(TimeSpan timeSpan) { - var endTime = _systemClock.UtcNow + timeSpan; + var endTime = _timeProvider.GetTimestamp(timeSpan); - while (_systemClock.UtcNow + Heartbeat.Interval < endTime) + while (_timeProvider.GetTimestamp(Heartbeat.Interval) < endTime) { - _systemClock.UtcNow += Heartbeat.Interval; - _timeoutControl.Tick(_systemClock.UtcNow); + _timeProvider.Advance(Heartbeat.Interval); + _timeoutControl.Tick(_timeProvider.GetTimestamp()); } - _systemClock.UtcNow = endTime; - _timeoutControl.Tick(_systemClock.UtcNow); + _timeProvider.AdvanceTo(endTime); + _timeoutControl.Tick(_timeProvider.GetTimestamp()); } } diff --git a/src/Servers/Kestrel/Transport.Quic/src/Internal/ISystemClock.cs b/src/Servers/Kestrel/Transport.Quic/src/Internal/ISystemClock.cs deleted file mode 100644 index 30e33045162f..000000000000 --- a/src/Servers/Kestrel/Transport.Quic/src/Internal/ISystemClock.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Internal; - -/// -/// Abstracts the system clock to facilitate testing. -/// -internal interface ISystemClock -{ - /// - /// Retrieves the current system time in UTC. - /// - DateTimeOffset UtcNow { get; } -} - -internal sealed class SystemClock : ISystemClock -{ - public DateTimeOffset UtcNow => DateTimeOffset.UtcNow; -} diff --git a/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicConnectionContext.cs b/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicConnectionContext.cs index 9fa0fd358452..ff02fd3421d5 100644 --- a/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicConnectionContext.cs +++ b/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicConnectionContext.cs @@ -19,7 +19,7 @@ internal partial class QuicConnectionContext : TransportMultiplexedConnection private bool _streamPoolHeartbeatInitialized; // Ticks updated once per-second in heartbeat event. - private long _heartbeatTicks; + private long _heartbeatTimestamp; private readonly object _poolLock = new object(); private readonly object _shutdownLock = new object(); @@ -33,7 +33,7 @@ internal partial class QuicConnectionContext : TransportMultiplexedConnection internal const int InitialStreamPoolSize = 5; internal const int MaxStreamPoolSize = 100; - internal const long StreamPoolExpiryTicks = TimeSpan.TicksPerSecond * 5; + internal const long StreamPoolExpirySeconds = 5; public QuicConnectionContext(QuicConnection connection, QuicTransportContext context) { @@ -240,6 +240,8 @@ internal bool TryReturnStream(QuicStreamContext stream) { lock (_poolLock) { + var timeProvider = _context.Options.TimeProvider; + if (!_streamPoolHeartbeatInitialized) { // Heartbeat feature is added to connection features by Kestrel. @@ -254,16 +256,16 @@ internal bool TryReturnStream(QuicStreamContext stream) heartbeatFeature.OnHeartbeat(static state => ((QuicConnectionContext)state).RemoveExpiredStreams(), this); - // Set ticks for the first time. Ticks are then updated in heartbeat. - var now = _context.Options.SystemClock.UtcNow.Ticks; - Volatile.Write(ref _heartbeatTicks, now); + // Set timestamp for the first time. Timestamps are then updated in heartbeat. + var now = timeProvider.GetTimestamp(); + Volatile.Write(ref _heartbeatTimestamp, now); _streamPoolHeartbeatInitialized = true; } if (stream.CanReuse && StreamPool.Count < MaxStreamPoolSize) { - stream.PoolExpirationTicks = Volatile.Read(ref _heartbeatTicks) + StreamPoolExpiryTicks; + stream.PoolExpirationTimestamp = Volatile.Read(ref _heartbeatTimestamp) + StreamPoolExpirySeconds * timeProvider.TimestampFrequency; StreamPool.Push(stream); QuicLog.StreamPooled(_log, stream); @@ -284,8 +286,8 @@ private void RemoveExpiredStreams() lock (_poolLock) { // Update ticks on heartbeat. A precise value isn't necessary. - var now = _context.Options.SystemClock.UtcNow.Ticks; - Volatile.Write(ref _heartbeatTicks, now); + var now = _context.Options.TimeProvider.GetTimestamp(); + Volatile.Write(ref _heartbeatTimestamp, now); StreamPool.RemoveExpired(now); } diff --git a/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicStreamContext.cs b/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicStreamContext.cs index 5f0a7d31e1ab..d81f0ec4757a 100644 --- a/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicStreamContext.cs +++ b/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicStreamContext.cs @@ -91,7 +91,7 @@ public void Initialize(QuicStream stream) CanWrite = _stream.CanWrite; _error = null; StreamId = _stream.Id; - PoolExpirationTicks = 0; + PoolExpirationTimestamp = 0; Transport = _originalTransport; Application = _originalApplication; @@ -136,7 +136,7 @@ public override string ConnectionId set => _connectionId = value; } - public long PoolExpirationTicks { get; set; } + public long PoolExpirationTimestamp { get; set; } public void Start() { diff --git a/src/Servers/Kestrel/Transport.Quic/src/QuicTransportOptions.cs b/src/Servers/Kestrel/Transport.Quic/src/QuicTransportOptions.cs index 3a455d7dc68d..e2f58f124b0a 100644 --- a/src/Servers/Kestrel/Transport.Quic/src/QuicTransportOptions.cs +++ b/src/Servers/Kestrel/Transport.Quic/src/QuicTransportOptions.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Runtime.Versioning; -using Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Internal; namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Quic; @@ -80,5 +79,5 @@ private static void ValidateErrorCode(long errorCode) } } - internal ISystemClock SystemClock = new SystemClock(); + internal TimeProvider TimeProvider = TimeProvider.System; } diff --git a/src/Servers/Kestrel/Transport.Quic/test/Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Tests.csproj b/src/Servers/Kestrel/Transport.Quic/test/Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Tests.csproj index b7ba36177491..f11876cf5a7e 100644 --- a/src/Servers/Kestrel/Transport.Quic/test/Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Tests.csproj +++ b/src/Servers/Kestrel/Transport.Quic/test/Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Tests.csproj @@ -15,6 +15,7 @@ + diff --git a/src/Servers/Kestrel/Transport.Quic/test/QuicConnectionContextTests.cs b/src/Servers/Kestrel/Transport.Quic/test/QuicConnectionContextTests.cs index ef38b4848852..f6c14230fd09 100644 --- a/src/Servers/Kestrel/Transport.Quic/test/QuicConnectionContextTests.cs +++ b/src/Servers/Kestrel/Transport.Quic/test/QuicConnectionContextTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Buffers; +using System.Diagnostics; using System.Net.Http; using System.Net.Quic; using System.Text; @@ -499,15 +500,13 @@ public async Task StreamPool_StreamAbortedOnClientAndServer_NotPooled() [ConditionalFact] [MsQuicSupported] - [QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/37862")] public async Task StreamPool_Heartbeat_ExpiredStreamRemoved() { // Arrange using var httpEventSource = new HttpEventSourceListener(LoggerFactory); - var now = new DateTimeOffset(2021, 7, 6, 12, 0, 0, TimeSpan.Zero); - var testSystemClock = new TestSystemClock { UtcNow = now }; - await using var connectionListener = await QuicTestHelpers.CreateConnectionListenerFactory(LoggerFactory, testSystemClock); + var timeProvider = new MockTimeProvider(); + await using var connectionListener = await QuicTestHelpers.CreateConnectionListenerFactory(LoggerFactory, timeProvider); var options = QuicTestHelpers.CreateClientConnectionOptions(connectionListener.EndPoint); await using var clientConnection = await QuicConnection.ConnectAsync(options); @@ -526,10 +525,9 @@ public async Task StreamPool_Heartbeat_ExpiredStreamRemoved() Assert.Equal(1, quicConnectionContext.StreamPool.Count); QuicStreamContext pooledStream = quicConnectionContext.StreamPool._array[0]; Assert.Same(stream1, pooledStream); - Assert.Equal(now.Ticks + QuicConnectionContext.StreamPoolExpiryTicks, pooledStream.PoolExpirationTicks); + Assert.Equal(timeProvider.GetTimestamp() + QuicConnectionContext.StreamPoolExpirySeconds * timeProvider.TimestampFrequency, pooledStream.PoolExpirationTimestamp); - now = now.AddMilliseconds(100); - testSystemClock.UtcNow = now; + timeProvider.Advance(TimeSpan.FromSeconds(0.1)); testHeartbeatFeature.RaiseHeartbeat(); // Not removed. Assert.Equal(1, quicConnectionContext.StreamPool.Count); @@ -539,18 +537,16 @@ public async Task StreamPool_Heartbeat_ExpiredStreamRemoved() Assert.Equal(1, quicConnectionContext.StreamPool.Count); pooledStream = quicConnectionContext.StreamPool._array[0]; Assert.Same(stream1, pooledStream); - Assert.Equal(now.Ticks + QuicConnectionContext.StreamPoolExpiryTicks, pooledStream.PoolExpirationTicks); + Assert.Equal(timeProvider.GetTimestamp() + QuicConnectionContext.StreamPoolExpirySeconds * timeProvider.TimestampFrequency, pooledStream.PoolExpirationTimestamp); Assert.Same(stream1, stream2); - now = now.AddTicks(QuicConnectionContext.StreamPoolExpiryTicks); - testSystemClock.UtcNow = now; + timeProvider.Advance(TimeSpan.FromSeconds(QuicConnectionContext.StreamPoolExpirySeconds)); testHeartbeatFeature.RaiseHeartbeat(); // Not removed. Assert.Equal(1, quicConnectionContext.StreamPool.Count); - now = now.AddTicks(1); - testSystemClock.UtcNow = now; + timeProvider.Advance(TimeSpan.FromTicks(1)); testHeartbeatFeature.RaiseHeartbeat(); // Removed. Assert.Equal(0, quicConnectionContext.StreamPool.Count); @@ -720,11 +716,6 @@ private record RequestState( public int ActiveConcurrentConnections { get; set; } }; - private class TestSystemClock : ISystemClock - { - public DateTimeOffset UtcNow { get; set; } - } - private class TestHeartbeatFeature : IConnectionHeartbeatFeature { private readonly List<(Action Action, object State)> _actions = new List<(Action, object)>(); diff --git a/src/Servers/Kestrel/Transport.Quic/test/QuicTestHelpers.cs b/src/Servers/Kestrel/Transport.Quic/test/QuicTestHelpers.cs index 4fa223873d5a..2eab51cca0b5 100644 --- a/src/Servers/Kestrel/Transport.Quic/test/QuicTestHelpers.cs +++ b/src/Servers/Kestrel/Transport.Quic/test/QuicTestHelpers.cs @@ -29,16 +29,16 @@ internal static class QuicTestHelpers public static QuicTransportFactory CreateTransportFactory( ILoggerFactory loggerFactory = null, - ISystemClock systemClock = null, + TimeProvider timeProvider = null, long defaultCloseErrorCode = 0) { var quicTransportOptions = new QuicTransportOptions(); quicTransportOptions.MaxBidirectionalStreamCount = 200; quicTransportOptions.MaxUnidirectionalStreamCount = 200; quicTransportOptions.DefaultCloseErrorCode = defaultCloseErrorCode; - if (systemClock != null) + if (timeProvider != null) { - quicTransportOptions.SystemClock = systemClock; + quicTransportOptions.TimeProvider = timeProvider; } return new QuicTransportFactory(loggerFactory ?? NullLoggerFactory.Instance, Options.Create(quicTransportOptions)); @@ -46,14 +46,14 @@ public static QuicTransportFactory CreateTransportFactory( public static async Task CreateConnectionListenerFactory( ILoggerFactory loggerFactory = null, - ISystemClock systemClock = null, + TimeProvider timeProvider = null, bool clientCertificateRequired = false, long defaultCloseErrorCode = 0, int port = 0) { var transportFactory = CreateTransportFactory( loggerFactory, - systemClock, + timeProvider, defaultCloseErrorCode: defaultCloseErrorCode); var endpoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), port); @@ -65,10 +65,10 @@ public static async Task CreateConnectionListenerFactory public static async Task CreateConnectionListenerFactory( TlsConnectionCallbackOptions tlsConnectionOptions, ILoggerFactory loggerFactory = null, - ISystemClock systemClock = null, + TimeProvider timeProvider = null, int port = 0) { - var transportFactory = CreateTransportFactory(loggerFactory, systemClock); + var transportFactory = CreateTransportFactory(loggerFactory, timeProvider); var endpoint = new IPEndPoint(IPAddress.Loopback, port); diff --git a/src/Servers/Kestrel/perf/Microbenchmarks/HeaderCollectionBenchmark.cs b/src/Servers/Kestrel/perf/Microbenchmarks/HeaderCollectionBenchmark.cs index 67f1a4e90ed5..5f31f7fcfa68 100644 --- a/src/Servers/Kestrel/perf/Microbenchmarks/HeaderCollectionBenchmark.cs +++ b/src/Servers/Kestrel/perf/Microbenchmarks/HeaderCollectionBenchmark.cs @@ -21,7 +21,7 @@ public class HeaderCollectionBenchmark private const int InnerLoopCount = 1024 * 1024; private static readonly byte[] _bytesServer = Encoding.ASCII.GetBytes("\r\nServer: Kestrel"); - private static readonly DateHeaderValueManager _dateHeaderValueManager = new DateHeaderValueManager(); + private static readonly DateHeaderValueManager _dateHeaderValueManager = new DateHeaderValueManager(TimeProvider.System); private HttpResponseHeaders _responseHeadersDirect; private HttpResponse _response; @@ -343,7 +343,7 @@ public void Setup() var http1Connection = new Http1Connection(connectionContext); http1Connection.Reset(); - serviceContext.DateHeaderValueManager.OnHeartbeat(DateTimeOffset.UtcNow); + serviceContext.DateHeaderValueManager.OnHeartbeat(); _responseHeadersDirect = (HttpResponseHeaders)http1Connection.ResponseHeaders; var context = new DefaultHttpContext(http1Connection); diff --git a/src/Servers/Kestrel/perf/Microbenchmarks/Http1ConnectionBenchmark.cs b/src/Servers/Kestrel/perf/Microbenchmarks/Http1ConnectionBenchmark.cs index 65f7472b68ec..7ba27b5848c6 100644 --- a/src/Servers/Kestrel/perf/Microbenchmarks/Http1ConnectionBenchmark.cs +++ b/src/Servers/Kestrel/perf/Microbenchmarks/Http1ConnectionBenchmark.cs @@ -39,7 +39,7 @@ public void Setup() serviceContext: serviceContext, connectionContext: null, transport: pair.Transport, - timeoutControl: new TimeoutControl(timeoutHandler: null), + timeoutControl: new TimeoutControl(timeoutHandler: null, TimeProvider.System), memoryPool: memoryPool, connectionFeatures: new FeatureCollection()); diff --git a/src/Servers/Kestrel/perf/Microbenchmarks/Http1ConnectionParsingOverheadBenchmark.cs b/src/Servers/Kestrel/perf/Microbenchmarks/Http1ConnectionParsingOverheadBenchmark.cs index dfac1481c43a..02d4993f9107 100644 --- a/src/Servers/Kestrel/perf/Microbenchmarks/Http1ConnectionParsingOverheadBenchmark.cs +++ b/src/Servers/Kestrel/perf/Microbenchmarks/Http1ConnectionParsingOverheadBenchmark.cs @@ -35,7 +35,7 @@ public void Setup() serviceContext: serviceContext, connectionContext: null, transport: pair.Transport, - timeoutControl: new TimeoutControl(timeoutHandler: null), + timeoutControl: new TimeoutControl(timeoutHandler: null, TimeProvider.System), memoryPool: memoryPool, connectionFeatures: new FeatureCollection()); diff --git a/src/Servers/Kestrel/perf/Microbenchmarks/Http1LargeWritingBenchmark.cs b/src/Servers/Kestrel/perf/Microbenchmarks/Http1LargeWritingBenchmark.cs index 14f4f7837482..0472ad83b751 100644 --- a/src/Servers/Kestrel/perf/Microbenchmarks/Http1LargeWritingBenchmark.cs +++ b/src/Servers/Kestrel/perf/Microbenchmarks/Http1LargeWritingBenchmark.cs @@ -71,13 +71,13 @@ private TestHttp1Connection MakeHttp1Connection() var serviceContext = TestContextFactory.CreateServiceContext( serverOptions: new KestrelServerOptions(), httpParser: new HttpParser(), - dateHeaderValueManager: new DateHeaderValueManager()); + dateHeaderValueManager: new DateHeaderValueManager(TimeProvider.System)); var connectionContext = TestContextFactory.CreateHttpConnectionContext( serviceContext: serviceContext, connectionContext: null, transport: pair.Transport, - timeoutControl: new TimeoutControl(timeoutHandler: null), + timeoutControl: new TimeoutControl(timeoutHandler: null, TimeProvider.System), memoryPool: _memoryPool, connectionFeatures: new FeatureCollection()); @@ -85,7 +85,7 @@ private TestHttp1Connection MakeHttp1Connection() http1Connection.Reset(); http1Connection.InitializeBodyControl(MessageBody.ZeroContentLengthKeepAlive); - serviceContext.DateHeaderValueManager.OnHeartbeat(DateTimeOffset.UtcNow); + serviceContext.DateHeaderValueManager.OnHeartbeat(); return http1Connection; } diff --git a/src/Servers/Kestrel/perf/Microbenchmarks/Http1ReadingBenchmark.cs b/src/Servers/Kestrel/perf/Microbenchmarks/Http1ReadingBenchmark.cs index 39a5efb02896..81ea77dc0c17 100644 --- a/src/Servers/Kestrel/perf/Microbenchmarks/Http1ReadingBenchmark.cs +++ b/src/Servers/Kestrel/perf/Microbenchmarks/Http1ReadingBenchmark.cs @@ -100,13 +100,13 @@ private TestHttp1Connection MakeHttp1Connection() var serviceContext = TestContextFactory.CreateServiceContext( serverOptions: new KestrelServerOptions(), httpParser: new HttpParser(), - dateHeaderValueManager: new DateHeaderValueManager()); + dateHeaderValueManager: new DateHeaderValueManager(TimeProvider.System)); var connectionContext = TestContextFactory.CreateHttpConnectionContext( serviceContext: serviceContext, connectionContext: null, transport: pair.Transport, - timeoutControl: new TimeoutControl(timeoutHandler: null), + timeoutControl: new TimeoutControl(timeoutHandler: null, TimeProvider.System), memoryPool: _memoryPool, connectionFeatures: new FeatureCollection()); @@ -114,7 +114,7 @@ private TestHttp1Connection MakeHttp1Connection() http1Connection.Reset(); http1Connection.InitializeBodyControl(new Http1ContentLengthMessageBody(http1Connection, contentLength: 100, keepAlive: true)); - serviceContext.DateHeaderValueManager.OnHeartbeat(DateTimeOffset.UtcNow); + serviceContext.DateHeaderValueManager.OnHeartbeat(); return http1Connection; } diff --git a/src/Servers/Kestrel/perf/Microbenchmarks/Http1WritingBenchmark.cs b/src/Servers/Kestrel/perf/Microbenchmarks/Http1WritingBenchmark.cs index 58f2a8822ad9..2efbd813a169 100644 --- a/src/Servers/Kestrel/perf/Microbenchmarks/Http1WritingBenchmark.cs +++ b/src/Servers/Kestrel/perf/Microbenchmarks/Http1WritingBenchmark.cs @@ -105,13 +105,13 @@ private TestHttp1Connection MakeHttp1Connection() var serviceContext = TestContextFactory.CreateServiceContext( serverOptions: new KestrelServerOptions(), httpParser: new HttpParser(), - dateHeaderValueManager: new DateHeaderValueManager()); + dateHeaderValueManager: new DateHeaderValueManager(TimeProvider.System)); var connectionContext = TestContextFactory.CreateHttpConnectionContext( serviceContext: serviceContext, connectionContext: null, transport: pair.Transport, - timeoutControl: new TimeoutControl(timeoutHandler: null), + timeoutControl: new TimeoutControl(timeoutHandler: null, TimeProvider.System), memoryPool: _memoryPool, connectionFeatures: new FeatureCollection()); @@ -119,7 +119,7 @@ private TestHttp1Connection MakeHttp1Connection() http1Connection.Reset(); http1Connection.InitializeBodyControl(MessageBody.ZeroContentLengthKeepAlive); - serviceContext.DateHeaderValueManager.OnHeartbeat(DateTimeOffset.UtcNow); + serviceContext.DateHeaderValueManager.OnHeartbeat(); return http1Connection; } diff --git a/src/Servers/Kestrel/perf/Microbenchmarks/Http2/Http2ConnectionBenchmarkBase.cs b/src/Servers/Kestrel/perf/Microbenchmarks/Http2/Http2ConnectionBenchmarkBase.cs index 89d955a7db6b..d2b0b7cac05e 100644 --- a/src/Servers/Kestrel/perf/Microbenchmarks/Http2/Http2ConnectionBenchmarkBase.cs +++ b/src/Servers/Kestrel/perf/Microbenchmarks/Http2/Http2ConnectionBenchmarkBase.cs @@ -69,9 +69,9 @@ public virtual void GlobalSetup() var serviceContext = TestContextFactory.CreateServiceContext( serverOptions: new KestrelServerOptions(), - dateHeaderValueManager: new DateHeaderValueManager(), - systemClock: new MockSystemClock()); - serviceContext.DateHeaderValueManager.OnHeartbeat(default); + dateHeaderValueManager: new DateHeaderValueManager(TimeProvider.System), + timeProvider: TimeProvider.System); + serviceContext.DateHeaderValueManager.OnHeartbeat(); var featureCollection = new FeatureCollection(); featureCollection.Set(new TestConnectionMetricsContextFeature()); diff --git a/src/Servers/Kestrel/perf/Microbenchmarks/Http2/Http2FrameWriterBenchmark.cs b/src/Servers/Kestrel/perf/Microbenchmarks/Http2/Http2FrameWriterBenchmark.cs index a80186a0af60..5d010762e1a9 100644 --- a/src/Servers/Kestrel/perf/Microbenchmarks/Http2/Http2FrameWriterBenchmark.cs +++ b/src/Servers/Kestrel/perf/Microbenchmarks/Http2/Http2FrameWriterBenchmark.cs @@ -32,7 +32,7 @@ public void GlobalSetup() var serviceContext = TestContextFactory.CreateServiceContext( serverOptions: new KestrelServerOptions(), httpParser: new HttpParser(), - dateHeaderValueManager: new DateHeaderValueManager()); + dateHeaderValueManager: new DateHeaderValueManager(TimeProvider.System)); _frameWriter = new Http2FrameWriter( new NullPipeWriter(), diff --git a/src/Servers/Kestrel/perf/Microbenchmarks/Http3/Http3ConnectionBenchmarkBase.cs b/src/Servers/Kestrel/perf/Microbenchmarks/Http3/Http3ConnectionBenchmarkBase.cs index ac508ddcc447..ae144e61081b 100644 --- a/src/Servers/Kestrel/perf/Microbenchmarks/Http3/Http3ConnectionBenchmarkBase.cs +++ b/src/Servers/Kestrel/perf/Microbenchmarks/Http3/Http3ConnectionBenchmarkBase.cs @@ -52,15 +52,15 @@ public virtual void GlobalSetup() _httpRequestHeaders[InternalHeaderNames.Scheme] = new StringValues("http"); _httpRequestHeaders[InternalHeaderNames.Authority] = new StringValues("localhost:80"); + var mockTimeProvider = new MockTimeProvider(); + var serviceContext = TestContextFactory.CreateServiceContext( serverOptions: new KestrelServerOptions(), - dateHeaderValueManager: new DateHeaderValueManager(), - systemClock: new MockSystemClock()); - serviceContext.DateHeaderValueManager.OnHeartbeat(default); - - var mockSystemClock = new Microsoft.AspNetCore.Testing.MockSystemClock(); + dateHeaderValueManager: new DateHeaderValueManager(mockTimeProvider), + timeProvider: mockTimeProvider); + serviceContext.DateHeaderValueManager.OnHeartbeat(); - _http3 = new Http3InMemory(serviceContext, mockSystemClock, new DefaultTimeoutHandler(), NullLoggerFactory.Instance); + _http3 = new Http3InMemory(serviceContext, mockTimeProvider, new DefaultTimeoutHandler(), NullLoggerFactory.Instance); _http3.InitializeConnectionAsync(ProcessRequest).GetAwaiter().GetResult(); } diff --git a/src/Servers/Kestrel/perf/Microbenchmarks/HttpProtocolFeatureCollection.cs b/src/Servers/Kestrel/perf/Microbenchmarks/HttpProtocolFeatureCollection.cs index 9155abd1ccef..e749b89c1a97 100644 --- a/src/Servers/Kestrel/perf/Microbenchmarks/HttpProtocolFeatureCollection.cs +++ b/src/Servers/Kestrel/perf/Microbenchmarks/HttpProtocolFeatureCollection.cs @@ -233,7 +233,7 @@ public HttpProtocolFeatureCollection() var serviceContext = TestContextFactory.CreateServiceContext( serverOptions: new KestrelServerOptions(), httpParser: new HttpParser(), - dateHeaderValueManager: new DateHeaderValueManager()); + dateHeaderValueManager: new DateHeaderValueManager(TimeProvider.System)); var connectionContext = TestContextFactory.CreateHttpConnectionContext( serviceContext: serviceContext, diff --git a/src/Servers/Kestrel/perf/Microbenchmarks/Microsoft.AspNetCore.Server.Kestrel.Microbenchmarks.csproj b/src/Servers/Kestrel/perf/Microbenchmarks/Microsoft.AspNetCore.Server.Kestrel.Microbenchmarks.csproj index 073496464d85..acb6b02307c2 100644 --- a/src/Servers/Kestrel/perf/Microbenchmarks/Microsoft.AspNetCore.Server.Kestrel.Microbenchmarks.csproj +++ b/src/Servers/Kestrel/perf/Microbenchmarks/Microsoft.AspNetCore.Server.Kestrel.Microbenchmarks.csproj @@ -11,7 +11,7 @@ - + diff --git a/src/Servers/Kestrel/perf/Microbenchmarks/Mocks/MockSystemClock.cs b/src/Servers/Kestrel/perf/Microbenchmarks/Mocks/MockSystemClock.cs deleted file mode 100644 index 58ed52a202e9..000000000000 --- a/src/Servers/Kestrel/perf/Microbenchmarks/Mocks/MockSystemClock.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; - -namespace Microsoft.AspNetCore.Server.Kestrel.Microbenchmarks; - -internal sealed class MockSystemClock : ISystemClock -{ - public DateTimeOffset UtcNow { get; } - public long UtcNowTicks { get; } - public DateTimeOffset UtcNowUnsynchronized { get; } -} diff --git a/src/Servers/Kestrel/perf/Microbenchmarks/Mocks/MockTimeoutControl.cs b/src/Servers/Kestrel/perf/Microbenchmarks/Mocks/MockTimeoutControl.cs index eb32c081a3af..196773c1f19c 100644 --- a/src/Servers/Kestrel/perf/Microbenchmarks/Mocks/MockTimeoutControl.cs +++ b/src/Servers/Kestrel/perf/Microbenchmarks/Mocks/MockTimeoutControl.cs @@ -33,11 +33,11 @@ public void InitializeHttp2(InputFlowControl connectionInputFlowControl) { } - public void ResetTimeout(long ticks, TimeoutReason timeoutReason) + public void ResetTimeout(TimeSpan timeout, TimeoutReason timeoutReason) { } - public void SetTimeout(long ticks, TimeoutReason timeoutReason) + public void SetTimeout(TimeSpan timeout, TimeoutReason timeoutReason) { } @@ -65,7 +65,7 @@ public void StopTimingWrite() { } - public void Tick(DateTimeOffset now) + public void Tick(long timestamp) { } } diff --git a/src/Servers/Kestrel/perf/Microbenchmarks/RequestParsingBenchmark.cs b/src/Servers/Kestrel/perf/Microbenchmarks/RequestParsingBenchmark.cs index 648ccce3a470..b7fb6b556bd3 100644 --- a/src/Servers/Kestrel/perf/Microbenchmarks/RequestParsingBenchmark.cs +++ b/src/Servers/Kestrel/perf/Microbenchmarks/RequestParsingBenchmark.cs @@ -31,7 +31,7 @@ public void Setup() var serviceContext = TestContextFactory.CreateServiceContext( serverOptions: new KestrelServerOptions(), httpParser: new HttpParser(), - dateHeaderValueManager: new DateHeaderValueManager()); + dateHeaderValueManager: new DateHeaderValueManager(TimeProvider.System)); var connectionContext = TestContextFactory.CreateHttpConnectionContext( serviceContext: serviceContext, @@ -39,7 +39,7 @@ public void Setup() transport: pair.Transport, memoryPool: _memoryPool, connectionFeatures: new FeatureCollection(), - timeoutControl: new TimeoutControl(timeoutHandler: null)); + timeoutControl: new TimeoutControl(timeoutHandler: null, TimeProvider.System)); var http1Connection = new Http1Connection(connectionContext); diff --git a/src/Servers/Kestrel/perf/Microbenchmarks/ResponseHeaderCollectionBenchmark.cs b/src/Servers/Kestrel/perf/Microbenchmarks/ResponseHeaderCollectionBenchmark.cs index 2a61fe881060..feab3f68b0ea 100644 --- a/src/Servers/Kestrel/perf/Microbenchmarks/ResponseHeaderCollectionBenchmark.cs +++ b/src/Servers/Kestrel/perf/Microbenchmarks/ResponseHeaderCollectionBenchmark.cs @@ -22,7 +22,7 @@ public class ResponseHeaderCollectionBenchmark private const int InnerLoopCount = 128; private static readonly byte[] _bytesServer = Encoding.ASCII.GetBytes("\r\nServer: Kestrel"); - private static readonly DateHeaderValueManager _dateHeaderValueManager = new DateHeaderValueManager(); + private static readonly DateHeaderValueManager _dateHeaderValueManager = new DateHeaderValueManager(TimeProvider.System); private HttpResponseHeaders _responseHeadersDirect; private HttpResponse _response; @@ -191,7 +191,7 @@ public void Setup() var http1Connection = new Http1Connection(connectionContext); http1Connection.Reset(); - serviceContext.DateHeaderValueManager.OnHeartbeat(DateTimeOffset.UtcNow); + serviceContext.DateHeaderValueManager.OnHeartbeat(); _responseHeadersDirect = (HttpResponseHeaders)http1Connection.ResponseHeaders; var context = new DefaultHttpContext(http1Connection); diff --git a/src/Servers/Kestrel/perf/Microbenchmarks/ResponseHeadersWritingBenchmark.cs b/src/Servers/Kestrel/perf/Microbenchmarks/ResponseHeadersWritingBenchmark.cs index ab1bfbc1a084..2faf4a3189ad 100644 --- a/src/Servers/Kestrel/perf/Microbenchmarks/ResponseHeadersWritingBenchmark.cs +++ b/src/Servers/Kestrel/perf/Microbenchmarks/ResponseHeadersWritingBenchmark.cs @@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; +using Microsoft.AspNetCore.Testing; using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.Server.Kestrel.Microbenchmarks; @@ -178,8 +179,8 @@ public void GlobalSetup() { _responseHeaders = new HttpResponseHeaders(); _responseHeadersDict = _responseHeaders; - _dateHeaderValueManager = new DateHeaderValueManager(); - _dateHeaderValueManager.OnHeartbeat(DateTimeOffset.Now); + _dateHeaderValueManager = new DateHeaderValueManager(TimeProvider.System); + _dateHeaderValueManager.OnHeartbeat(); _writer = new Writer(); } diff --git a/src/Servers/Kestrel/shared/PooledStreamStack.cs b/src/Servers/Kestrel/shared/PooledStreamStack.cs index 24f3a42e004d..93b04a4a3a8f 100644 --- a/src/Servers/Kestrel/shared/PooledStreamStack.cs +++ b/src/Servers/Kestrel/shared/PooledStreamStack.cs @@ -10,7 +10,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel; internal interface IPooledStream { - long PoolExpirationTicks { get; } + long PoolExpirationTimestamp { get; } void DisposeCore(); } @@ -87,12 +87,12 @@ private void PushWithResize(TValue item) _size++; } - public void RemoveExpired(long now) + public void RemoveExpired(long timestamp) { int size = _size; StreamAsValueType[] array = _array; - var removeCount = CalculateRemoveCount(now, size, array); + var removeCount = CalculateRemoveCount(timestamp, size, array); if (removeCount == 0) { return; @@ -122,12 +122,12 @@ public void RemoveExpired(long now) _size = newSize; } - private static int CalculateRemoveCount(long now, int size, StreamAsValueType[] array) + private static int CalculateRemoveCount(long timestamp, int size, StreamAsValueType[] array) { for (var i = 0; i < size; i++) { TValue stream = array[i]; - if (stream.PoolExpirationTicks >= now) + if (stream.PoolExpirationTimestamp >= timestamp) { // Stream is still valid. All streams after this will have a later expiration. // No reason to keep checking. Return count of streams to remove. diff --git a/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs b/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs index fb688bcd5a51..e02573b7b225 100644 --- a/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs +++ b/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs @@ -6,7 +6,6 @@ using System.Diagnostics; using System.Globalization; using System.IO.Pipelines; -using System.Linq; using System.Net.Http; using System.Net.Http.QPack; using System.Text; @@ -17,12 +16,9 @@ using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Internal; using Microsoft.AspNetCore.Server.Kestrel.Core; -using Microsoft.AspNetCore.Server.Kestrel.Core.Features; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; -using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; -using Microsoft.AspNetCore.Server.Kestrel.Core.WebTransport; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; using static System.IO.Pipelines.DuplexPipe; @@ -37,13 +33,13 @@ internal class Http3InMemory protected static readonly byte[] _helloWorldBytes = Encoding.ASCII.GetBytes("hello, world"); protected static readonly byte[] _maxData = Encoding.ASCII.GetBytes(new string('a', 16 * 1024)); - public Http3InMemory(ServiceContext serviceContext, MockSystemClock mockSystemClock, ITimeoutHandler timeoutHandler, ILoggerFactory loggerFactory) + public Http3InMemory(ServiceContext serviceContext, MockTimeProvider mockTimeProvider, ITimeoutHandler timeoutHandler, ILoggerFactory loggerFactory) { _serviceContext = serviceContext; - _timeoutControl = new TimeoutControl(new TimeoutControlConnectionInvoker(this, timeoutHandler)); + _timeoutControl = new TimeoutControl(new TimeoutControlConnectionInvoker(this, timeoutHandler), mockTimeProvider); _timeoutControl.Debugger = new TestDebugger(); - _mockSystemClock = mockSystemClock; + _mockTimeProvider = mockTimeProvider; _serverReceivedSettings = Channel.CreateUnbounded>(); Logger = loggerFactory.CreateLogger(); @@ -73,7 +69,7 @@ public void OnTimeout(TimeoutReason reason) } internal ServiceContext _serviceContext; - private MockSystemClock _mockSystemClock; + private MockTimeProvider _mockTimeProvider; internal HttpConnection _httpConnection; internal readonly TimeoutControl _timeoutControl; internal readonly MemoryPool _memoryPool = PinnedBlockMemoryPoolFactory.Create(); @@ -199,27 +195,28 @@ internal void VerifyGoAway(Http3FrameWithPayload frame, long expectedLastStreamI } } - public void AdvanceClock(TimeSpan timeSpan) + public void AdvanceTime(TimeSpan timeSpan) { - Logger.LogDebug($"Advancing clock {timeSpan}."); + Logger.LogDebug("Advancing timeProvider {timeSpan}.", timeSpan); - var clock = _mockSystemClock; - var endTime = clock.UtcNow + timeSpan; + var timeProvider = _mockTimeProvider; + var endTime = timeProvider.GetTimestamp(timeSpan); - while (clock.UtcNow + Heartbeat.Interval < endTime) + while (timeProvider.GetTimestamp(Heartbeat.Interval) < endTime) { - clock.UtcNow += Heartbeat.Interval; - _timeoutControl.Tick(clock.UtcNow); + timeProvider.Advance(Heartbeat.Interval); + _timeoutControl.Tick(timeProvider.GetTimestamp()); } - clock.UtcNow = endTime; - _timeoutControl.Tick(clock.UtcNow); + timeProvider.AdvanceTo(endTime); + _timeoutControl.Tick(timeProvider.GetTimestamp()); } - public void TriggerTick(DateTimeOffset now) + public void TriggerTick(TimeSpan timeSpan = default) { - _mockSystemClock.UtcNow = now; - Connection?.Tick(now); + _mockTimeProvider.Advance(timeSpan); + var timestamp = _mockTimeProvider.GetTimestamp(); + Connection?.Tick(timestamp); } public async Task InitializeConnectionAsync(RequestDelegate application) diff --git a/src/Servers/Kestrel/shared/test/MockSystemClock.cs b/src/Servers/Kestrel/shared/test/MockSystemClock.cs deleted file mode 100644 index 71a231637a79..000000000000 --- a/src/Servers/Kestrel/shared/test/MockSystemClock.cs +++ /dev/null @@ -1,44 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Threading; -using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; - -namespace Microsoft.AspNetCore.Testing; - -public class MockSystemClock : ISystemClock -{ - private long _utcNowTicks; - - public MockSystemClock() - { - // Use a random DateTimeOffset to ensure tests that incorrectly use the current DateTimeOffset fail always instead of only rarely. - // Pick a date between the min DateTimeOffset and a day before the max DateTimeOffset so there's room to advance the clock. - _utcNowTicks = NextLong(DateTimeOffset.MinValue.Ticks, DateTimeOffset.MaxValue.Ticks - TimeSpan.FromDays(1).Ticks); - } - - public DateTimeOffset UtcNow - { - get - { - UtcNowCalled++; - return new DateTimeOffset(Interlocked.Read(ref _utcNowTicks), TimeSpan.Zero); - } - set - { - Interlocked.Exchange(ref _utcNowTicks, value.Ticks); - } - } - - public long UtcNowTicks => UtcNow.Ticks; - - public DateTimeOffset UtcNowUnsynchronized => UtcNow; - - public int UtcNowCalled { get; private set; } - - private long NextLong(long minValue, long maxValue) - { - return (long)(Random.Shared.NextDouble() * (maxValue - minValue) + minValue); - } -} diff --git a/src/Servers/Kestrel/shared/test/TestContextFactory.cs b/src/Servers/Kestrel/shared/test/TestContextFactory.cs index e72deb676129..5413e4c13249 100644 --- a/src/Servers/Kestrel/shared/test/TestContextFactory.cs +++ b/src/Servers/Kestrel/shared/test/TestContextFactory.cs @@ -1,13 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; using System.Buffers; -using System.Collections.Generic; using System.IO.Pipelines; using System.Net; -using System.Threading; -using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Core; @@ -29,7 +25,7 @@ public static ServiceContext CreateServiceContext( KestrelServerOptions serverOptions, IHttpParser httpParser = null, PipeScheduler scheduler = null, - ISystemClock systemClock = null, + TimeProvider timeProvider = null, DateHeaderValueManager dateHeaderValueManager = null, ConnectionManager connectionManager = null, Heartbeat heartbeat = null) @@ -39,7 +35,7 @@ public static ServiceContext CreateServiceContext( Log = new KestrelTrace(NullLoggerFactory.Instance), Scheduler = scheduler, HttpParser = httpParser, - SystemClock = systemClock, + TimeProvider = timeProvider, DateHeaderValueManager = dateHeaderValueManager, ConnectionManager = connectionManager, Heartbeat = heartbeat, diff --git a/src/Servers/Kestrel/shared/test/TestServiceContext.cs b/src/Servers/Kestrel/shared/test/TestServiceContext.cs index 4893853164c8..49712725a9a9 100644 --- a/src/Servers/Kestrel/shared/test/TestServiceContext.cs +++ b/src/Servers/Kestrel/shared/test/TestServiceContext.cs @@ -38,17 +38,16 @@ private static KestrelTrace CreateLoggingTrace(ILoggerFactory loggerFactory) public void InitializeHeartbeat() { - var heartbeatManager = new HeartbeatManager(ConnectionManager); - DateHeaderValueManager = new DateHeaderValueManager(); + DateHeaderValueManager = new DateHeaderValueManager(TimeProvider.System); Heartbeat = new Heartbeat( - new IHeartbeatHandler[] { DateHeaderValueManager, heartbeatManager }, - new SystemClock(), + new IHeartbeatHandler[] { DateHeaderValueManager, ConnectionManager }, + TimeProvider.System, DebuggerWrapper.Singleton, Log, Heartbeat.Interval); - MockSystemClock = null; - SystemClock = heartbeatManager; + MockTimeProvider = null; + TimeProvider = TimeProvider.System; } private void Initialize(ILoggerFactory loggerFactory, KestrelTrace kestrelTrace, bool disableHttp1LineFeedTerminators, KestrelMetrics metrics) @@ -56,9 +55,9 @@ private void Initialize(ILoggerFactory loggerFactory, KestrelTrace kestrelTrace, LoggerFactory = loggerFactory; Log = kestrelTrace; Scheduler = PipeScheduler.ThreadPool; - MockSystemClock = new MockSystemClock(); - SystemClock = MockSystemClock; - DateHeaderValueManager = new DateHeaderValueManager(); + MockTimeProvider = new MockTimeProvider(); + TimeProvider = MockTimeProvider; + DateHeaderValueManager = new DateHeaderValueManager(MockTimeProvider); ConnectionManager = new ConnectionManager(Log, ResourceCounter.Unlimited); HttpParser = new HttpParser(Log.IsEnabled(LogLevel.Information), disableHttp1LineFeedTerminators); ServerOptions = new KestrelServerOptions @@ -66,13 +65,13 @@ private void Initialize(ILoggerFactory loggerFactory, KestrelTrace kestrelTrace, AddServerHeader = false }; - DateHeaderValueManager.OnHeartbeat(SystemClock.UtcNow); + DateHeaderValueManager.OnHeartbeat(); Metrics = metrics; } public ILoggerFactory LoggerFactory { get; set; } - public MockSystemClock MockSystemClock { get; set; } + public MockTimeProvider MockTimeProvider { get; set; } public Func> MemoryPoolFactory { get; set; } = System.Buffers.PinnedBlockMemoryPoolFactory.Create; diff --git a/src/Servers/Kestrel/test/FunctionalTests/ResponseTests.cs b/src/Servers/Kestrel/test/FunctionalTests/ResponseTests.cs index 17eb640826d9..50c67b81a968 100644 --- a/src/Servers/Kestrel/test/FunctionalTests/ResponseTests.cs +++ b/src/Servers/Kestrel/test/FunctionalTests/ResponseTests.cs @@ -768,8 +768,8 @@ public async Task ConnectionNotClosedWhenClientSatisfiesMinimumDataRateGivenLarg }; testContext.InitializeHeartbeat(); - var dateHeaderValueManager = new DateHeaderValueManager(); - dateHeaderValueManager.OnHeartbeat(DateTimeOffset.MinValue); + var dateHeaderValueManager = new DateHeaderValueManager(TimeProvider.System); + dateHeaderValueManager.OnHeartbeat(); testContext.DateHeaderValueManager = dateHeaderValueManager; var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)); @@ -845,8 +845,8 @@ public async Task ConnectionNotClosedWhenClientSatisfiesMinimumDataRateGivenLarg }; testContext.InitializeHeartbeat(); - var dateHeaderValueManager = new DateHeaderValueManager(); - dateHeaderValueManager.OnHeartbeat(DateTimeOffset.MinValue); + var dateHeaderValueManager = new DateHeaderValueManager(TimeProvider.System); + dateHeaderValueManager.OnHeartbeat(); testContext.DateHeaderValueManager = dateHeaderValueManager; var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)); @@ -929,8 +929,8 @@ public async Task ClientCanReceiveFullConnectionCloseResponseWithoutErrorAtALowD }; testContext.InitializeHeartbeat(); - var dateHeaderValueManager = new DateHeaderValueManager(); - dateHeaderValueManager.OnHeartbeat(DateTimeOffset.MinValue); + var dateHeaderValueManager = new DateHeaderValueManager(TimeProvider.System); + dateHeaderValueManager.OnHeartbeat(); testContext.DateHeaderValueManager = dateHeaderValueManager; var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)); diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs index 61f5bda90ef4..7e5cec61fa81 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs @@ -634,7 +634,7 @@ public async Task StreamPool_UnendedStreamErrorsOnStart_NotReturnedToPool() // TriggerTick will trigger the stream to be returned to the pool so we can assert it TriggerTick(); - AdvanceClock(TimeSpan.FromTicks(Constants.RequestBodyDrainTimeout.Ticks + 1)); + AdvanceTime(TimeSpan.FromTicks(Constants.RequestBodyDrainTimeout.Ticks + 1)); // TriggerTick will trigger the stream to attempt to be returned to the pool TriggerTick(); @@ -670,12 +670,12 @@ await ExpectAsync(Http2FrameType.HEADERS, _connection.StreamPool.TryPeek(out var pooledStream); - AdvanceClock(TimeSpan.FromSeconds(1)); + AdvanceTime(TimeSpan.FromSeconds(1)); // Stream has not expired and is still in pool Assert.Equal(1, _connection.StreamPool.Count); - AdvanceClock(TimeSpan.FromSeconds(6)); + AdvanceTime(TimeSpan.FromSeconds(6)); // Stream has expired and has been removed from pool Assert.Equal(0, _connection.StreamPool.Count); diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2KeepAliveTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2KeepAliveTests.cs index 47d9ed74d51e..d06ad5df401f 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2KeepAliveTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2KeepAliveTests.cs @@ -28,13 +28,11 @@ public async Task KeepAlivePingTimeout_InfiniteTimeSpan_NoGoAway() await InitializeConnectionAsync(_noopApplication).DefaultTimeout(); - DateTimeOffset now = _serviceContext.MockSystemClock.UtcNow; - // Heartbeat - TriggerTick(now); + TriggerTick(); // Heartbeat that exceeds interval - TriggerTick(now + TimeSpan.FromSeconds(1.1 * 2)); + TriggerTick(TimeSpan.FromSeconds(1.1 * 2)); await ExpectAsync(Http2FrameType.PING, withLength: 8, @@ -42,9 +40,9 @@ await ExpectAsync(Http2FrameType.PING, withStreamId: 0).DefaultTimeout(); // Heartbeat that exceeds timeout - TriggerTick(now + TimeSpan.FromSeconds(1.1 * 3)); - TriggerTick(now + TimeSpan.FromSeconds(1.1 * 4)); - TriggerTick(now + TimeSpan.FromSeconds(1.1 * 20)); + TriggerTick(TimeSpan.FromSeconds(1.1)); + TriggerTick(TimeSpan.FromSeconds(1.1)); + TriggerTick(TimeSpan.FromSeconds(1.1 * 20)); Assert.Equal(KeepAliveState.PingSent, _connection._keepAlive._state); @@ -58,13 +56,11 @@ public async Task IntervalExceeded_WithoutActivity_PingSent() await InitializeConnectionAsync(_noopApplication).DefaultTimeout(); - DateTimeOffset now = _serviceContext.MockSystemClock.UtcNow; - // Heartbeat - TriggerTick(now); + TriggerTick(); // Heartbeat that exceeds interval - TriggerTick(now + TimeSpan.FromSeconds(1.1 * 2)); + TriggerTick(TimeSpan.FromSeconds(1.1 * 2)); await ExpectAsync(Http2FrameType.PING, withLength: 8, @@ -81,10 +77,8 @@ public async Task IntervalExceeded_WithActivity_NoPingSent() await InitializeConnectionAsync(_noopApplication).DefaultTimeout(); - DateTimeOffset now = _serviceContext.MockSystemClock.UtcNow; - // Heartbeat - TriggerTick(now); + TriggerTick(); await SendPingAsync(Http2PingFrameFlags.NONE).DefaultTimeout(); await ExpectAsync(Http2FrameType.PING, @@ -93,7 +87,7 @@ await ExpectAsync(Http2FrameType.PING, withStreamId: 0).DefaultTimeout(); // Heartbeat that exceeds interval - TriggerTick(now + TimeSpan.FromSeconds(1.1)); + TriggerTick(TimeSpan.FromSeconds(1.1)); await StopConnectionAsync(expectedLastStreamId: 0, ignoreNonGoAwayFrames: false).DefaultTimeout(); } @@ -105,13 +99,11 @@ public async Task IntervalNotExceeded_NoPingSent() await InitializeConnectionAsync(_noopApplication).DefaultTimeout(); - DateTimeOffset now = new DateTimeOffset(1, TimeSpan.Zero); - // Heartbeats - TriggerTick(now); - TriggerTick(now + TimeSpan.FromSeconds(1.1)); - TriggerTick(now + TimeSpan.FromSeconds(1.1 * 2)); - TriggerTick(now + TimeSpan.FromSeconds(1.1 * 3)); + TriggerTick(); + TriggerTick(TimeSpan.FromSeconds(1.1)); + TriggerTick(TimeSpan.FromSeconds(1.1)); + TriggerTick(TimeSpan.FromSeconds(1.1)); await StopConnectionAsync(expectedLastStreamId: 0, ignoreNonGoAwayFrames: false).DefaultTimeout(); } @@ -123,19 +115,17 @@ public async Task IntervalExceeded_MultipleTimes_PingsNotSentWhileAwaitingOnAck( await InitializeConnectionAsync(_noopApplication).DefaultTimeout(); - DateTimeOffset now = _serviceContext.MockSystemClock.UtcNow; - - TriggerTick(now); - TriggerTick(now + TimeSpan.FromSeconds(1.1 * 2)); + TriggerTick(); + TriggerTick(TimeSpan.FromSeconds(1.1 * 2)); await ExpectAsync(Http2FrameType.PING, withLength: 8, withFlags: (byte)Http2PingFrameFlags.NONE, withStreamId: 0).DefaultTimeout(); - TriggerTick(now + TimeSpan.FromSeconds(1.1 * 3)); - TriggerTick(now + TimeSpan.FromSeconds(1.1 * 4)); - TriggerTick(now + TimeSpan.FromSeconds(1.1 * 5)); + TriggerTick(TimeSpan.FromSeconds(1.1)); + TriggerTick(TimeSpan.FromSeconds(1.1)); + TriggerTick(TimeSpan.FromSeconds(1.1)); await StopConnectionAsync(expectedLastStreamId: 0, ignoreNonGoAwayFrames: false).DefaultTimeout(); } @@ -147,11 +137,9 @@ public async Task IntervalExceeded_MultipleTimes_PingSentAfterAck() await InitializeConnectionAsync(_noopApplication).DefaultTimeout(); - DateTimeOffset now = _serviceContext.MockSystemClock.UtcNow; - // Heartbeats - TriggerTick(now); - TriggerTick(now + TimeSpan.FromSeconds(1.1 * 2)); + TriggerTick(); + TriggerTick(TimeSpan.FromSeconds(1.1 * 2)); await ExpectAsync(Http2FrameType.PING, withLength: 8, @@ -159,8 +147,8 @@ await ExpectAsync(Http2FrameType.PING, withStreamId: 0).DefaultTimeout(); await SendPingAsync(Http2PingFrameFlags.ACK).DefaultTimeout(); - TriggerTick(now + TimeSpan.FromSeconds(1.1 * 3)); - TriggerTick(now + TimeSpan.FromSeconds(1.1 * 4)); + TriggerTick(TimeSpan.FromSeconds(1.1)); + TriggerTick(TimeSpan.FromSeconds(1.1)); await ExpectAsync(Http2FrameType.PING, withLength: 8, @@ -168,8 +156,8 @@ await ExpectAsync(Http2FrameType.PING, withStreamId: 0).DefaultTimeout(); await SendPingAsync(Http2PingFrameFlags.ACK).DefaultTimeout(); - TriggerTick(now + TimeSpan.FromSeconds(1.1 * 5)); - TriggerTick(now + TimeSpan.FromSeconds(1.1 * 6)); + TriggerTick(TimeSpan.FromSeconds(1.1)); + TriggerTick(TimeSpan.FromSeconds(1.1)); await ExpectAsync(Http2FrameType.PING, withLength: 8, @@ -187,13 +175,11 @@ public async Task TimeoutExceeded_NoAck_GoAway() await InitializeConnectionAsync(_noopApplication).DefaultTimeout(); - DateTimeOffset now = _serviceContext.MockSystemClock.UtcNow; - // Heartbeat - TriggerTick(now); + TriggerTick(); // Heartbeat that exceeds interval - TriggerTick(now + TimeSpan.FromSeconds(1.1 * 2)); + TriggerTick(TimeSpan.FromSeconds(1.1 * 2)); await ExpectAsync(Http2FrameType.PING, withLength: 8, @@ -201,10 +187,10 @@ await ExpectAsync(Http2FrameType.PING, withStreamId: 0).DefaultTimeout(); // Heartbeat that exceeds timeout - TriggerTick(now + TimeSpan.FromSeconds(1.1 * 3)); - TriggerTick(now + TimeSpan.FromSeconds(1.1 * 4)); - TriggerTick(now + TimeSpan.FromSeconds(1.1 * 5)); - TriggerTick(now + TimeSpan.FromSeconds(1.1 * 6)); + TriggerTick(TimeSpan.FromSeconds(1.1)); + TriggerTick(TimeSpan.FromSeconds(1.1)); + TriggerTick(TimeSpan.FromSeconds(1.1)); + TriggerTick(TimeSpan.FromSeconds(1.1)); Assert.Equal(KeepAliveState.Timeout, _connection._keepAlive._state); @@ -219,13 +205,11 @@ public async Task TimeoutExceeded_NonPingActivity_NoGoAway() await InitializeConnectionAsync(_noopApplication).DefaultTimeout(); - DateTimeOffset now = _serviceContext.MockSystemClock.UtcNow; - // Heartbeat - TriggerTick(now); + TriggerTick(); // Heartbeat that exceeds interval - TriggerTick(now + TimeSpan.FromSeconds(1.1 * 2)); + TriggerTick(TimeSpan.FromSeconds(1.1 * 2)); Assert.Equal(KeepAliveState.PingSent, _connection._keepAlive._state); await ExpectAsync(Http2FrameType.PING, @@ -251,10 +235,8 @@ public async Task IntervalExceeded_StreamStarted_NoPingSent() await InitializeConnectionAsync(_noopApplication).DefaultTimeout(); - DateTimeOffset now = _serviceContext.MockSystemClock.UtcNow; - // Heartbeat - TriggerTick(now); + TriggerTick(); await StartStreamAsync(1, _browserRequestHeaders, endStream: true).DefaultTimeout(); @@ -264,7 +246,7 @@ await ExpectAsync(Http2FrameType.HEADERS, withStreamId: 1).DefaultTimeout(); // Heartbeat that exceeds interval - TriggerTick(now + TimeSpan.FromSeconds(1.1)); + TriggerTick(TimeSpan.FromSeconds(1.1)); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false).DefaultTimeout(); } @@ -286,10 +268,8 @@ await InitializeConnectionAsync(async c => await c.Request.Body.FlushAsync(); }, expectedWindowUpdate: false).DefaultTimeout(); - DateTimeOffset now = _serviceContext.MockSystemClock.UtcNow; - // Heartbeat - TriggerTick(now); + TriggerTick(); await StartStreamAsync(1, _browserRequestHeaders, endStream: false).DefaultTimeout(); @@ -300,7 +280,7 @@ await InitializeConnectionAsync(async c => await SendDataAsync(1, new byte[16383], false).DefaultTimeout(); // Heartbeat that exceeds interval - TriggerTick(now + TimeSpan.FromSeconds(1.1)); + TriggerTick(TimeSpan.FromSeconds(1.1)); // Send ping that will update the keep alive on the server await SendPingAsync(Http2PingFrameFlags.NONE).DefaultTimeout(); @@ -310,7 +290,7 @@ await ExpectAsync(Http2FrameType.PING, withStreamId: 0).DefaultTimeout(); // Heartbeat that exceeds interval - TriggerTick(now + TimeSpan.FromSeconds(1.1 * 2)); + TriggerTick(TimeSpan.FromSeconds(1.1)); // Continue request delegate on server tcs.SetResult(); @@ -341,10 +321,8 @@ await InitializeConnectionAsync(async c => await c.Request.Body.FlushAsync(); }, expectedWindowUpdate: false).DefaultTimeout(); - DateTimeOffset now = _serviceContext.MockSystemClock.UtcNow; - // Heartbeat - TriggerTick(now); + TriggerTick(); await StartStreamAsync(1, _browserRequestHeaders, endStream: false).DefaultTimeout(); @@ -355,10 +333,10 @@ await InitializeConnectionAsync(async c => await SendDataAsync(1, new byte[16383], false).DefaultTimeout(); // Heartbeat that exceeds interval - TriggerTick(now + TimeSpan.FromSeconds(1.1)); + TriggerTick(TimeSpan.FromSeconds(1.1)); // Heartbeat that triggers keep alive ping - TriggerTick(now + TimeSpan.FromSeconds(1.1 * 2)); + TriggerTick(TimeSpan.FromSeconds(1.1)); await ExpectAsync(Http2FrameType.PING, withLength: 8, @@ -369,7 +347,7 @@ await ExpectAsync(Http2FrameType.PING, await SendPingAsync(Http2PingFrameFlags.ACK).DefaultTimeout(); // Heartbeat that exceeds interval - TriggerTick(now + TimeSpan.FromSeconds(1.1 * 3)); + TriggerTick(TimeSpan.FromSeconds(1.1)); // Continue request delegate on server tcs.SetResult(); diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs index 77239526401a..a656cd1c8bc1 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Buffers; using System.Buffers.Binary; using System.Collections.Concurrent; @@ -118,6 +119,7 @@ protected static IEnumerable> ReadRateRequestHeader internal readonly DynamicHPackEncoder _hpackEncoder; private readonly byte[] _headerEncodingBuffer = new byte[Http2PeerSettings.MinAllowedMaxFrameSize]; + private readonly MockTimeProvider _timeProvider = new(); internal readonly TimeoutControl _timeoutControl; protected readonly Mock _mockConnectionContext = new Mock(); internal readonly Mock _mockTimeoutHandler = new Mock(); @@ -162,7 +164,7 @@ public Http2TestBase() _hpackDecoder = new HPackDecoder((int)_clientSettings.HeaderTableSize, MaxRequestHeaderFieldSize); _hpackEncoder = new DynamicHPackEncoder(); - _timeoutControl = new TimeoutControl(_mockTimeoutHandler.Object); + _timeoutControl = new TimeoutControl(_mockTimeoutHandler.Object, _timeProvider); _mockTimeoutControl = new Mock(_timeoutControl) { CallBase = true }; _timeoutControl.Debugger = Mock.Of(); @@ -390,6 +392,8 @@ public override void Initialize(TestContext context, MethodInfo methodInfo, obje _serviceContext = new TestServiceContext(LoggerFactory) { Scheduler = PipeScheduler.Inline, + MockTimeProvider = _timeProvider, + TimeProvider = _timeProvider, }; TestSink.MessageLogged += context => @@ -473,7 +477,7 @@ protected void CreateConnection() _mockTimeoutHandler.Setup(h => h.OnTimeout(It.IsAny())) .Callback(r => httpConnection.OnTimeout(r)); - _timeoutControl.Initialize(_serviceContext.SystemClock.UtcNow.Ticks); + _timeoutControl.Initialize(); } private class TestConnectionMetricsContextFeature : IConnectionMetricsContextFeature @@ -1345,25 +1349,31 @@ protected void VerifyDecodedRequestHeaders(IEnumerable memoryPool, PipeScheduler writerScheduler) => new PipeOptions @@ -1424,14 +1434,14 @@ public MockTimeoutControlBase(ITimeoutControl realTimeoutControl) public virtual TimeoutReason TimerReason => _realTimeoutControl.TimerReason; - public virtual void SetTimeout(long ticks, TimeoutReason timeoutReason) + public virtual void SetTimeout(TimeSpan timeout, TimeoutReason timeoutReason) { - _realTimeoutControl.SetTimeout(ticks, timeoutReason); + _realTimeoutControl.SetTimeout(timeout, timeoutReason); } - public virtual void ResetTimeout(long ticks, TimeoutReason timeoutReason) + public virtual void ResetTimeout(TimeSpan timeout, TimeoutReason timeoutReason) { - _realTimeoutControl.ResetTimeout(ticks, timeoutReason); + _realTimeoutControl.ResetTimeout(timeout, timeoutReason); } public virtual void CancelTimeout() @@ -1484,9 +1494,9 @@ public virtual void BytesWrittenToBuffer(MinDataRate minRate, long size) _realTimeoutControl.BytesWrittenToBuffer(minRate, size); } - public virtual void Tick(DateTimeOffset now) + public virtual void Tick(long timestamp) { - _realTimeoutControl.Tick(now); + _realTimeoutControl.Tick(timestamp); } public long GetResponseDrainDeadline(long ticks, MinDataRate minRate) diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TimeoutTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TimeoutTests.cs index fcdb46d9c80a..96701afc427b 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TimeoutTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TimeoutTests.cs @@ -22,11 +22,11 @@ public async Task Preamble_NotReceivedInitially_WithinKeepAliveTimeout_ClosesCon _connectionTask = _connection.ProcessRequestsAsync(new DummyApplication(_noopApplication)); - AdvanceClock(limits.KeepAliveTimeout + Heartbeat.Interval); + AdvanceTime(limits.KeepAliveTimeout + Heartbeat.Interval); _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); - AdvanceClock(TimeSpan.FromTicks(1)); + AdvanceTime(TimeSpan.FromTicks(1)); _mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.KeepAlive), Times.Once); @@ -42,11 +42,11 @@ public async Task HEADERS_NotReceivedInitially_WithinKeepAliveTimeout_ClosesConn await InitializeConnectionAsync(_noopApplication); - AdvanceClock(limits.KeepAliveTimeout + Heartbeat.Interval); + AdvanceTime(limits.KeepAliveTimeout + Heartbeat.Interval); _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); - AdvanceClock(TimeSpan.FromTicks(1)); + AdvanceTime(TimeSpan.FromTicks(1)); _mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.KeepAlive), Times.Once); @@ -62,16 +62,16 @@ public async Task HEADERS_NotReceivedAfterFirstRequest_WithinKeepAliveTimeout_Cl await InitializeConnectionAsync(_noopApplication); - AdvanceClock(limits.KeepAliveTimeout + Heartbeat.Interval); + AdvanceTime(limits.KeepAliveTimeout + Heartbeat.Interval); // keep-alive timeout set but not fired. - _mockTimeoutControl.Verify(c => c.SetTimeout(It.IsAny(), TimeoutReason.KeepAlive), Times.Once); + _mockTimeoutControl.Verify(c => c.SetTimeout(It.IsAny(), TimeoutReason.KeepAlive), Times.Once); _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); // The KeepAlive timeout is set when the stream completes processing on a background thread, so we need to hook the // keep-alive set afterwards to make a reliable test. var setTimeoutTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - _mockTimeoutControl.Setup(c => c.SetTimeout(It.IsAny(), TimeoutReason.KeepAlive)).Callback((t, r) => + _mockTimeoutControl.Setup(c => c.SetTimeout(It.IsAny(), TimeoutReason.KeepAlive)).Callback((t, r) => { _timeoutControl.SetTimeout(t, r); setTimeoutTcs.SetResult(); @@ -81,7 +81,7 @@ public async Task HEADERS_NotReceivedAfterFirstRequest_WithinKeepAliveTimeout_Cl await SendHeadersAsync(1, Http2HeadersFrameFlags.END_STREAM, _browserRequestHeaders); await SendEmptyContinuationFrameAsync(1, Http2ContinuationFrameFlags.END_HEADERS); - _mockTimeoutControl.Verify(c => c.SetTimeout(It.IsAny(), TimeoutReason.RequestHeaders), Times.Once); + _mockTimeoutControl.Verify(c => c.SetTimeout(It.IsAny(), TimeoutReason.RequestHeaders), Times.Once); await ExpectAsync(Http2FrameType.HEADERS, withLength: 36, @@ -90,11 +90,11 @@ await ExpectAsync(Http2FrameType.HEADERS, await setTimeoutTcs.Task.DefaultTimeout(); - AdvanceClock(limits.KeepAliveTimeout + Heartbeat.Interval); + AdvanceTime(limits.KeepAliveTimeout + Heartbeat.Interval); _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); - AdvanceClock(TimeSpan.FromTicks(1)); + AdvanceTime(TimeSpan.FromTicks(1)); _mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.KeepAlive), Times.Once); @@ -113,8 +113,8 @@ public async Task PING_WithinKeepAliveTimeout_ResetKeepAliveTimeout() await InitializeConnectionAsync(_noopApplication); // Connection starts and sets keep alive timeout - _mockTimeoutControl.Verify(c => c.SetTimeout(It.IsAny(), TimeoutReason.KeepAlive), Times.Once); - _mockTimeoutControl.Verify(c => c.ResetTimeout(It.IsAny(), TimeoutReason.KeepAlive), Times.Never); + _mockTimeoutControl.Verify(c => c.SetTimeout(It.IsAny(), TimeoutReason.KeepAlive), Times.Once); + _mockTimeoutControl.Verify(c => c.ResetTimeout(It.IsAny(), TimeoutReason.KeepAlive), Times.Never); await SendPingAsync(Http2PingFrameFlags.NONE); await ExpectAsync(Http2FrameType.PING, @@ -123,7 +123,7 @@ await ExpectAsync(Http2FrameType.PING, withStreamId: 0); // Server resets keep alive timeout - _mockTimeoutControl.Verify(c => c.ResetTimeout(It.IsAny(), TimeoutReason.KeepAlive), Times.Once); + _mockTimeoutControl.Verify(c => c.ResetTimeout(It.IsAny(), TimeoutReason.KeepAlive), Times.Once); } [Fact] @@ -136,8 +136,8 @@ public async Task PING_NoKeepAliveTimeout_DoesNotResetKeepAliveTimeout() await InitializeConnectionAsync(_echoApplication); // Connection starts and sets keep alive timeout - _mockTimeoutControl.Verify(c => c.SetTimeout(It.IsAny(), TimeoutReason.KeepAlive), Times.Once); - _mockTimeoutControl.Verify(c => c.ResetTimeout(It.IsAny(), TimeoutReason.KeepAlive), Times.Never); + _mockTimeoutControl.Verify(c => c.SetTimeout(It.IsAny(), TimeoutReason.KeepAlive), Times.Once); + _mockTimeoutControl.Verify(c => c.ResetTimeout(It.IsAny(), TimeoutReason.KeepAlive), Times.Never); _mockTimeoutControl.Verify(c => c.CancelTimeout(), Times.Never); // Stream will stay open because it is waiting for request body to end @@ -153,7 +153,7 @@ await ExpectAsync(Http2FrameType.PING, withStreamId: 0); // Server doesn't reset keep alive timeout because it isn't running - _mockTimeoutControl.Verify(c => c.ResetTimeout(It.IsAny(), TimeoutReason.KeepAlive), Times.Never); + _mockTimeoutControl.Verify(c => c.ResetTimeout(It.IsAny(), TimeoutReason.KeepAlive), Times.Never); // End stream await SendDataAsync(1, _helloWorldBytes, endStream: true); @@ -178,13 +178,13 @@ public async Task HEADERS_ReceivedWithoutAllCONTINUATIONs_WithinRequestHeadersTi await SendEmptyContinuationFrameAsync(1, Http2ContinuationFrameFlags.NONE); - AdvanceClock(limits.RequestHeadersTimeout + Heartbeat.Interval); + AdvanceTime(limits.RequestHeadersTimeout + Heartbeat.Interval); _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); await SendEmptyContinuationFrameAsync(1, Http2ContinuationFrameFlags.NONE); - AdvanceClock(TimeSpan.FromTicks(1)); + AdvanceTime(TimeSpan.FromTicks(1)); _mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.RequestHeaders), Times.Once); @@ -212,13 +212,13 @@ public async Task ResponseDrain_SlowerThanMinimumDataRate_AbortsConnection() await WaitForConnectionStopAsync(expectedLastStreamId: 0, ignoreNonGoAwayFrames: false); - AdvanceClock(TimeSpan.FromSeconds(_bytesReceived / limits.MinResponseDataRate.BytesPerSecond) + + AdvanceTime(TimeSpan.FromSeconds(_bytesReceived / limits.MinResponseDataRate.BytesPerSecond) + limits.MinResponseDataRate.GracePeriod + Heartbeat.Interval - TimeSpan.FromSeconds(.5)); _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); _mockConnectionContext.Verify(c => c.Abort(It.IsAny()), Times.Never); - AdvanceClock(TimeSpan.FromSeconds(1)); + AdvanceTime(TimeSpan.FromSeconds(1)); _mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.WriteDataRate), Times.Once); @@ -242,7 +242,7 @@ public async Task AbortedStream_ResetsAndDrainsRequest_RefusesFramesAfterCooldow // Remove callback that completes _pair.Application.Output on abort. _mockConnectionContext.Reset(); - var mockSystemClock = _serviceContext.MockSystemClock; + var mockTimeProvider = _serviceContext.MockTimeProvider; var headers = new[] { @@ -268,7 +268,7 @@ async Task AdvanceClockAndSendFrames() while (!closed) { // Just past the timeout - mockSystemClock.UtcNow += Constants.RequestBodyDrainTimeout + TimeSpan.FromTicks(1); + mockTimeProvider.Advance(Constants.RequestBodyDrainTimeout + TimeSpan.FromTicks(1)); // Send an extra frame to make it fail switch (finalFrameType) @@ -355,12 +355,12 @@ await ExpectAsync(Http2FrameType.HEADERS, TriggerTick(); // Don't read data frame to induce "socket" backpressure. - AdvanceClock(TimeSpan.FromSeconds((_bytesReceived + _helloWorldBytes.Length) / limits.MinResponseDataRate.BytesPerSecond) + + AdvanceTime(TimeSpan.FromSeconds((_bytesReceived + _helloWorldBytes.Length) / limits.MinResponseDataRate.BytesPerSecond) + limits.MinResponseDataRate.GracePeriod + Heartbeat.Interval - TimeSpan.FromSeconds(.5)); _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); - AdvanceClock(TimeSpan.FromSeconds(1)); + AdvanceTime(TimeSpan.FromSeconds(1)); _mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.WriteDataRate), Times.Once); @@ -410,11 +410,11 @@ await ExpectAsync(Http2FrameType.HEADERS, limits.MinResponseDataRate.GracePeriod + Heartbeat.Interval - TimeSpan.FromSeconds(.5); // Don't read data frame to induce "socket" backpressure. - AdvanceClock(timeToWriteMaxData); + AdvanceTime(timeToWriteMaxData); _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); - AdvanceClock(TimeSpan.FromSeconds(1)); + AdvanceTime(TimeSpan.FromSeconds(1)); _mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.WriteDataRate), Times.Once); @@ -462,12 +462,12 @@ await ExpectAsync(Http2FrameType.DATA, TriggerTick(); // Don't send WINDOW_UPDATE to induce flow-control backpressure - AdvanceClock(TimeSpan.FromSeconds(_bytesReceived / limits.MinResponseDataRate.BytesPerSecond) + + AdvanceTime(TimeSpan.FromSeconds(_bytesReceived / limits.MinResponseDataRate.BytesPerSecond) + limits.MinResponseDataRate.GracePeriod + Heartbeat.Interval - TimeSpan.FromSeconds(.5)); _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); - AdvanceClock(TimeSpan.FromSeconds(1)); + AdvanceTime(TimeSpan.FromSeconds(1)); _mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.WriteDataRate), Times.Once); @@ -516,11 +516,11 @@ await ExpectAsync(Http2FrameType.DATA, limits.MinResponseDataRate.GracePeriod + Heartbeat.Interval - TimeSpan.FromSeconds(.5); // Don't send WINDOW_UPDATE to induce flow-control backpressure - AdvanceClock(timeToWriteMaxData); + AdvanceTime(timeToWriteMaxData); _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); - AdvanceClock(TimeSpan.FromSeconds(1)); + AdvanceTime(TimeSpan.FromSeconds(1)); _mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.WriteDataRate), Times.Once); @@ -581,11 +581,11 @@ await ExpectAsync(Http2FrameType.DATA, limits.MinResponseDataRate.GracePeriod + Heartbeat.Interval - TimeSpan.FromSeconds(.5); // Don't send WINDOW_UPDATE to induce flow-control backpressure - AdvanceClock(timeToWriteMaxData); + AdvanceTime(timeToWriteMaxData); _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); - AdvanceClock(TimeSpan.FromSeconds(1)); + AdvanceTime(TimeSpan.FromSeconds(1)); _mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.WriteDataRate), Times.Once); @@ -627,11 +627,11 @@ await ExpectAsync(Http2FrameType.DATA, withStreamId: 1); // Don't send any more data and advance just to and then past the grace period. - AdvanceClock(limits.MinRequestBodyDataRate.GracePeriod); + AdvanceTime(limits.MinRequestBodyDataRate.GracePeriod); _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); - AdvanceClock(TimeSpan.FromTicks(1)); + AdvanceTime(TimeSpan.FromTicks(1)); _mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.ReadDataRate), Times.Once); @@ -677,11 +677,11 @@ await ExpectAsync(Http2FrameType.DATA, var timeToReadMaxData = TimeSpan.FromSeconds(_maxData.Length / limits.MinRequestBodyDataRate.BytesPerSecond) - TimeSpan.FromSeconds(.5); // Don't send any more data and advance just to and then past the rate timeout. - AdvanceClock(timeToReadMaxData); + AdvanceTime(timeToReadMaxData); _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); - AdvanceClock(TimeSpan.FromSeconds(1)); + AdvanceTime(TimeSpan.FromSeconds(1)); _mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.ReadDataRate), Times.Once); @@ -743,11 +743,11 @@ await ExpectAsync(Http2FrameType.DATA, timeToReadMaxData -= TimeSpan.FromSeconds(.5); // Don't send any more data and advance just to and then past the rate timeout. - AdvanceClock(timeToReadMaxData); + AdvanceTime(timeToReadMaxData); _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); - AdvanceClock(TimeSpan.FromSeconds(1)); + AdvanceTime(TimeSpan.FromSeconds(1)); _mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.ReadDataRate), Times.Once); @@ -810,11 +810,11 @@ await ExpectAsync(Http2FrameType.DATA, var timeToReadMaxData = TimeSpan.FromSeconds(_maxData.Length / limits.MinRequestBodyDataRate.BytesPerSecond) - TimeSpan.FromSeconds(.5); // Don't send any more data and advance just to and then past the rate timeout. - AdvanceClock(timeToReadMaxData); + AdvanceTime(timeToReadMaxData); _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); - AdvanceClock(TimeSpan.FromSeconds(1)); + AdvanceTime(TimeSpan.FromSeconds(1)); _mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.ReadDataRate), Times.Once); @@ -861,11 +861,11 @@ await ExpectAsync(Http2FrameType.DATA, withStreamId: 1); // Don't send any more data and advance just to and then past the grace period. - AdvanceClock(limits.MinRequestBodyDataRate.GracePeriod); + AdvanceTime(limits.MinRequestBodyDataRate.GracePeriod); _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); - AdvanceClock(TimeSpan.FromTicks(1)); + AdvanceTime(TimeSpan.FromTicks(1)); _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); @@ -929,7 +929,7 @@ await ExpectAsync(Http2FrameType.DATA, withStreamId: 3); // No matter how much time elapses there is no read timeout because the connection window is too small. - AdvanceClock(TimeSpan.FromDays(1)); + AdvanceTime(TimeSpan.FromDays(1)); _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); @@ -949,11 +949,11 @@ await ExpectAsync(Http2FrameType.HEADERS, var expectedUpdateSize = ((framesConnectionInWindow / 2) + 1) * _maxData.Length + _helloWorldBytes.Length; Assert.Equal(expectedUpdateSize, updateFrame.WindowUpdateSizeIncrement); - AdvanceClock(limits.MinRequestBodyDataRate.GracePeriod); + AdvanceTime(limits.MinRequestBodyDataRate.GracePeriod); _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); - AdvanceClock(TimeSpan.FromTicks(1)); + AdvanceTime(TimeSpan.FromTicks(1)); _mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.ReadDataRate), Times.Once); diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2WebSocketTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2WebSocketTests.cs index 1bf786788b64..cbe0d1ce1d14 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2WebSocketTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2WebSocketTests.cs @@ -338,7 +338,7 @@ await InitializeConnectionAsync(async context => await SendHeadersAsync(1, Http2HeadersFrameFlags.END_HEADERS, headers); // Don't send any more data and advance just to and then past the grace period. - AdvanceClock(limits.MinRequestBodyDataRate.GracePeriod + TimeSpan.FromTicks(1)); + AdvanceTime(limits.MinRequestBodyDataRate.GracePeriod + TimeSpan.FromTicks(1)); _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3ConnectionTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3ConnectionTests.cs index 25be0a175b51..b1ff24b43bf8 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3ConnectionTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3ConnectionTests.cs @@ -290,8 +290,6 @@ await Http3Api.WaitForConnectionErrorAsync( [Fact] public async Task ControlStream_ClientToServer_Completes_ConnectionError() { - var now = _serviceContext.MockSystemClock.UtcNow; - await Http3Api.InitializeConnectionAsync(_noopApplication); var controlStream = await Http3Api.CreateControlStream(id: 0); @@ -302,8 +300,8 @@ public async Task ControlStream_ClientToServer_Completes_ConnectionError() // Wait for control stream to finish processing and exit. await controlStream.OnStreamCompletedTask.DefaultTimeout(); - Http3Api.TriggerTick(now); - Http3Api.TriggerTick(now + TimeSpan.FromSeconds(1)); + Http3Api.TriggerTick(); + Http3Api.TriggerTick(TimeSpan.FromSeconds(1)); await Http3Api.WaitForConnectionErrorAsync( ignoreNonGoAwayFrames: true, @@ -339,16 +337,14 @@ public async Task GOAWAY_TriggersLifetimeNotification_ConnectionClosedRequested( [Fact] public async Task ControlStream_ServerToClient_ErrorInitializing_ConnectionError() { - var now = _serviceContext.MockSystemClock.UtcNow; - await Http3Api.InitializeConnectionAsync(_noopApplication); var controlStream = await Http3Api.GetInboundControlStream(); controlStream.StreamContext.Close(); - Http3Api.TriggerTick(now); - Http3Api.TriggerTick(now + TimeSpan.FromSeconds(1)); + Http3Api.TriggerTick(); + Http3Api.TriggerTick(TimeSpan.FromSeconds(1)); await Http3Api.WaitForConnectionErrorAsync( ignoreNonGoAwayFrames: true, diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TestBase.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TestBase.cs index b5d0f612afd5..9ab1efb1726b 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TestBase.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TestBase.cs @@ -145,7 +145,7 @@ public override void Initialize(TestContext context, MethodInfo methodInfo, obje Scheduler = PipeScheduler.Inline, }; - Http3Api = new Http3InMemory(_serviceContext, _serviceContext.MockSystemClock, _mockTimeoutHandler.Object, LoggerFactory); + Http3Api = new Http3InMemory(_serviceContext, _serviceContext.MockTimeProvider, _mockTimeoutHandler.Object, LoggerFactory); } public void AssertExpectedErrorMessages(string expectedErrorMessage) diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TimeoutTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TimeoutTests.cs index fb741c527cb3..8120b171059f 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TimeoutTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TimeoutTests.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Net.Http; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Http; @@ -25,7 +26,7 @@ public async Task KeepAliveTimeout_ControlStreamNotReceived_ConnectionClosed() var controlStream = await Http3Api.GetInboundControlStream().DefaultTimeout(); await controlStream.ExpectSettingsAsync().DefaultTimeout(); - Http3Api.AdvanceClock(limits.KeepAliveTimeout + TimeSpan.FromTicks(1)); + Http3Api.AdvanceTime(limits.KeepAliveTimeout + TimeSpan.FromTicks(1)); await Http3Api.WaitForConnectionStopAsync(0, false, expectedErrorCode: Http3ErrorCode.NoError); } @@ -41,7 +42,7 @@ public async Task KeepAliveTimeout_RequestNotReceived_ConnectionClosed() var controlStream = await Http3Api.GetInboundControlStream().DefaultTimeout(); await controlStream.ExpectSettingsAsync().DefaultTimeout(); - Http3Api.AdvanceClock(limits.KeepAliveTimeout + TimeSpan.FromTicks(1)); + Http3Api.AdvanceTime(limits.KeepAliveTimeout + TimeSpan.FromTicks(1)); await Http3Api.WaitForConnectionStopAsync(0, false, expectedErrorCode: Http3ErrorCode.NoError); } @@ -69,7 +70,7 @@ public async Task KeepAliveTimeout_AfterRequestComplete_ConnectionClosed() await requestStream.ExpectReceiveEndOfStream(); await requestStream.OnDisposedTask.DefaultTimeout(); - Http3Api.AdvanceClock(limits.KeepAliveTimeout + Heartbeat.Interval + TimeSpan.FromTicks(1)); + Http3Api.AdvanceTime(limits.KeepAliveTimeout + Heartbeat.Interval + TimeSpan.FromTicks(1)); await Http3Api.WaitForConnectionStopAsync(4, false, expectedErrorCode: Http3ErrorCode.NoError); } @@ -101,11 +102,11 @@ await Http3Api.InitializeConnectionAsync(_ => await requestReceivedTcs.Task; - Http3Api.AdvanceClock(limits.KeepAliveTimeout); - Http3Api.AdvanceClock(limits.KeepAliveTimeout); - Http3Api.AdvanceClock(limits.KeepAliveTimeout); - Http3Api.AdvanceClock(limits.KeepAliveTimeout); - Http3Api.AdvanceClock(limits.KeepAliveTimeout); + Http3Api.AdvanceTime(limits.KeepAliveTimeout); + Http3Api.AdvanceTime(limits.KeepAliveTimeout); + Http3Api.AdvanceTime(limits.KeepAliveTimeout); + Http3Api.AdvanceTime(limits.KeepAliveTimeout); + Http3Api.AdvanceTime(limits.KeepAliveTimeout); requestFinishedTcs.SetResult(); @@ -114,7 +115,7 @@ await Http3Api.InitializeConnectionAsync(_ => await requestStream.ExpectReceiveEndOfStream(); await requestStream.OnDisposedTask.DefaultTimeout(); - Http3Api.AdvanceClock(limits.KeepAliveTimeout + Heartbeat.Interval + TimeSpan.FromTicks(1)); + Http3Api.AdvanceTime(limits.KeepAliveTimeout + Heartbeat.Interval + TimeSpan.FromTicks(1)); await Http3Api.WaitForConnectionStopAsync(4, false, expectedErrorCode: Http3ErrorCode.NoError); } @@ -122,7 +123,8 @@ await Http3Api.InitializeConnectionAsync(_ => [Fact] public async Task HEADERS_IncompleteFrameReceivedWithinRequestHeadersTimeout_StreamError() { - var now = _serviceContext.MockSystemClock.UtcNow; + var timeProvider = _serviceContext.MockTimeProvider; + var timestamp = timeProvider.GetTimestamp(); var limits = _serviceContext.ServerOptions.Limits; var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(_noopApplication, null).DefaultTimeout(); @@ -136,12 +138,12 @@ public async Task HEADERS_IncompleteFrameReceivedWithinRequestHeadersTimeout_Str var serverRequestStream = Http3Api.Connection._streams[requestStream.StreamId]; - Http3Api.TriggerTick(now); - Http3Api.TriggerTick(now + limits.RequestHeadersTimeout); + Http3Api.TriggerTick(); + Http3Api.TriggerTick(limits.RequestHeadersTimeout); - Assert.Equal((now + limits.RequestHeadersTimeout).Ticks, serverRequestStream.StreamTimeoutTicks); + Assert.Equal(timeProvider.GetTimestamp(timestamp, limits.RequestHeadersTimeout), serverRequestStream.StreamTimeoutTimestamp); - Http3Api.TriggerTick(now + limits.RequestHeadersTimeout + TimeSpan.FromTicks(1)); + Http3Api.TriggerTick(TimeSpan.FromTicks(1)); await requestStream.WaitForStreamErrorAsync( Http3ErrorCode.RequestRejected, @@ -156,7 +158,7 @@ public async Task HEADERS_HeaderFrameReceivedWithinRequestHeadersTimeout_Success { Http3Api._serviceContext.ServerOptions.EnableWebTransportAndH3Datagrams = pendingStreamsEnabled; - var now = _serviceContext.MockSystemClock.UtcNow; + var timestamp = _serviceContext.MockTimeProvider.GetTimestamp(); var limits = _serviceContext.ServerOptions.Limits; var headers = new[] { @@ -186,16 +188,16 @@ public async Task HEADERS_HeaderFrameReceivedWithinRequestHeadersTimeout_Success serverRequestStream = Http3Api.Connection._streams[requestStream.StreamId]; } - Http3Api.TriggerTick(now); - Http3Api.TriggerTick(now + limits.RequestHeadersTimeout); + Http3Api.TriggerTick(); + Http3Api.AdvanceTime(limits.RequestHeadersTimeout); - Assert.Equal((now + limits.RequestHeadersTimeout).Ticks, serverRequestStream.StreamTimeoutTicks); + Assert.Equal(_serviceContext.TimeProvider.GetTimestamp(timestamp, limits.RequestHeadersTimeout), serverRequestStream.StreamTimeoutTimestamp); await requestStream.SendHeadersAsync(headers).DefaultTimeout(); await requestStream.OnHeaderReceivedTask.DefaultTimeout(); - Http3Api.TriggerTick(now + limits.RequestHeadersTimeout + TimeSpan.FromTicks(1)); + Http3Api.AdvanceTime(TimeSpan.FromTicks(1)); await requestStream.SendDataAsync(Memory.Empty, endStream: true); @@ -209,7 +211,8 @@ public async Task ControlStream_HeaderNotReceivedWithinRequestHeadersTimeout_Str { Http3Api._serviceContext.ServerOptions.EnableWebTransportAndH3Datagrams = true; - var now = _serviceContext.MockSystemClock.UtcNow; + var timeProvider = _serviceContext.MockTimeProvider; + var timestamp = timeProvider.GetTimestamp(); var limits = _serviceContext.ServerOptions.Limits; await Http3Api.InitializeConnectionAsync(_noopApplication).DefaultTimeout(); @@ -222,12 +225,12 @@ public async Task ControlStream_HeaderNotReceivedWithinRequestHeadersTimeout_Str await outboundControlStream.OnUnidentifiedStreamCreatedTask.DefaultTimeout(); var serverInboundControlStream = Http3Api.Connection._unidentifiedStreams[outboundControlStream.StreamId]; - Http3Api.TriggerTick(now); - Http3Api.TriggerTick(now + limits.RequestHeadersTimeout); + Http3Api.TriggerTick(); + Http3Api.AdvanceTime(limits.RequestHeadersTimeout); - Assert.Equal((now + limits.RequestHeadersTimeout).Ticks, serverInboundControlStream.StreamTimeoutTicks); + Assert.Equal(timeProvider.GetTimestamp(timestamp, limits.RequestHeadersTimeout), serverInboundControlStream.StreamTimeoutTimestamp); - Http3Api.TriggerTick(now + limits.RequestHeadersTimeout + TimeSpan.FromTicks(1)); + Http3Api.AdvanceTime(TimeSpan.FromTicks(1)); } [Fact] @@ -235,7 +238,9 @@ public async Task ControlStream_HeaderNotReceivedWithinRequestHeadersTimeout_Str { Http3Api._serviceContext.ServerOptions.EnableWebTransportAndH3Datagrams = false; - var now = _serviceContext.MockSystemClock.UtcNow; + var timeProvider = _serviceContext.MockTimeProvider; + var timestamp = timeProvider.GetTimestamp(); + Http3Api._timeoutControl.Initialize(); var limits = _serviceContext.ServerOptions.Limits; var headers = new[] { @@ -256,12 +261,12 @@ public async Task ControlStream_HeaderNotReceivedWithinRequestHeadersTimeout_Str var serverInboundControlStream = Http3Api.Connection._streams[outboundControlStream.StreamId]; - Http3Api.TriggerTick(now); - Http3Api.TriggerTick(now + limits.RequestHeadersTimeout); + Http3Api.TriggerTick(); + Http3Api.TriggerTick(limits.RequestHeadersTimeout); - Assert.Equal((now + limits.RequestHeadersTimeout).Ticks, serverInboundControlStream.StreamTimeoutTicks); + Assert.Equal(timeProvider.GetTimestamp(timestamp, limits.RequestHeadersTimeout), serverInboundControlStream.StreamTimeoutTimestamp); - Http3Api.TriggerTick(now + limits.RequestHeadersTimeout + TimeSpan.FromTicks(1)); + Http3Api.TriggerTick(TimeSpan.FromTicks(1)); await outboundControlStream.WaitForStreamErrorAsync( Http3ErrorCode.StreamCreationError, @@ -272,7 +277,6 @@ await outboundControlStream.WaitForStreamErrorAsync( [Fact] public async Task ControlStream_HeaderReceivedWithinRequestHeadersTimeout_StreamError() { - var now = _serviceContext.MockSystemClock.UtcNow; var limits = _serviceContext.ServerOptions.Limits; await Http3Api.InitializeConnectionAsync(_noopApplication).DefaultTimeout(); @@ -280,14 +284,14 @@ public async Task ControlStream_HeaderReceivedWithinRequestHeadersTimeout_Stream var controlStream = await Http3Api.GetInboundControlStream().DefaultTimeout(); await controlStream.ExpectSettingsAsync().DefaultTimeout(); - Http3Api.TriggerTick(now); - Http3Api.TriggerTick(now + limits.RequestHeadersTimeout + TimeSpan.FromTicks(1)); + Http3Api.TriggerTick(); + Http3Api.TriggerTick(limits.RequestHeadersTimeout + TimeSpan.FromTicks(1)); var outboundControlStream = await Http3Api.CreateControlStream(id: 0); await outboundControlStream.OnStreamCreatedTask.DefaultTimeout(); - Http3Api.TriggerTick(now + limits.RequestHeadersTimeout + TimeSpan.FromTicks(1)); + Http3Api.TriggerTick(); } [Theory] @@ -297,7 +301,7 @@ public async Task ControlStream_RequestHeadersTimeoutMaxValue_ExpirationIsMaxVal { Http3Api._serviceContext.ServerOptions.EnableWebTransportAndH3Datagrams = pendingStreamEnabled; - var now = _serviceContext.MockSystemClock.UtcNow; + var timeProvider = _serviceContext.MockTimeProvider; var limits = _serviceContext.ServerOptions.Limits; limits.RequestHeadersTimeout = TimeSpan.MaxValue; @@ -320,21 +324,21 @@ public async Task ControlStream_RequestHeadersTimeoutMaxValue_ExpirationIsMaxVal serverInboundControlStream = Http3Api.Connection._streams[outboundControlStream.StreamId]; } - Http3Api.TriggerTick(now); + Http3Api.TriggerTick(); - Assert.Equal(TimeSpan.MaxValue.Ticks, serverInboundControlStream.StreamTimeoutTicks); + Assert.Equal(TimeSpan.MaxValue.ToTicks(timeProvider), serverInboundControlStream.StreamTimeoutTimestamp); } [Fact] public async Task DATA_Received_TooSlowlyOnSmallRead_AbortsConnectionAfterGracePeriod() { - var mockSystemClock = _serviceContext.MockSystemClock; + var mockTimeProvider = _serviceContext.MockTimeProvider; var limits = _serviceContext.ServerOptions.Limits; // Use non-default value to ensure the min request and response rates aren't mixed up. limits.MinRequestBodyDataRate = new MinDataRate(480, TimeSpan.FromSeconds(2.5)); - Http3Api._timeoutControl.Initialize(mockSystemClock.UtcNow.Ticks); + Http3Api._timeoutControl.Initialize(); await Http3Api.InitializeConnectionAsync(_readRateApplication); @@ -350,11 +354,11 @@ public async Task DATA_Received_TooSlowlyOnSmallRead_AbortsConnectionAfterGraceP await requestStream.ExpectDataAsync(); // Don't send any more data and advance just to and then past the grace period. - Http3Api.AdvanceClock(limits.MinRequestBodyDataRate.GracePeriod); + Http3Api.AdvanceTime(limits.MinRequestBodyDataRate.GracePeriod); _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); - Http3Api.AdvanceClock(TimeSpan.FromTicks(1)); + Http3Api.AdvanceTime(TimeSpan.FromTicks(1)); _mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.ReadDataRate), Times.Once); @@ -370,9 +374,7 @@ await Http3Api.WaitForConnectionErrorAsync( [Fact] public async Task ResponseDrain_SlowerThanMinimumDataRate_AbortsConnection() { - var now = _serviceContext.MockSystemClock.UtcNow; var limits = _serviceContext.ServerOptions.Limits; - var mockSystemClock = _serviceContext.MockSystemClock; // Use non-default value to ensure the min request and response rates aren't mixed up. limits.MinResponseDataRate = new MinDataRate(480, TimeSpan.FromSeconds(2.5)); @@ -392,13 +394,13 @@ public async Task ResponseDrain_SlowerThanMinimumDataRate_AbortsConnection() await requestStream.OnDisposingTask.DefaultTimeout(); - Http3Api.TriggerTick(now); + Http3Api.TriggerTick(); Assert.Null(requestStream.StreamContext._error); - Http3Api.TriggerTick(now + TimeSpan.FromTicks(1)); + Http3Api.TriggerTick(TimeSpan.FromTicks(1)); Assert.Null(requestStream.StreamContext._error); - Http3Api.TriggerTick(now + limits.MinResponseDataRate.GracePeriod + TimeSpan.FromTicks(1)); + Http3Api.TriggerTick(limits.MinResponseDataRate.GracePeriod); requestStream.StartStreamDisposeTcs.TrySetResult(); @@ -438,7 +440,7 @@ public async Task RunApp(HttpContext context) [Fact] public async Task DATA_Sent_TooSlowlyDueToSocketBackPressureOnSmallWrite_AbortsConnectionAfterGracePeriod() { - var mockSystemClock = _serviceContext.MockSystemClock; + var mockTimeProvider = _serviceContext.MockTimeProvider; var limits = _serviceContext.ServerOptions.Limits; // Use non-default value to ensure the min request and response rates aren't mixed up. @@ -447,7 +449,7 @@ public async Task DATA_Sent_TooSlowlyDueToSocketBackPressureOnSmallWrite_AbortsC // Disable response buffering so "socket" backpressure is observed immediately. limits.MaxResponseBufferSize = 0; - Http3Api._timeoutControl.Initialize(mockSystemClock.UtcNow.Ticks); + Http3Api._timeoutControl.Initialize(); var app = new EchoAppWithNotification(); var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(app.RunApp, _browserRequestHeaders, endStream: false); @@ -458,15 +460,15 @@ public async Task DATA_Sent_TooSlowlyDueToSocketBackPressureOnSmallWrite_AbortsC await app.WriteStartedTask.DefaultTimeout(); // Complete timing of the request body so we don't induce any unexpected request body rate timeouts. - Http3Api._timeoutControl.Tick(mockSystemClock.UtcNow); + Http3Api._timeoutControl.Tick(mockTimeProvider.GetTimestamp()); // Don't read data frame to induce "socket" backpressure. - Http3Api.AdvanceClock(TimeSpan.FromSeconds((requestStream.BytesReceived + _helloWorldBytes.Length) / limits.MinResponseDataRate.BytesPerSecond) + + Http3Api.AdvanceTime(TimeSpan.FromSeconds((requestStream.BytesReceived + _helloWorldBytes.Length) / limits.MinResponseDataRate.BytesPerSecond) + limits.MinResponseDataRate.GracePeriod + Heartbeat.Interval - TimeSpan.FromSeconds(.5)); _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); - Http3Api.AdvanceClock(TimeSpan.FromSeconds(1)); + Http3Api.AdvanceTime(TimeSpan.FromSeconds(1)); _mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.WriteDataRate), Times.Once); @@ -480,7 +482,7 @@ public async Task DATA_Sent_TooSlowlyDueToSocketBackPressureOnSmallWrite_AbortsC [Fact] public async Task DATA_Sent_TooSlowlyDueToSocketBackPressureOnLargeWrite_AbortsConnectionAfterRateTimeout() { - var mockSystemClock = _serviceContext.MockSystemClock; + var mockTimeProvider = _serviceContext.MockTimeProvider; var limits = _serviceContext.ServerOptions.Limits; // Use non-default value to ensure the min request and response rates aren't mixed up. @@ -489,7 +491,7 @@ public async Task DATA_Sent_TooSlowlyDueToSocketBackPressureOnLargeWrite_AbortsC // Disable response buffering so "socket" backpressure is observed immediately. limits.MaxResponseBufferSize = 0; - Http3Api._timeoutControl.Initialize(mockSystemClock.UtcNow.Ticks); + Http3Api._timeoutControl.Initialize(); var app = new EchoAppWithNotification(); var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(app.RunApp, _browserRequestHeaders, endStream: false); @@ -500,17 +502,17 @@ public async Task DATA_Sent_TooSlowlyDueToSocketBackPressureOnLargeWrite_AbortsC await app.WriteStartedTask.DefaultTimeout(); // Complete timing of the request body so we don't induce any unexpected request body rate timeouts. - Http3Api._timeoutControl.Tick(mockSystemClock.UtcNow); + Http3Api._timeoutControl.Tick(mockTimeProvider.GetTimestamp()); var timeToWriteMaxData = TimeSpan.FromSeconds((requestStream.BytesReceived + _maxData.Length) / limits.MinResponseDataRate.BytesPerSecond) + limits.MinResponseDataRate.GracePeriod + Heartbeat.Interval - TimeSpan.FromSeconds(.5); // Don't read data frame to induce "socket" backpressure. - Http3Api.AdvanceClock(timeToWriteMaxData); + Http3Api.AdvanceTime(timeToWriteMaxData); _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); - Http3Api.AdvanceClock(TimeSpan.FromSeconds(1)); + Http3Api.AdvanceTime(TimeSpan.FromSeconds(1)); _mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.WriteDataRate), Times.Once); @@ -523,13 +525,13 @@ public async Task DATA_Sent_TooSlowlyDueToSocketBackPressureOnLargeWrite_AbortsC [Fact] public async Task DATA_Received_TooSlowlyOnLargeRead_AbortsConnectionAfterRateTimeout() { - var mockSystemClock = _serviceContext.MockSystemClock; + var mockTimeProvider = _serviceContext.MockTimeProvider; var limits = _serviceContext.ServerOptions.Limits; // Use non-default value to ensure the min request and response rates aren't mixed up. limits.MinRequestBodyDataRate = new MinDataRate(480, TimeSpan.FromSeconds(2.5)); - Http3Api._timeoutControl.Initialize(mockSystemClock.UtcNow.Ticks); + Http3Api._timeoutControl.Initialize(); await Http3Api.InitializeConnectionAsync(_readRateApplication); @@ -549,11 +551,11 @@ public async Task DATA_Received_TooSlowlyOnLargeRead_AbortsConnectionAfterRateTi var timeToReadMaxData = TimeSpan.FromSeconds(_maxData.Length / limits.MinRequestBodyDataRate.BytesPerSecond) - TimeSpan.FromSeconds(.5); // Don't send any more data and advance just to and then past the rate timeout. - Http3Api.AdvanceClock(timeToReadMaxData); + Http3Api.AdvanceTime(timeToReadMaxData); _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); - Http3Api.AdvanceClock(TimeSpan.FromSeconds(1)); + Http3Api.AdvanceTime(TimeSpan.FromSeconds(1)); _mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.ReadDataRate), Times.Once); @@ -569,13 +571,13 @@ await Http3Api.WaitForConnectionErrorAsync( [Fact] public async Task DATA_Received_TooSlowlyOnMultipleStreams_AbortsConnectionAfterAdditiveRateTimeout() { - var mockSystemClock = _serviceContext.MockSystemClock; + var mockTimeProvider = _serviceContext.MockTimeProvider; var limits = _serviceContext.ServerOptions.Limits; // Use non-default value to ensure the min request and response rates aren't mixed up. limits.MinRequestBodyDataRate = new MinDataRate(480, TimeSpan.FromSeconds(2.5)); - Http3Api._timeoutControl.Initialize(mockSystemClock.UtcNow.Ticks); + Http3Api._timeoutControl.Initialize(); await Http3Api.InitializeConnectionAsync(_readRateApplication); @@ -604,11 +606,11 @@ public async Task DATA_Received_TooSlowlyOnMultipleStreams_AbortsConnectionAfter timeToReadMaxData -= TimeSpan.FromSeconds(.5); // Don't send any more data and advance just to and then past the rate timeout. - Http3Api.AdvanceClock(timeToReadMaxData); + Http3Api.AdvanceTime(timeToReadMaxData); _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); - Http3Api.AdvanceClock(TimeSpan.FromSeconds(1)); + Http3Api.AdvanceTime(TimeSpan.FromSeconds(1)); _mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.ReadDataRate), Times.Once); @@ -624,13 +626,13 @@ await Http3Api.WaitForConnectionErrorAsync( [Fact] public async Task DATA_Received_TooSlowlyOnSecondStream_AbortsConnectionAfterNonAdditiveRateTimeout() { - var mockSystemClock = _serviceContext.MockSystemClock; + var mockTimeProvider = _serviceContext.MockTimeProvider; var limits = _serviceContext.ServerOptions.Limits; // Use non-default value to ensure the min request and response rates aren't mixed up. limits.MinRequestBodyDataRate = new MinDataRate(480, TimeSpan.FromSeconds(2.5)); - Http3Api._timeoutControl.Initialize(mockSystemClock.UtcNow.Ticks); + Http3Api._timeoutControl.Initialize(); await Http3Api.InitializeConnectionAsync(_readRateApplication); @@ -660,11 +662,11 @@ public async Task DATA_Received_TooSlowlyOnSecondStream_AbortsConnectionAfterNon var timeToReadMaxData = TimeSpan.FromSeconds(_maxData.Length / limits.MinRequestBodyDataRate.BytesPerSecond) - TimeSpan.FromSeconds(.5); // Don't send any more data and advance just to and then past the rate timeout. - Http3Api.AdvanceClock(timeToReadMaxData); + Http3Api.AdvanceTime(timeToReadMaxData); _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); - Http3Api.AdvanceClock(TimeSpan.FromSeconds(1)); + Http3Api.AdvanceTime(TimeSpan.FromSeconds(1)); _mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.ReadDataRate), Times.Once); @@ -680,13 +682,13 @@ await Http3Api.WaitForConnectionErrorAsync( [Fact] public async Task DATA_Received_SlowlyWhenRateLimitDisabledPerRequest_DoesNotAbortConnection() { - var mockSystemClock = _serviceContext.MockSystemClock; + var mockTimeProvider = _serviceContext.MockTimeProvider; var limits = _serviceContext.ServerOptions.Limits; // Use non-default value to ensure the min request and response rates aren't mixed up. limits.MinRequestBodyDataRate = new MinDataRate(480, TimeSpan.FromSeconds(2.5)); - Http3Api._timeoutControl.Initialize(mockSystemClock.UtcNow.Ticks); + Http3Api._timeoutControl.Initialize(); await Http3Api.InitializeConnectionAsync(context => { @@ -709,11 +711,11 @@ await Http3Api.InitializeConnectionAsync(context => await requestStream.ExpectDataAsync(); // Don't send any more data and advance just to and then past the grace period. - Http3Api.AdvanceClock(limits.MinRequestBodyDataRate.GracePeriod); + Http3Api.AdvanceTime(limits.MinRequestBodyDataRate.GracePeriod); _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); - Http3Api.AdvanceClock(TimeSpan.FromTicks(1)); + Http3Api.AdvanceTime(TimeSpan.FromTicks(1)); _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/InMemory.FunctionalTests.csproj b/src/Servers/Kestrel/test/InMemory.FunctionalTests/InMemory.FunctionalTests.csproj index ecc1447c1f7c..576463f85fca 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/InMemory.FunctionalTests.csproj +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/InMemory.FunctionalTests.csproj @@ -11,6 +11,7 @@ + diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/KeepAliveTimeoutTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/KeepAliveTimeoutTests.cs index b994582239c3..57f7e8d8bce1 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/KeepAliveTimeoutTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/KeepAliveTimeoutTests.cs @@ -27,7 +27,6 @@ public class KeepAliveTimeoutTests : LoggedTest public async Task ConnectionClosedWhenKeepAliveTimeoutExpires() { var testContext = new TestServiceContext(LoggerFactory); - var heartbeatManager = new HeartbeatManager(testContext.ConnectionManager); await using (var server = CreateServer(testContext)) { @@ -43,8 +42,8 @@ await connection.Send( await ReceiveResponse(connection, testContext); // Min amount of time between requests that triggers a keep-alive timeout. - testContext.MockSystemClock.UtcNow += _keepAliveTimeout + Heartbeat.Interval + TimeSpan.FromTicks(1); - heartbeatManager.OnHeartbeat(testContext.SystemClock.UtcNow); + testContext.MockTimeProvider.Advance(_keepAliveTimeout + Heartbeat.Interval + TimeSpan.FromTicks(1)); + testContext.ConnectionManager.OnHeartbeat(); await connection.WaitForConnectionClose(); } @@ -55,7 +54,6 @@ await connection.Send( public async Task ConnectionKeptAliveBetweenRequests() { var testContext = new TestServiceContext(LoggerFactory); - var heartbeatManager = new HeartbeatManager(testContext.ConnectionManager); await using (var server = CreateServer(testContext)) { @@ -73,8 +71,8 @@ await connection.Send( await ReceiveResponse(connection, testContext); // Max amount of time between requests that doesn't trigger a keep-alive timeout. - testContext.MockSystemClock.UtcNow += _keepAliveTimeout + Heartbeat.Interval; - heartbeatManager.OnHeartbeat(testContext.SystemClock.UtcNow); + testContext.MockTimeProvider.Advance(_keepAliveTimeout + Heartbeat.Interval); + testContext.ConnectionManager.OnHeartbeat(); } } } @@ -84,7 +82,6 @@ await connection.Send( public async Task ConnectionNotTimedOutWhileRequestBeingSent() { var testContext = new TestServiceContext(LoggerFactory); - var heartbeatManager = new HeartbeatManager(testContext.ConnectionManager); await using (var server = CreateServer(testContext)) { @@ -108,8 +105,8 @@ await connection.Send( "a", ""); - testContext.MockSystemClock.UtcNow += _shortDelay; - heartbeatManager.OnHeartbeat(testContext.SystemClock.UtcNow); + testContext.MockTimeProvider.Advance(_shortDelay); + testContext.ConnectionManager.OnHeartbeat(); } await connection.Send( @@ -125,7 +122,6 @@ await connection.Send( private async Task ConnectionNotTimedOutWhileAppIsRunning() { var testContext = new TestServiceContext(LoggerFactory); - var heartbeatManager = new HeartbeatManager(testContext.ConnectionManager); var cts = new CancellationTokenSource(); await using (var server = CreateServer(testContext, longRunningCt: cts.Token)) @@ -144,8 +140,8 @@ await connection.Send( for (var totalDelay = TimeSpan.Zero; totalDelay < _longDelay; totalDelay += _shortDelay) { - testContext.MockSystemClock.UtcNow += _shortDelay; - heartbeatManager.OnHeartbeat(testContext.SystemClock.UtcNow); + testContext.MockTimeProvider.Advance(_shortDelay); + testContext.ConnectionManager.OnHeartbeat(); } cts.Cancel(); @@ -166,7 +162,6 @@ await connection.Send( private async Task ConnectionTimesOutWhenOpenedButNoRequestSent() { var testContext = new TestServiceContext(LoggerFactory); - var heartbeatManager = new HeartbeatManager(testContext.ConnectionManager); await using (var server = CreateServer(testContext)) { @@ -175,8 +170,8 @@ private async Task ConnectionTimesOutWhenOpenedButNoRequestSent() await connection.TransportConnection.WaitForReadTask; // Min amount of time between requests that triggers a keep-alive timeout. - testContext.MockSystemClock.UtcNow += _keepAliveTimeout + Heartbeat.Interval + TimeSpan.FromTicks(1); - heartbeatManager.OnHeartbeat(testContext.SystemClock.UtcNow); + testContext.MockTimeProvider.Advance(_keepAliveTimeout + Heartbeat.Interval + TimeSpan.FromTicks(1)); + testContext.ConnectionManager.OnHeartbeat(); await connection.WaitForConnectionClose(); } @@ -187,7 +182,6 @@ private async Task ConnectionTimesOutWhenOpenedButNoRequestSent() private async Task KeepAliveTimeoutDoesNotApplyToUpgradedConnections() { var testContext = new TestServiceContext(LoggerFactory); - var heartbeatManager = new HeartbeatManager(testContext.ConnectionManager); var cts = new CancellationTokenSource(); await using (var server = CreateServer(testContext, upgradeCt: cts.Token)) @@ -211,8 +205,8 @@ await connection.Receive( for (var totalDelay = TimeSpan.Zero; totalDelay < _longDelay; totalDelay += _shortDelay) { - testContext.MockSystemClock.UtcNow += _shortDelay; - heartbeatManager.OnHeartbeat(testContext.SystemClock.UtcNow); + testContext.MockTimeProvider.Advance(_shortDelay); + testContext.ConnectionManager.OnHeartbeat(); } cts.Cancel(); diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestBodyTimeoutTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestBodyTimeoutTests.cs index 0dab106577ae..8d791988fd16 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestBodyTimeoutTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestBodyTimeoutTests.cs @@ -22,7 +22,6 @@ public async Task RequestTimesOutWhenRequestBodyNotReceivedAtSpecifiedMinimumRat { var gracePeriod = TimeSpan.FromSeconds(5); var serviceContext = new TestServiceContext(LoggerFactory); - var heartbeatManager = new HeartbeatManager(serviceContext.ConnectionManager); var appRunningEvent = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -31,22 +30,22 @@ public async Task RequestTimesOutWhenRequestBodyNotReceivedAtSpecifiedMinimumRat context.Features.Get().MinDataRate = new MinDataRate(bytesPerSecond: 1, gracePeriod: gracePeriod); - // 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 + // The server must call Request.Body.ReadAsync() *before* the test sets timeProvider.UtcNow (which is triggered by the + // server calling appRunningEvent.SetResult(null)). If timeProvider.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); + // 1. [test] timeProvider.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, + // 6. [test] timeProvider.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 + // timeProvider.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: @@ -70,11 +69,11 @@ await connection.Send( await appRunningEvent.Task.DefaultTimeout(); - // Advance the clock gracePeriod + TimeSpan.FromSeconds(1) + // Advance the timeProvider gracePeriod + TimeSpan.FromSeconds(1) for (var i = 0; i < 6; i++) { - serviceContext.MockSystemClock.UtcNow += TimeSpan.FromSeconds(1); - heartbeatManager.OnHeartbeat(serviceContext.SystemClock.UtcNow); + serviceContext.MockTimeProvider.Advance(TimeSpan.FromSeconds(1)); + serviceContext.ConnectionManager.OnHeartbeat(); } await connection.Receive( @@ -98,9 +97,9 @@ public async Task RequestTimesOutWhenNotDrainedWithinDrainTimeoutPeriod() serviceContext.InitializeHeartbeat(); // Ensure there's still a constant date header value. - var clock = new MockSystemClock(); - var date = new DateHeaderValueManager(); - date.OnHeartbeat(clock.UtcNow); + var timeProvider = new MockTimeProvider(); + var date = new DateHeaderValueManager(timeProvider); + date.OnHeartbeat(); serviceContext.DateHeaderValueManager = date; var appRunningEvent = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -143,7 +142,6 @@ public async Task ConnectionClosedEvenIfAppSwallowsException() { var gracePeriod = TimeSpan.FromSeconds(5); var serviceContext = new TestServiceContext(LoggerFactory); - var heartbeatManager = new HeartbeatManager(serviceContext.ConnectionManager); var appRunningTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var exceptionSwallowedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -190,8 +188,8 @@ await connection.Send( // Advance the clock gracePeriod + TimeSpan.FromSeconds(1) for (var i = 0; i < 6; i++) { - serviceContext.MockSystemClock.UtcNow += TimeSpan.FromSeconds(1); - heartbeatManager.OnHeartbeat(serviceContext.SystemClock.UtcNow); + serviceContext.MockTimeProvider.Advance(TimeSpan.FromSeconds(1)); + serviceContext.ConnectionManager.OnHeartbeat(); } await exceptionSwallowedTcs.Task.DefaultTimeout(); diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestHeadersTimeoutTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestHeadersTimeoutTests.cs index 4adb3e6e1f60..a379483bcd6d 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestHeadersTimeoutTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestHeadersTimeoutTests.cs @@ -5,6 +5,7 @@ using System.IO.Pipelines; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport; using Microsoft.AspNetCore.Testing; @@ -25,7 +26,6 @@ public class RequestHeadersTimeoutTests : LoggedTest public async Task ConnectionAbortedWhenRequestHeadersNotReceivedInTime(string headers) { var testContext = new TestServiceContext(LoggerFactory); - var heartbeatManager = new HeartbeatManager(testContext.ConnectionManager); await using (var server = CreateServer(testContext)) { @@ -38,8 +38,8 @@ await connection.Send( headers); // Min amount of time between requests that triggers a request headers timeout. - testContext.MockSystemClock.UtcNow += RequestHeadersTimeout + Heartbeat.Interval + TimeSpan.FromTicks(1); - heartbeatManager.OnHeartbeat(testContext.SystemClock.UtcNow); + testContext.MockTimeProvider.Advance(RequestHeadersTimeout + Heartbeat.Interval + TimeSpan.FromTicks(1)); + testContext.ConnectionManager.OnHeartbeat(); await ReceiveTimeoutResponse(connection, testContext); } @@ -50,7 +50,6 @@ await connection.Send( public async Task RequestHeadersTimeoutCanceledAfterHeadersReceived() { var testContext = new TestServiceContext(LoggerFactory); - var heartbeatManager = new HeartbeatManager(testContext.ConnectionManager); await using (var server = CreateServer(testContext)) { @@ -66,8 +65,8 @@ await connection.Send( ""); // Min amount of time between requests that triggers a request headers timeout. - testContext.MockSystemClock.UtcNow += RequestHeadersTimeout + Heartbeat.Interval + TimeSpan.FromTicks(1); - heartbeatManager.OnHeartbeat(testContext.SystemClock.UtcNow); + testContext.MockTimeProvider.Advance(RequestHeadersTimeout + Heartbeat.Interval + TimeSpan.FromTicks(1)); + testContext.ConnectionManager.OnHeartbeat(); await connection.Send( "a"); @@ -83,7 +82,6 @@ await connection.Send( public async Task ConnectionAbortedWhenRequestLineNotReceivedInTime(string requestLine) { var testContext = new TestServiceContext(LoggerFactory); - var heartbeatManager = new HeartbeatManager(testContext.ConnectionManager); await using (var server = CreateServer(testContext)) { @@ -94,8 +92,8 @@ public async Task ConnectionAbortedWhenRequestLineNotReceivedInTime(string reque await connection.Send(requestLine); // Min amount of time between requests that triggers a request headers timeout. - testContext.MockSystemClock.UtcNow += RequestHeadersTimeout + Heartbeat.Interval + TimeSpan.FromTicks(1); - heartbeatManager.OnHeartbeat(testContext.SystemClock.UtcNow); + testContext.MockTimeProvider.Advance(RequestHeadersTimeout + Heartbeat.Interval + TimeSpan.FromTicks(1)); + testContext.ConnectionManager.OnHeartbeat(); await ReceiveTimeoutResponse(connection, testContext); } @@ -106,7 +104,6 @@ public async Task ConnectionAbortedWhenRequestLineNotReceivedInTime(string reque public async Task TimeoutNotResetOnEachRequestLineCharacterReceived() { var testContext = new TestServiceContext(LoggerFactory); - var heartbeatManager = new HeartbeatManager(testContext.ConnectionManager); // Disable response rate, so we can finish the send loop without timing out the response. testContext.ServerOptions.Limits.MinResponseDataRate = null; @@ -121,8 +118,8 @@ public async Task TimeoutNotResetOnEachRequestLineCharacterReceived() { await connection.Send(ch.ToString()); - testContext.MockSystemClock.UtcNow += ShortDelay; - heartbeatManager.OnHeartbeat(testContext.SystemClock.UtcNow); + testContext.MockTimeProvider.Advance(ShortDelay); + testContext.ConnectionManager.OnHeartbeat(); } await ReceiveTimeoutResponse(connection, testContext); diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestTests.cs index 71b065eede03..51f776e4dbd9 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestTests.cs @@ -1628,7 +1628,6 @@ public async Task DoesNotEnforceRequestBodyMinimumDataRateOnUpgradedRequest() var appEvent = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var delayEvent = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var serviceContext = new TestServiceContext(LoggerFactory); - var heartbeatManager = new HeartbeatManager(serviceContext.ConnectionManager); await using (var server = new TestServer(async context => { @@ -1664,8 +1663,8 @@ await connection.Send( await appEvent.Task.DefaultTimeout(); - serviceContext.MockSystemClock.UtcNow += TimeSpan.FromSeconds(5); - heartbeatManager.OnHeartbeat(serviceContext.SystemClock.UtcNow); + serviceContext.MockTimeProvider.Advance(TimeSpan.FromSeconds(5)); + serviceContext.ConnectionManager.OnHeartbeat(); delayEvent.SetResult(); diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/ResponseDrainingTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/ResponseDrainingTests.cs index f156b1f41046..8a0904cba657 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/ResponseDrainingTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/ResponseDrainingTests.cs @@ -1,15 +1,12 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; using System.Net; -using System.Threading.Tasks; using Microsoft.AspNetCore.Server.Kestrel.Core.Features; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Logging; -using Xunit; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests; @@ -26,7 +23,6 @@ public class ResponseDrainingTests : TestApplicationErrorLoggerLoggedTest public async Task ConnectionClosedWhenResponseNotDrainedAtMinimumDataRate(ListenOptions listenOptions) { var testContext = new TestServiceContext(LoggerFactory); - var heartbeatManager = new HeartbeatManager(testContext.ConnectionManager); var minRate = new MinDataRate(16384, TimeSpan.FromSeconds(2)); await using (var server = new TestServer(context => @@ -62,17 +58,17 @@ await connection.Send( // Advance the clock to the grace period for (var i = 0; i < 2; i++) { - testContext.MockSystemClock.UtcNow += TimeSpan.FromSeconds(1); - heartbeatManager.OnHeartbeat(testContext.SystemClock.UtcNow); + testContext.MockTimeProvider.Advance(TimeSpan.FromSeconds(1)); + testContext.ConnectionManager.OnHeartbeat(); } - testContext.MockSystemClock.UtcNow += Heartbeat.Interval - TimeSpan.FromSeconds(.5); - heartbeatManager.OnHeartbeat(testContext.SystemClock.UtcNow); + testContext.MockTimeProvider.Advance(Heartbeat.Interval - TimeSpan.FromSeconds(.5)); + testContext.ConnectionManager.OnHeartbeat(); Assert.Null(transportConnection.AbortReason); - testContext.MockSystemClock.UtcNow += TimeSpan.FromSeconds(1); - heartbeatManager.OnHeartbeat(testContext.SystemClock.UtcNow); + testContext.MockTimeProvider.Advance(TimeSpan.FromSeconds(1)); + testContext.ConnectionManager.OnHeartbeat(); Assert.NotNull(transportConnection.AbortReason); Assert.Equal(CoreStrings.ConnectionTimedBecauseResponseMininumDataRateNotSatisfied, transportConnection.AbortReason.Message); diff --git a/src/Servers/Kestrel/test/Sockets.BindTests/Sockets.BindTests.csproj b/src/Servers/Kestrel/test/Sockets.BindTests/Sockets.BindTests.csproj index 5a39a90d9b4e..5d7992a557a7 100644 --- a/src/Servers/Kestrel/test/Sockets.BindTests/Sockets.BindTests.csproj +++ b/src/Servers/Kestrel/test/Sockets.BindTests/Sockets.BindTests.csproj @@ -12,7 +12,7 @@ - + diff --git a/src/Servers/Kestrel/test/Sockets.FunctionalTests/Sockets.FunctionalTests.csproj b/src/Servers/Kestrel/test/Sockets.FunctionalTests/Sockets.FunctionalTests.csproj index 3e8bcdf17ec0..2b8e9b81899c 100644 --- a/src/Servers/Kestrel/test/Sockets.FunctionalTests/Sockets.FunctionalTests.csproj +++ b/src/Servers/Kestrel/test/Sockets.FunctionalTests/Sockets.FunctionalTests.csproj @@ -13,7 +13,7 @@ - + diff --git a/src/Shared/test/Certificates/Certificates.cs b/src/Shared/test/Certificates/Certificates.cs index d0c2a0c043b6..7b4b5eeeba97 100644 --- a/src/Shared/test/Certificates/Certificates.cs +++ b/src/Shared/test/Certificates/Certificates.cs @@ -3,6 +3,7 @@ using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; +using Microsoft.AspNetCore.Testing; namespace Microsoft.AspNetCore.Authentication.Certificate; @@ -13,7 +14,7 @@ public static class Certificates static Certificates() { - DateTimeOffset now = DateTimeOffset.UtcNow; + var now = TimeProvider.System.GetUtcNow(); SelfSignedPrimaryRoot = MakeCert( "CN=Valid Self Signed Client EKU,OU=dev,DC=idunno-dev,DC=org", diff --git a/src/Shared/test/MockTimeProvider.cs b/src/Shared/test/MockTimeProvider.cs new file mode 100644 index 000000000000..b7527ad7590b --- /dev/null +++ b/src/Shared/test/MockTimeProvider.cs @@ -0,0 +1,90 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; + +namespace Microsoft.AspNetCore.Testing; + +public class MockTimeProvider : TimeProvider +{ + private readonly long _timestampFrequency = + // 10_000_000; // Windows + // 100_000_000; // 8 zeros, in between + // 1_000_000_000; // Linux + Stopwatch.Frequency; + private long _utcTicks; + private long _timestamp; + + public MockTimeProvider() + { + // Use a random DateTimeOffset to ensure tests that incorrectly use the current DateTimeOffset fail always instead of only rarely. + var tenYears = TimeSpan.FromDays(365 * 10).Ticks; + _utcTicks = DateTimeOffset.UtcNow.Ticks + Random.Shared.NextInt64(-tenYears, tenYears); + // Timestamps often measure system uptime. + _timestamp = Random.Shared.NextInt64(0, System.GetTimestamp() * 100); + } + + public MockTimeProvider(DateTimeOffset now) + { + _utcTicks = now.Ticks; + // Timestamps often measure system uptime. + _timestamp = Random.Shared.NextInt64(0, System.GetTimestamp() * 100); + } + + public override DateTimeOffset GetUtcNow() + { + UtcNowCalled++; + return new DateTimeOffset(Interlocked.Read(ref _utcTicks), TimeSpan.Zero); + } + + public override long GetTimestamp() => Interlocked.Read(ref _timestamp); + + public override long TimestampFrequency => _timestampFrequency; + + public int UtcNowCalled { get; private set; } + + public void Advance(TimeSpan timeSpan) + { + if (timeSpan < TimeSpan.Zero) + { + throw new ArgumentOutOfRangeException(nameof(timeSpan), timeSpan, "Cannot go back in time."); + } + Interlocked.Add(ref _utcTicks, timeSpan.Ticks); + checked + { + Interlocked.Add(ref _timestamp, (long)(timeSpan.Ticks * ((double)_timestampFrequency / TimeSpan.TicksPerSecond))); + } + } + + public void AdvanceTo(DateTimeOffset newUtcNow) + { + var nowTicks = newUtcNow.UtcTicks; + var priorTicks = Interlocked.Exchange(ref _utcTicks, nowTicks); + if (priorTicks > nowTicks) + { + var priorTime = new DateTimeOffset(priorTicks, TimeSpan.Zero); + throw new ArgumentOutOfRangeException(nameof(newUtcNow), newUtcNow, $"Cannot go back in time. The prior time was {priorTime}"); + } + // Advance Timestamp by the same amount. + var timestampOffset = (long)((nowTicks - priorTicks) * ((double)_timestampFrequency / TimeSpan.TicksPerSecond)); + Interlocked.Add(ref _timestamp, timestampOffset); + } + + public void AdvanceTo(long timestamp) + { + var priorTimestamp = Interlocked.Exchange(ref _timestamp, timestamp); + if (priorTimestamp > timestamp) + { + throw new ArgumentOutOfRangeException(nameof(timestamp), timestamp, $"Cannot go back in time. The prior timestamp was {priorTimestamp}"); + } + // Advance UtcNow by the same amount. + checked + { + var utcOffset = (long)((timestamp - priorTimestamp) * ((double)TimeSpan.TicksPerSecond / _timestampFrequency)); + Interlocked.Add(ref _utcTicks, utcOffset); + } + } + + public override ITimer CreateTimer(TimerCallback callback, object state, TimeSpan dueTime, TimeSpan period) + => throw new NotImplementedException(); +} diff --git a/src/SignalR/server/SignalR/test/HubConnectionHandlerTestUtils/TestTimeProvider.cs b/src/SignalR/server/SignalR/test/HubConnectionHandlerTestUtils/TestTimeProvider.cs deleted file mode 100644 index bbde8664c233..000000000000 --- a/src/SignalR/server/SignalR/test/HubConnectionHandlerTestUtils/TestTimeProvider.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.AspNetCore.SignalR.Tests; - -public class TestTimeProvider : TimeProvider -{ - private long _nowTicks; - - public TestTimeProvider() - { - // Use a random DateTimeOffset to ensure tests that incorrectly use the current DateTimeOffset fail always instead of only rarely. - // Pick a date between the min DateTimeOffset and a day before the max DateTimeOffset so there's room to advance the clock. - _nowTicks = NextLong(0, long.MaxValue - (long)TimeSpan.FromDays(1).TotalSeconds) * TimestampFrequency; - } - - public override long GetTimestamp() => _nowTicks; - - public void Advance(TimeSpan offset) - { - Interlocked.Add(ref _nowTicks, (long)(offset.TotalSeconds * TimestampFrequency)); - } - - private static long NextLong(long minValue, long maxValue) - { - return (long)(Random.Shared.NextDouble() * (maxValue - minValue) + minValue); - } -} diff --git a/src/SignalR/server/SignalR/test/HubConnectionHandlerTests.cs b/src/SignalR/server/SignalR/test/HubConnectionHandlerTests.cs index 8fe8e44d7520..ed13833e35e3 100644 --- a/src/SignalR/server/SignalR/test/HubConnectionHandlerTests.cs +++ b/src/SignalR/server/SignalR/test/HubConnectionHandlerTests.cs @@ -2735,7 +2735,7 @@ public async Task WritesPingMessageIfNothingWrittenWhenKeepAliveIntervalElapses( using (StartVerifiableLog()) { var interval = TimeSpan.FromMilliseconds(100); - var timeProvider = new TestTimeProvider(); + var timeProvider = new MockTimeProvider(); var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(services => services.Configure(options => options.KeepAliveInterval = interval), LoggerFactory); @@ -2797,7 +2797,7 @@ public async Task ConnectionNotTimedOutIfClientNeverPings() using (StartVerifiableLog()) { var timeout = TimeSpan.FromMilliseconds(100); - var timeProvider = new TestTimeProvider(); + var timeProvider = new MockTimeProvider(); var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(services => services.Configure(options => options.ClientTimeoutInterval = timeout), LoggerFactory); @@ -2833,7 +2833,7 @@ public async Task ConnectionTimesOutIfInitialPingAndThenNoMessages() using (StartVerifiableLog()) { var timeout = TimeSpan.FromMilliseconds(100); - var timeProvider = new TestTimeProvider(); + var timeProvider = new MockTimeProvider(); var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(services => services.Configure(options => options.ClientTimeoutInterval = timeout), LoggerFactory); @@ -2860,7 +2860,7 @@ public async Task ReceivingMessagesPreventsConnectionTimeoutFromOccuring() using (StartVerifiableLog()) { var timeout = TimeSpan.FromMilliseconds(300); - var timeProvider = new TestTimeProvider(); + var timeProvider = new MockTimeProvider(); var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(services => services.Configure(options => options.ClientTimeoutInterval = timeout), LoggerFactory); diff --git a/src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests.csproj b/src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests.csproj index 24f414747f69..e1654b2368a8 100644 --- a/src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests.csproj +++ b/src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests.csproj @@ -5,7 +5,8 @@ - + +