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