Skip to content

Commit b87c3c5

Browse files
committed
fix: prevent duplicated scheduling of callbacks
1 parent e879d5a commit b87c3c5

File tree

7 files changed

+48
-15
lines changed

7 files changed

+48
-15
lines changed

docs/TimeProviderExtensions.AutoAdvanceBehavior.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ public System.TimeSpan ClockAdvanceAmount { get; set; }
3232
#### Exceptions
3333

3434
[System.ArgumentOutOfRangeException](https://docs.microsoft.com/en-us/dotnet/api/System.ArgumentOutOfRangeException 'System.ArgumentOutOfRangeException')
35-
Thrown when set to a value than [System.TimeSpan.Zero](https://docs.microsoft.com/en-us/dotnet/api/System.TimeSpan.Zero 'System.TimeSpan.Zero').
35+
Thrown when set to a value less than [System.TimeSpan.Zero](https://docs.microsoft.com/en-us/dotnet/api/System.TimeSpan.Zero 'System.TimeSpan.Zero').
3636
3737
### Remarks
3838
Set to [System.TimeSpan.Zero](https://docs.microsoft.com/en-us/dotnet/api/System.TimeSpan.Zero 'System.TimeSpan.Zero') to disable auto advance. The default value is [System.TimeSpan.Zero](https://docs.microsoft.com/en-us/dotnet/api/System.TimeSpan.Zero 'System.TimeSpan.Zero').
@@ -54,7 +54,7 @@ public System.TimeSpan TimestampAdvanceAmount { get; set; }
5454
#### Exceptions
5555

5656
[System.ArgumentOutOfRangeException](https://docs.microsoft.com/en-us/dotnet/api/System.ArgumentOutOfRangeException 'System.ArgumentOutOfRangeException')
57-
Thrown when set to a value than [System.TimeSpan.Zero](https://docs.microsoft.com/en-us/dotnet/api/System.TimeSpan.Zero 'System.TimeSpan.Zero').
57+
Thrown when set to a value less than [System.TimeSpan.Zero](https://docs.microsoft.com/en-us/dotnet/api/System.TimeSpan.Zero 'System.TimeSpan.Zero').
5858
5959
### Remarks
6060
Set to [System.TimeSpan.Zero](https://docs.microsoft.com/en-us/dotnet/api/System.TimeSpan.Zero 'System.TimeSpan.Zero') to disable auto advance. The default value is [System.TimeSpan.Zero](https://docs.microsoft.com/en-us/dotnet/api/System.TimeSpan.Zero 'System.TimeSpan.Zero').

docs/TimeProviderExtensions.ManualTimeProvider.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ The [ManualTimeProvider](TimeProviderExtensions.ManualTimeProvider.md 'TimeProvi
241241

242242
### Remarks
243243
Override this methods to return a custom implementation of [ManualTimer](TimeProviderExtensions.ManualTimer.md 'TimeProviderExtensions.ManualTimer'). This also allows for intercepting and wrapping
244-
the provided timer [callback](TimeProviderExtensions.ManualTimeProvider.md#TimeProviderExtensions.ManualTimeProvider.CreateManualTimer(System.Threading.TimerCallback,object,TimeProviderExtensions.ManualTimeProvider).callback 'TimeProviderExtensions.ManualTimeProvider.CreateManualTimer(System.Threading.TimerCallback, object, TimeProviderExtensions.ManualTimeProvider).callback') and [state](TimeProviderExtensions.ManualTimeProvider.md#TimeProviderExtensions.ManualTimeProvider.CreateManualTimer(System.Threading.TimerCallback,object,TimeProviderExtensions.ManualTimeProvider).state 'TimeProviderExtensions.ManualTimeProvider.CreateManualTimer(System.Threading.TimerCallback, object, TimeProviderExtensions.ManualTimeProvider).state'), enabling more advanced testing scenarioes.
244+
the provided timer [callback](TimeProviderExtensions.ManualTimeProvider.md#TimeProviderExtensions.ManualTimeProvider.CreateManualTimer(System.Threading.TimerCallback,object,TimeProviderExtensions.ManualTimeProvider).callback 'TimeProviderExtensions.ManualTimeProvider.CreateManualTimer(System.Threading.TimerCallback, object, TimeProviderExtensions.ManualTimeProvider).callback') and [state](TimeProviderExtensions.ManualTimeProvider.md#TimeProviderExtensions.ManualTimeProvider.CreateManualTimer(System.Threading.TimerCallback,object,TimeProviderExtensions.ManualTimeProvider).state 'TimeProviderExtensions.ManualTimeProvider.CreateManualTimer(System.Threading.TimerCallback, object, TimeProviderExtensions.ManualTimeProvider).state'), enabling more advanced testing scenarios.
245245

246246
<a name='TimeProviderExtensions.ManualTimeProvider.CreateTimer(System.Threading.TimerCallback,object,System.TimeSpan,System.TimeSpan)'></a>
247247

docs/TimeProviderExtensions.ManualTimer.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
## ManualTimer Class
55

6-
A implementaiton of a [System.Threading.ITimer](https://docs.microsoft.com/en-us/dotnet/api/System.Threading.ITimer 'System.Threading.ITimer') whose callbacks are scheduled via a [ManualTimeProvider](TimeProviderExtensions.ManualTimeProvider.md 'TimeProviderExtensions.ManualTimeProvider').
6+
A implementation of a [System.Threading.ITimer](https://docs.microsoft.com/en-us/dotnet/api/System.Threading.ITimer 'System.Threading.ITimer') whose callbacks are scheduled via a [ManualTimeProvider](TimeProviderExtensions.ManualTimeProvider.md 'TimeProviderExtensions.ManualTimeProvider').
77

88
```csharp
99
public class ManualTimer :

src/TimeProviderExtensions/ManualTimeProvider.cs

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Diagnostics;
22
using System.Globalization;
3+
using System.Runtime.CompilerServices;
34

45
namespace TimeProviderExtensions;
56

@@ -327,10 +328,13 @@ public void SetUtcNow(DateTimeOffset value)
327328
while (utcNow <= value && TryGetNext(value) is ManualTimerScheduler { CallbackTime: not null } scheduler)
328329
{
329330
utcNow = scheduler.CallbackTime.Value;
330-
scheduler.TimerElapsed();
331+
scheduler.TimerElapsed(scheduleNextCallback: true);
331332
}
332333

333-
utcNow = value;
334+
if (utcNow < value)
335+
{
336+
utcNow = value;
337+
}
334338

335339
Debug.Assert(callbacks.All(x => x.CallbackTime > utcNow), "Because there should never by any callbacks scheduled in the past at this point.");
336340
}
@@ -470,9 +474,13 @@ public void Jump(DateTimeOffset value)
470474
// in the jump period and invokes the callback that
471475
// number of times. Has to happen at least one time.
472476
var callbacksPassed = Math.Max(1, Math.Floor((double)jump.Ticks / scheduler.Period.Ticks));
473-
for (int i = 0; i < callbacksPassed; i++)
477+
478+
// Invoke scheduler.TimerElapsed with scheduleNextCallback = false
479+
// to prevent duplicates in the callbacks collection.
480+
// Only the last call should schedule next callback.
481+
for (int invocations = 0; invocations < callbacksPassed; invocations++)
474482
{
475-
scheduler.TimerElapsed();
483+
scheduler.TimerElapsed(scheduleNextCallback: invocations == callbacksPassed - 1);
476484
}
477485
}
478486

@@ -500,7 +508,7 @@ internal void ScheduleCallback(ManualTimerScheduler scheduler, TimeSpan waitTime
500508
{
501509
lock (callbacks)
502510
{
503-
Debug.Assert(!callbacks.Contains(scheduler), "A scheduler should only be added to callbacks one time.");
511+
Debug.Assert(!callbacks.Contains(scheduler), "A scheduler should only be added to callbacks one time, and is expected to be removed before callback is invoked.");
504512

505513
scheduler.CallbackTime = utcNow + waitTime;
506514

@@ -517,11 +525,11 @@ internal void ScheduleCallback(ManualTimerScheduler scheduler, TimeSpan waitTime
517525
}
518526
}
519527

520-
internal void RemoveCallback(ManualTimerScheduler timerCallback)
528+
internal void RemoveCallback(ManualTimerScheduler scheduler)
521529
{
522530
lock (callbacks)
523531
{
524-
var existingIndexOf = callbacks.FindIndex(0, x => ReferenceEquals(x, timerCallback));
532+
var existingIndexOf = callbacks.FindIndex(0, x => ReferenceEquals(x, scheduler));
525533
if (existingIndexOf >= 0)
526534
{
527535
callbacks.RemoveAt(existingIndexOf);

src/TimeProviderExtensions/ManualTimerScheduler.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,13 @@ internal void Change(TimeSpan dueTime, TimeSpan period)
5151
}
5252
}
5353

54-
internal void TimerElapsed()
54+
internal void TimerElapsed(bool scheduleNextCallback)
5555
{
5656
CallbackTime = null;
5757
callback.Invoke(state);
5858
CallbackInvokeCount++;
5959

60-
if (Period != Timeout.InfiniteTimeSpan && Period != TimeSpan.Zero)
60+
if (scheduleNextCallback && Period != Timeout.InfiniteTimeSpan && Period != TimeSpan.Zero)
6161
{
6262
ScheduleCallback(Period);
6363
}
@@ -67,7 +67,7 @@ private void ScheduleCallback(TimeSpan waitTime)
6767
{
6868
if (waitTime == TimeSpan.Zero)
6969
{
70-
TimerElapsed();
70+
TimerElapsed(scheduleNextCallback: true);
7171
}
7272
else
7373
{

test/TimeProviderExtensions.Tests/ManualTimeProviderTests.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,6 @@ public void ActiveTimers_with_after_timer_state_change()
318318
sut.ActiveTimers.Should().Be(0);
319319
}
320320

321-
322321
[Fact]
323322
public void CreateManualTimer_with_custom_timer_type()
324323
{

test/TimeProviderExtensions.Tests/ManualTimeProviderTimerTests.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,4 +209,30 @@ public void Jumping_past_longer_than_recurrence()
209209

210210
callbackTimes.Should().Equal(startTime + target);
211211
}
212+
213+
[Fact]
214+
public void jumping_causes_multiple_timers_invokes_callback_in_order()
215+
{
216+
var sut = new ManualTimeProvider();
217+
var callbacks = new List<(int timerId, TimeSpan callbackTime)>();
218+
var startTime = sut.GetTimestamp();
219+
using var timer1 = sut.CreateTimer(_ => callbacks.Add((1, sut.GetElapsedTime(startTime))), null, TimeSpan.FromMilliseconds(3), TimeSpan.FromMilliseconds(3));
220+
using var timer2 = sut.CreateTimer(_ => callbacks.Add((2, sut.GetElapsedTime(startTime))), null, TimeSpan.FromMilliseconds(3), TimeSpan.FromMilliseconds(3));
221+
using var timer3 = sut.CreateTimer(_ => callbacks.Add((3, sut.GetElapsedTime(startTime))), null, TimeSpan.FromMilliseconds(6), TimeSpan.FromMilliseconds(5));
222+
223+
sut.Jump(TimeSpan.FromMilliseconds(3));
224+
sut.Jump(TimeSpan.FromMilliseconds(3));
225+
sut.Jump(TimeSpan.FromMilliseconds(3));
226+
sut.Jump(TimeSpan.FromMilliseconds(2));
227+
228+
callbacks.Should().Equal(
229+
(1, TimeSpan.FromMilliseconds(3)),
230+
(2, TimeSpan.FromMilliseconds(3)),
231+
(3, TimeSpan.FromMilliseconds(6)),
232+
(1, TimeSpan.FromMilliseconds(6)),
233+
(2, TimeSpan.FromMilliseconds(6)),
234+
(1, TimeSpan.FromMilliseconds(9)),
235+
(2, TimeSpan.FromMilliseconds(9)),
236+
(3, TimeSpan.FromMilliseconds(11)));
237+
}
212238
}

0 commit comments

Comments
 (0)