diff --git a/src/SignalR/common/Shared/ISystemClock.cs b/src/SignalR/common/Shared/ISystemClock.cs deleted file mode 100644 index e3e1ed385866..000000000000 --- a/src/SignalR/common/Shared/ISystemClock.cs +++ /dev/null @@ -1,12 +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.Internal; - -internal interface ISystemClock -{ - /// - /// Retrieves ticks for the current system up time. - /// - long CurrentTicks { get; } -} diff --git a/src/SignalR/common/Shared/SystemClock.cs b/src/SignalR/common/Shared/SystemClock.cs deleted file mode 100644 index 124079fe93b2..000000000000 --- a/src/SignalR/common/Shared/SystemClock.cs +++ /dev/null @@ -1,13 +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.Internal; - -/// -/// Provides access to the normal system clock. -/// -internal sealed class SystemClock : ISystemClock -{ - /// - public long CurrentTicks => Environment.TickCount64; -} diff --git a/src/SignalR/server/Core/src/HubConnectionContext.cs b/src/SignalR/server/Core/src/HubConnectionContext.cs index 185fb1b4f52e..a2a9f24429ef 100644 --- a/src/SignalR/server/Core/src/HubConnectionContext.cs +++ b/src/SignalR/server/Core/src/HubConnectionContext.cs @@ -10,7 +10,6 @@ using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Http.Features; -using Microsoft.AspNetCore.Internal; using Microsoft.AspNetCore.SignalR.Internal; using Microsoft.AspNetCore.SignalR.Protocol; using Microsoft.Extensions.Logging; @@ -29,11 +28,11 @@ public partial class HubConnectionContext private readonly ILogger _logger; private readonly CancellationTokenSource _connectionAbortedTokenSource = new CancellationTokenSource(); private readonly TaskCompletionSource _abortCompletedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - private readonly long _keepAliveInterval; - private readonly long _clientTimeoutInterval; + private readonly TimeSpan _keepAliveInterval; + private readonly TimeSpan _clientTimeoutInterval; private readonly SemaphoreSlim _writeLock = new SemaphoreSlim(1); private readonly object _receiveMessageTimeoutLock = new object(); - private readonly ISystemClock _systemClock; + private readonly TimeProvider _timeProvider; private readonly CancellationTokenRegistration _closedRegistration; private readonly CancellationTokenRegistration? _closedRequestedRegistration; @@ -46,7 +45,7 @@ public partial class HubConnectionContext private readonly int _streamBufferCapacity; private readonly long? _maxMessageSize; private bool _receivedMessageTimeoutEnabled; - private long _receivedMessageElapsedTicks; + private TimeSpan _receivedMessageElapsed; private long _receivedMessageTick; private ClaimsPrincipal? _user; @@ -58,8 +57,9 @@ public partial class HubConnectionContext /// The options to configure the HubConnectionContext. public HubConnectionContext(ConnectionContext connectionContext, HubConnectionContextOptions contextOptions, ILoggerFactory loggerFactory) { - _keepAliveInterval = (long)contextOptions.KeepAliveInterval.TotalMilliseconds; - _clientTimeoutInterval = (long)contextOptions.ClientTimeoutInterval.TotalMilliseconds; + _timeProvider = contextOptions.TimeProvider ?? TimeProvider.System; + _keepAliveInterval = contextOptions.KeepAliveInterval; + _clientTimeoutInterval = contextOptions.ClientTimeoutInterval; _streamBufferCapacity = contextOptions.StreamBufferCapacity; _maxMessageSize = contextOptions.MaximumReceiveMessageSize; @@ -76,8 +76,7 @@ public HubConnectionContext(ConnectionContext connectionContext, HubConnectionCo HubCallerContext = new DefaultHubCallerContext(this); - _systemClock = contextOptions.SystemClock ?? new SystemClock(); - _lastSendTick = _systemClock.CurrentTicks; + _lastSendTick = _timeProvider.GetTimestamp(); var maxInvokeLimit = contextOptions.MaximumParallelInvocations; ActiveInvocationLimit = new ChannelBasedSemaphore(maxInvokeLimit); @@ -614,7 +613,8 @@ private async Task AbortAsyncSlow() private void KeepAliveTick() { - var currentTime = _systemClock.CurrentTicks; + var currentTime = _timeProvider.GetTimestamp(); + var elapsed = _timeProvider.GetElapsedTime(Volatile.Read(ref _lastSendTick), currentTime); // Implements the keep-alive tick behavior // Each tick, we check if the time since the last send is larger than the keep alive duration (in ticks). @@ -622,7 +622,7 @@ private void KeepAliveTick() // true "ping rate" of the server could be (_hubOptions.KeepAliveInterval + HubEndPoint.KeepAliveTimerInterval), // because if the interval elapses right after the last tick of this timer, it won't be detected until the next tick. - if (currentTime - Volatile.Read(ref _lastSendTick) > _keepAliveInterval) + if (elapsed > _keepAliveInterval) { // Haven't sent a message for the entire keep-alive duration, so send a ping. // If the transport channel is full, this will fail, but that's OK because @@ -657,11 +657,11 @@ private void CheckClientTimeout() { if (_receivedMessageTimeoutEnabled) { - _receivedMessageElapsedTicks = _systemClock.CurrentTicks - _receivedMessageTick; + _receivedMessageElapsed = _timeProvider.GetElapsedTime(_receivedMessageTick); - if (_receivedMessageElapsedTicks >= _clientTimeoutInterval) + if (_receivedMessageElapsed >= _clientTimeoutInterval) { - Log.ClientTimeout(_logger, TimeSpan.FromMilliseconds(_clientTimeoutInterval)); + Log.ClientTimeout(_logger, _clientTimeoutInterval); AbortAllowReconnect(); } } @@ -707,7 +707,7 @@ internal void BeginClientTimeout() lock (_receiveMessageTimeoutLock) { _receivedMessageTimeoutEnabled = true; - _receivedMessageTick = _systemClock.CurrentTicks; + _receivedMessageTick = _timeProvider.GetTimestamp(); } } @@ -717,7 +717,7 @@ internal void StopClientTimeout() { // we received a message so stop the timer and reset it // it will resume after the message has been processed - _receivedMessageElapsedTicks = 0; + _receivedMessageElapsed = TimeSpan.Zero; _receivedMessageTick = 0; _receivedMessageTimeoutEnabled = false; } diff --git a/src/SignalR/server/Core/src/HubConnectionContextOptions.cs b/src/SignalR/server/Core/src/HubConnectionContextOptions.cs index fb965fcbe9ba..af5dd734c266 100644 --- a/src/SignalR/server/Core/src/HubConnectionContextOptions.cs +++ b/src/SignalR/server/Core/src/HubConnectionContextOptions.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.AspNetCore.Internal; - namespace Microsoft.AspNetCore.SignalR; /// @@ -30,7 +28,7 @@ public class HubConnectionContextOptions /// public long? MaximumReceiveMessageSize { get; set; } - internal ISystemClock SystemClock { get; set; } = default!; + internal TimeProvider TimeProvider { get; set; } = default!; /// /// Gets or sets the maximum parallel hub method invocations. diff --git a/src/SignalR/server/Core/src/HubConnectionHandler.cs b/src/SignalR/server/Core/src/HubConnectionHandler.cs index 3269d6f7afcd..ab3d0f5bbd7b 100644 --- a/src/SignalR/server/Core/src/HubConnectionHandler.cs +++ b/src/SignalR/server/Core/src/HubConnectionHandler.cs @@ -3,7 +3,6 @@ using System.Linq; using Microsoft.AspNetCore.Connections; -using Microsoft.AspNetCore.Internal; using Microsoft.AspNetCore.SignalR.Internal; using Microsoft.AspNetCore.SignalR.Protocol; using Microsoft.Extensions.DependencyInjection; @@ -31,7 +30,7 @@ public class HubConnectionHandler : ConnectionHandler where THub : Hub private readonly int _maxParallelInvokes; // Internal for testing - internal ISystemClock SystemClock { get; set; } = new SystemClock(); + internal TimeProvider TimeProvider { get; set; } = TimeProvider.System; /// /// Initializes a new instance of the class. @@ -120,7 +119,7 @@ public override async Task OnConnectedAsync(ConnectionContext connection) ClientTimeoutInterval = _hubOptions.ClientTimeoutInterval ?? _globalHubOptions.ClientTimeoutInterval ?? HubOptionsSetup.DefaultClientTimeoutInterval, StreamBufferCapacity = _hubOptions.StreamBufferCapacity ?? _globalHubOptions.StreamBufferCapacity ?? HubOptionsSetup.DefaultStreamBufferCapacity, MaximumReceiveMessageSize = _maximumMessageSize, - SystemClock = SystemClock, + TimeProvider = TimeProvider, MaximumParallelInvocations = _maxParallelInvokes, }; diff --git a/src/SignalR/server/Core/src/Microsoft.AspNetCore.SignalR.Core.csproj b/src/SignalR/server/Core/src/Microsoft.AspNetCore.SignalR.Core.csproj index 741fa83d85ed..db4c7d7e81df 100644 --- a/src/SignalR/server/Core/src/Microsoft.AspNetCore.SignalR.Core.csproj +++ b/src/SignalR/server/Core/src/Microsoft.AspNetCore.SignalR.Core.csproj @@ -15,8 +15,6 @@ - - diff --git a/src/SignalR/server/SignalR/test/HubConnectionHandlerTestUtils/MockSystemClock.cs b/src/SignalR/server/SignalR/test/HubConnectionHandlerTestUtils/TestTimeProvider.cs similarity index 64% rename from src/SignalR/server/SignalR/test/HubConnectionHandlerTestUtils/MockSystemClock.cs rename to src/SignalR/server/SignalR/test/HubConnectionHandlerTestUtils/TestTimeProvider.cs index 3268451c0cfd..bbde8664c233 100644 --- a/src/SignalR/server/SignalR/test/HubConnectionHandlerTestUtils/MockSystemClock.cs +++ b/src/SignalR/server/SignalR/test/HubConnectionHandlerTestUtils/TestTimeProvider.cs @@ -1,31 +1,27 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.AspNetCore.Internal; - namespace Microsoft.AspNetCore.SignalR.Tests; -public class MockSystemClock : ISystemClock +public class TestTimeProvider : TimeProvider { private long _nowTicks; - public MockSystemClock() + 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).TotalMilliseconds); + _nowTicks = NextLong(0, long.MaxValue - (long)TimeSpan.FromDays(1).TotalSeconds) * TimestampFrequency; } - public long CurrentTicks + public override long GetTimestamp() => _nowTicks; + + public void Advance(TimeSpan offset) { - get => _nowTicks; - set - { - Interlocked.Exchange(ref _nowTicks, value); - } + Interlocked.Add(ref _nowTicks, (long)(offset.TotalSeconds * TimestampFrequency)); } - private long NextLong(long minValue, long maxValue) + 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 eefd6c4c60bd..943b6efa6a21 100644 --- a/src/SignalR/server/SignalR/test/HubConnectionHandlerTests.cs +++ b/src/SignalR/server/SignalR/test/HubConnectionHandlerTests.cs @@ -2735,13 +2735,13 @@ public async Task WritesPingMessageIfNothingWrittenWhenKeepAliveIntervalElapses( { using (StartVerifiableLog()) { - var intervalInMS = 100; - var clock = new MockSystemClock(); + var interval = TimeSpan.FromMilliseconds(100); + var timeProvider = new TestTimeProvider(); var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(services => services.Configure(options => - options.KeepAliveInterval = TimeSpan.FromMilliseconds(intervalInMS)), LoggerFactory); + options.KeepAliveInterval = interval), LoggerFactory); var connectionHandler = serviceProvider.GetService>(); - connectionHandler.SystemClock = clock; + connectionHandler.TimeProvider = timeProvider; using (var client = new TestClient(new NewtonsoftJsonHubProtocol())) { @@ -2752,7 +2752,7 @@ public async Task WritesPingMessageIfNothingWrittenWhenKeepAliveIntervalElapses( var heartbeatCount = 5; for (var i = 0; i < heartbeatCount; i++) { - clock.CurrentTicks = clock.CurrentTicks + intervalInMS + 1; + timeProvider.Advance(interval + TimeSpan.FromMilliseconds(1)); client.TickHeartbeat(); } @@ -2797,13 +2797,13 @@ public async Task ConnectionNotTimedOutIfClientNeverPings() { using (StartVerifiableLog()) { - var timeoutInMS = 100; - var clock = new MockSystemClock(); + var timeout = TimeSpan.FromMilliseconds(100); + var timeProvider = new TestTimeProvider(); var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(services => services.Configure(options => - options.ClientTimeoutInterval = TimeSpan.FromMilliseconds(timeoutInMS)), LoggerFactory); + options.ClientTimeoutInterval = timeout), LoggerFactory); var connectionHandler = serviceProvider.GetService>(); - connectionHandler.SystemClock = clock; + connectionHandler.TimeProvider = timeProvider; using (var client = new TestClient(new NewtonsoftJsonHubProtocol())) { @@ -2814,7 +2814,7 @@ public async Task ConnectionNotTimedOutIfClientNeverPings() // We go over the 100 ms timeout interval multiple times for (var i = 0; i < 3; i++) { - clock.CurrentTicks = clock.CurrentTicks + timeoutInMS + 1; + timeProvider.Advance(timeout + TimeSpan.FromMilliseconds(1)); client.TickHeartbeat(); } @@ -2833,13 +2833,13 @@ public async Task ConnectionTimesOutIfInitialPingAndThenNoMessages() { using (StartVerifiableLog()) { - var timeoutInMS = 100; - var clock = new MockSystemClock(); + var timeout = TimeSpan.FromMilliseconds(100); + var timeProvider = new TestTimeProvider(); var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(services => services.Configure(options => - options.ClientTimeoutInterval = TimeSpan.FromMilliseconds(timeoutInMS)), LoggerFactory); + options.ClientTimeoutInterval = timeout), LoggerFactory); var connectionHandler = serviceProvider.GetService>(); - connectionHandler.SystemClock = clock; + connectionHandler.TimeProvider = timeProvider; using (var client = new TestClient(new NewtonsoftJsonHubProtocol())) { @@ -2847,7 +2847,7 @@ public async Task ConnectionTimesOutIfInitialPingAndThenNoMessages() await client.Connected.DefaultTimeout(); await client.SendHubMessageAsync(PingMessage.Instance); - clock.CurrentTicks = clock.CurrentTicks + timeoutInMS + 1; + timeProvider.Advance(timeout + TimeSpan.FromMilliseconds(1)); client.TickHeartbeat(); await connectionHandlerTask.DefaultTimeout(); @@ -2860,13 +2860,13 @@ public async Task ReceivingMessagesPreventsConnectionTimeoutFromOccuring() { using (StartVerifiableLog()) { - var timeoutInMS = 300; - var clock = new MockSystemClock(); + var timeout = TimeSpan.FromMilliseconds(300); + var timeProvider = new TestTimeProvider(); var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(services => services.Configure(options => - options.ClientTimeoutInterval = TimeSpan.FromMilliseconds(timeoutInMS)), LoggerFactory); + options.ClientTimeoutInterval = timeout), LoggerFactory); var connectionHandler = serviceProvider.GetService>(); - connectionHandler.SystemClock = clock; + connectionHandler.TimeProvider = timeProvider; using (var client = new TestClient(new NewtonsoftJsonHubProtocol())) { @@ -2876,7 +2876,7 @@ public async Task ReceivingMessagesPreventsConnectionTimeoutFromOccuring() for (int i = 0; i < 10; i++) { - clock.CurrentTicks = clock.CurrentTicks + timeoutInMS - 1; + timeProvider.Advance(timeout - TimeSpan.FromMilliseconds(1)); client.TickHeartbeat(); await client.SendHubMessageAsync(PingMessage.Instance); }