Skip to content

Commit 69e3a5d

Browse files
authored
Replace ISystemClock in SignalR (#47895)
1 parent cc54ac3 commit 69e3a5d

File tree

8 files changed

+47
-81
lines changed

8 files changed

+47
-81
lines changed

src/SignalR/common/Shared/ISystemClock.cs

Lines changed: 0 additions & 12 deletions
This file was deleted.

src/SignalR/common/Shared/SystemClock.cs

Lines changed: 0 additions & 13 deletions
This file was deleted.

src/SignalR/server/Core/src/HubConnectionContext.cs

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
using Microsoft.AspNetCore.Connections;
1111
using Microsoft.AspNetCore.Connections.Features;
1212
using Microsoft.AspNetCore.Http.Features;
13-
using Microsoft.AspNetCore.Internal;
1413
using Microsoft.AspNetCore.SignalR.Internal;
1514
using Microsoft.AspNetCore.SignalR.Protocol;
1615
using Microsoft.Extensions.Logging;
@@ -29,11 +28,11 @@ public partial class HubConnectionContext
2928
private readonly ILogger _logger;
3029
private readonly CancellationTokenSource _connectionAbortedTokenSource = new CancellationTokenSource();
3130
private readonly TaskCompletionSource _abortCompletedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
32-
private readonly long _keepAliveInterval;
33-
private readonly long _clientTimeoutInterval;
31+
private readonly TimeSpan _keepAliveInterval;
32+
private readonly TimeSpan _clientTimeoutInterval;
3433
private readonly SemaphoreSlim _writeLock = new SemaphoreSlim(1);
3534
private readonly object _receiveMessageTimeoutLock = new object();
36-
private readonly ISystemClock _systemClock;
35+
private readonly TimeProvider _timeProvider;
3736
private readonly CancellationTokenRegistration _closedRegistration;
3837
private readonly CancellationTokenRegistration? _closedRequestedRegistration;
3938

@@ -46,7 +45,7 @@ public partial class HubConnectionContext
4645
private readonly int _streamBufferCapacity;
4746
private readonly long? _maxMessageSize;
4847
private bool _receivedMessageTimeoutEnabled;
49-
private long _receivedMessageElapsedTicks;
48+
private TimeSpan _receivedMessageElapsed;
5049
private long _receivedMessageTick;
5150
private ClaimsPrincipal? _user;
5251

@@ -58,8 +57,9 @@ public partial class HubConnectionContext
5857
/// <param name="contextOptions">The options to configure the HubConnectionContext.</param>
5958
public HubConnectionContext(ConnectionContext connectionContext, HubConnectionContextOptions contextOptions, ILoggerFactory loggerFactory)
6059
{
61-
_keepAliveInterval = (long)contextOptions.KeepAliveInterval.TotalMilliseconds;
62-
_clientTimeoutInterval = (long)contextOptions.ClientTimeoutInterval.TotalMilliseconds;
60+
_timeProvider = contextOptions.TimeProvider ?? TimeProvider.System;
61+
_keepAliveInterval = contextOptions.KeepAliveInterval;
62+
_clientTimeoutInterval = contextOptions.ClientTimeoutInterval;
6363
_streamBufferCapacity = contextOptions.StreamBufferCapacity;
6464
_maxMessageSize = contextOptions.MaximumReceiveMessageSize;
6565

@@ -76,8 +76,7 @@ public HubConnectionContext(ConnectionContext connectionContext, HubConnectionCo
7676

7777
HubCallerContext = new DefaultHubCallerContext(this);
7878

79-
_systemClock = contextOptions.SystemClock ?? new SystemClock();
80-
_lastSendTick = _systemClock.CurrentTicks;
79+
_lastSendTick = _timeProvider.GetTimestamp();
8180

8281
var maxInvokeLimit = contextOptions.MaximumParallelInvocations;
8382
ActiveInvocationLimit = new ChannelBasedSemaphore(maxInvokeLimit);
@@ -614,15 +613,16 @@ private async Task AbortAsyncSlow()
614613

615614
private void KeepAliveTick()
616615
{
617-
var currentTime = _systemClock.CurrentTicks;
616+
var currentTime = _timeProvider.GetTimestamp();
617+
var elapsed = _timeProvider.GetElapsedTime(Volatile.Read(ref _lastSendTick), currentTime);
618618

619619
// Implements the keep-alive tick behavior
620620
// Each tick, we check if the time since the last send is larger than the keep alive duration (in ticks).
621621
// If it is, we send a ping frame, if not, we no-op on this tick. This means that in the worst case, the
622622
// true "ping rate" of the server could be (_hubOptions.KeepAliveInterval + HubEndPoint.KeepAliveTimerInterval),
623623
// because if the interval elapses right after the last tick of this timer, it won't be detected until the next tick.
624624

625-
if (currentTime - Volatile.Read(ref _lastSendTick) > _keepAliveInterval)
625+
if (elapsed > _keepAliveInterval)
626626
{
627627
// Haven't sent a message for the entire keep-alive duration, so send a ping.
628628
// If the transport channel is full, this will fail, but that's OK because
@@ -657,11 +657,11 @@ private void CheckClientTimeout()
657657
{
658658
if (_receivedMessageTimeoutEnabled)
659659
{
660-
_receivedMessageElapsedTicks = _systemClock.CurrentTicks - _receivedMessageTick;
660+
_receivedMessageElapsed = _timeProvider.GetElapsedTime(_receivedMessageTick);
661661

662-
if (_receivedMessageElapsedTicks >= _clientTimeoutInterval)
662+
if (_receivedMessageElapsed >= _clientTimeoutInterval)
663663
{
664-
Log.ClientTimeout(_logger, TimeSpan.FromMilliseconds(_clientTimeoutInterval));
664+
Log.ClientTimeout(_logger, _clientTimeoutInterval);
665665
AbortAllowReconnect();
666666
}
667667
}
@@ -707,7 +707,7 @@ internal void BeginClientTimeout()
707707
lock (_receiveMessageTimeoutLock)
708708
{
709709
_receivedMessageTimeoutEnabled = true;
710-
_receivedMessageTick = _systemClock.CurrentTicks;
710+
_receivedMessageTick = _timeProvider.GetTimestamp();
711711
}
712712
}
713713

@@ -717,7 +717,7 @@ internal void StopClientTimeout()
717717
{
718718
// we received a message so stop the timer and reset it
719719
// it will resume after the message has been processed
720-
_receivedMessageElapsedTicks = 0;
720+
_receivedMessageElapsed = TimeSpan.Zero;
721721
_receivedMessageTick = 0;
722722
_receivedMessageTimeoutEnabled = false;
723723
}

src/SignalR/server/Core/src/HubConnectionContextOptions.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using Microsoft.AspNetCore.Internal;
5-
64
namespace Microsoft.AspNetCore.SignalR;
75

86
/// <summary>
@@ -30,7 +28,7 @@ public class HubConnectionContextOptions
3028
/// </summary>
3129
public long? MaximumReceiveMessageSize { get; set; }
3230

33-
internal ISystemClock SystemClock { get; set; } = default!;
31+
internal TimeProvider TimeProvider { get; set; } = default!;
3432

3533
/// <summary>
3634
/// Gets or sets the maximum parallel hub method invocations.

src/SignalR/server/Core/src/HubConnectionHandler.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
using System.Linq;
55
using Microsoft.AspNetCore.Connections;
6-
using Microsoft.AspNetCore.Internal;
76
using Microsoft.AspNetCore.SignalR.Internal;
87
using Microsoft.AspNetCore.SignalR.Protocol;
98
using Microsoft.Extensions.DependencyInjection;
@@ -31,7 +30,7 @@ public class HubConnectionHandler<THub> : ConnectionHandler where THub : Hub
3130
private readonly int _maxParallelInvokes;
3231

3332
// Internal for testing
34-
internal ISystemClock SystemClock { get; set; } = new SystemClock();
33+
internal TimeProvider TimeProvider { get; set; } = TimeProvider.System;
3534

3635
/// <summary>
3736
/// Initializes a new instance of the <see cref="HubConnectionHandler{THub}"/> class.
@@ -120,7 +119,7 @@ public override async Task OnConnectedAsync(ConnectionContext connection)
120119
ClientTimeoutInterval = _hubOptions.ClientTimeoutInterval ?? _globalHubOptions.ClientTimeoutInterval ?? HubOptionsSetup.DefaultClientTimeoutInterval,
121120
StreamBufferCapacity = _hubOptions.StreamBufferCapacity ?? _globalHubOptions.StreamBufferCapacity ?? HubOptionsSetup.DefaultStreamBufferCapacity,
122121
MaximumReceiveMessageSize = _maximumMessageSize,
123-
SystemClock = SystemClock,
122+
TimeProvider = TimeProvider,
124123
MaximumParallelInvocations = _maxParallelInvokes,
125124
};
126125

src/SignalR/server/Core/src/Microsoft.AspNetCore.SignalR.Core.csproj

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@
1515
<Compile Include="$(SharedSourceRoot)ObjectMethodExecutor\*.cs" />
1616
<Compile Include="$(SignalRSharedSourceRoot)AsyncEnumerableAdapters.cs" Link="Internal\AsyncEnumerableAdapters.cs" />
1717
<Compile Include="$(SignalRSharedSourceRoot)TaskCache.cs" Link="Internal\TaskCache.cs" />
18-
<Compile Include="$(SignalRSharedSourceRoot)ISystemClock.cs" Link="Internal\ISystemClock.cs" />
19-
<Compile Include="$(SignalRSharedSourceRoot)SystemClock.cs" Link="Internal\SystemClock.cs" />
2018
<Compile Include="$(SignalRSharedSourceRoot)ClientResultsManager.cs" Link="Internal\ClientResultsManager.cs" />
2119
<Compile Include="$(SignalRSharedSourceRoot)CreateLinkedToken.cs" Link="Internal\CreateLinkedToken.cs" />
2220
<Compile Include="$(SharedSourceRoot)ThrowHelpers\ArgumentNullThrowHelper.cs" LinkBase="Shared" />

src/SignalR/server/SignalR/test/HubConnectionHandlerTestUtils/MockSystemClock.cs renamed to src/SignalR/server/SignalR/test/HubConnectionHandlerTestUtils/TestTimeProvider.cs

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,27 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using Microsoft.AspNetCore.Internal;
5-
64
namespace Microsoft.AspNetCore.SignalR.Tests;
75

8-
public class MockSystemClock : ISystemClock
6+
public class TestTimeProvider : TimeProvider
97
{
108
private long _nowTicks;
119

12-
public MockSystemClock()
10+
public TestTimeProvider()
1311
{
1412
// Use a random DateTimeOffset to ensure tests that incorrectly use the current DateTimeOffset fail always instead of only rarely.
1513
// Pick a date between the min DateTimeOffset and a day before the max DateTimeOffset so there's room to advance the clock.
16-
_nowTicks = NextLong(0, long.MaxValue - (long)TimeSpan.FromDays(1).TotalMilliseconds);
14+
_nowTicks = NextLong(0, long.MaxValue - (long)TimeSpan.FromDays(1).TotalSeconds) * TimestampFrequency;
1715
}
1816

19-
public long CurrentTicks
17+
public override long GetTimestamp() => _nowTicks;
18+
19+
public void Advance(TimeSpan offset)
2020
{
21-
get => _nowTicks;
22-
set
23-
{
24-
Interlocked.Exchange(ref _nowTicks, value);
25-
}
21+
Interlocked.Add(ref _nowTicks, (long)(offset.TotalSeconds * TimestampFrequency));
2622
}
2723

28-
private long NextLong(long minValue, long maxValue)
24+
private static long NextLong(long minValue, long maxValue)
2925
{
3026
return (long)(Random.Shared.NextDouble() * (maxValue - minValue) + minValue);
3127
}

src/SignalR/server/SignalR/test/HubConnectionHandlerTests.cs

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2734,13 +2734,13 @@ public async Task WritesPingMessageIfNothingWrittenWhenKeepAliveIntervalElapses(
27342734
{
27352735
using (StartVerifiableLog())
27362736
{
2737-
var intervalInMS = 100;
2738-
var clock = new MockSystemClock();
2737+
var interval = TimeSpan.FromMilliseconds(100);
2738+
var timeProvider = new TestTimeProvider();
27392739
var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(services =>
27402740
services.Configure<HubOptions>(options =>
2741-
options.KeepAliveInterval = TimeSpan.FromMilliseconds(intervalInMS)), LoggerFactory);
2741+
options.KeepAliveInterval = interval), LoggerFactory);
27422742
var connectionHandler = serviceProvider.GetService<HubConnectionHandler<MethodHub>>();
2743-
connectionHandler.SystemClock = clock;
2743+
connectionHandler.TimeProvider = timeProvider;
27442744

27452745
using (var client = new TestClient(new NewtonsoftJsonHubProtocol()))
27462746
{
@@ -2751,7 +2751,7 @@ public async Task WritesPingMessageIfNothingWrittenWhenKeepAliveIntervalElapses(
27512751
var heartbeatCount = 5;
27522752
for (var i = 0; i < heartbeatCount; i++)
27532753
{
2754-
clock.CurrentTicks = clock.CurrentTicks + intervalInMS + 1;
2754+
timeProvider.Advance(interval + TimeSpan.FromMilliseconds(1));
27552755
client.TickHeartbeat();
27562756
}
27572757

@@ -2796,13 +2796,13 @@ public async Task ConnectionNotTimedOutIfClientNeverPings()
27962796
{
27972797
using (StartVerifiableLog())
27982798
{
2799-
var timeoutInMS = 100;
2800-
var clock = new MockSystemClock();
2799+
var timeout = TimeSpan.FromMilliseconds(100);
2800+
var timeProvider = new TestTimeProvider();
28012801
var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(services =>
28022802
services.Configure<HubOptions>(options =>
2803-
options.ClientTimeoutInterval = TimeSpan.FromMilliseconds(timeoutInMS)), LoggerFactory);
2803+
options.ClientTimeoutInterval = timeout), LoggerFactory);
28042804
var connectionHandler = serviceProvider.GetService<HubConnectionHandler<MethodHub>>();
2805-
connectionHandler.SystemClock = clock;
2805+
connectionHandler.TimeProvider = timeProvider;
28062806

28072807
using (var client = new TestClient(new NewtonsoftJsonHubProtocol()))
28082808
{
@@ -2813,7 +2813,7 @@ public async Task ConnectionNotTimedOutIfClientNeverPings()
28132813
// We go over the 100 ms timeout interval multiple times
28142814
for (var i = 0; i < 3; i++)
28152815
{
2816-
clock.CurrentTicks = clock.CurrentTicks + timeoutInMS + 1;
2816+
timeProvider.Advance(timeout + TimeSpan.FromMilliseconds(1));
28172817
client.TickHeartbeat();
28182818
}
28192819

@@ -2832,21 +2832,21 @@ public async Task ConnectionTimesOutIfInitialPingAndThenNoMessages()
28322832
{
28332833
using (StartVerifiableLog())
28342834
{
2835-
var timeoutInMS = 100;
2836-
var clock = new MockSystemClock();
2835+
var timeout = TimeSpan.FromMilliseconds(100);
2836+
var timeProvider = new TestTimeProvider();
28372837
var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(services =>
28382838
services.Configure<HubOptions>(options =>
2839-
options.ClientTimeoutInterval = TimeSpan.FromMilliseconds(timeoutInMS)), LoggerFactory);
2839+
options.ClientTimeoutInterval = timeout), LoggerFactory);
28402840
var connectionHandler = serviceProvider.GetService<HubConnectionHandler<MethodHub>>();
2841-
connectionHandler.SystemClock = clock;
2841+
connectionHandler.TimeProvider = timeProvider;
28422842

28432843
using (var client = new TestClient(new NewtonsoftJsonHubProtocol()))
28442844
{
28452845
var connectionHandlerTask = await client.ConnectAsync(connectionHandler);
28462846
await client.Connected.DefaultTimeout();
28472847
await client.SendHubMessageAsync(PingMessage.Instance);
28482848

2849-
clock.CurrentTicks = clock.CurrentTicks + timeoutInMS + 1;
2849+
timeProvider.Advance(timeout + TimeSpan.FromMilliseconds(1));
28502850
client.TickHeartbeat();
28512851

28522852
await connectionHandlerTask.DefaultTimeout();
@@ -2859,13 +2859,13 @@ public async Task ReceivingMessagesPreventsConnectionTimeoutFromOccuring()
28592859
{
28602860
using (StartVerifiableLog())
28612861
{
2862-
var timeoutInMS = 300;
2863-
var clock = new MockSystemClock();
2862+
var timeout = TimeSpan.FromMilliseconds(300);
2863+
var timeProvider = new TestTimeProvider();
28642864
var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(services =>
28652865
services.Configure<HubOptions>(options =>
2866-
options.ClientTimeoutInterval = TimeSpan.FromMilliseconds(timeoutInMS)), LoggerFactory);
2866+
options.ClientTimeoutInterval = timeout), LoggerFactory);
28672867
var connectionHandler = serviceProvider.GetService<HubConnectionHandler<MethodHub>>();
2868-
connectionHandler.SystemClock = clock;
2868+
connectionHandler.TimeProvider = timeProvider;
28692869

28702870
using (var client = new TestClient(new NewtonsoftJsonHubProtocol()))
28712871
{
@@ -2875,7 +2875,7 @@ public async Task ReceivingMessagesPreventsConnectionTimeoutFromOccuring()
28752875

28762876
for (int i = 0; i < 10; i++)
28772877
{
2878-
clock.CurrentTicks = clock.CurrentTicks + timeoutInMS - 1;
2878+
timeProvider.Advance(timeout - TimeSpan.FromMilliseconds(1));
28792879
client.TickHeartbeat();
28802880
await client.SendHubMessageAsync(PingMessage.Instance);
28812881
}

0 commit comments

Comments
 (0)