Skip to content

Commit 7a827b8

Browse files
committed
fix: disposing or cancelling cancellation token after timer callback should not throw
1 parent f82265d commit 7a827b8

File tree

7 files changed

+76
-2
lines changed

7 files changed

+76
-2
lines changed

src/TimeScheduler/System/Testing/TestPeriodicTimer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public override ValueTask<bool> WaitForNextTickAsync(CancellationToken cancellat
3434
cancellationRegistration?.Unregister();
3535
cancellationRegistration = cancellationToken.Register(() =>
3636
{
37-
completionSource.TrySetCanceled(cancellationToken);
37+
completionSource?.TrySetCanceled(cancellationToken);
3838
});
3939

4040
return new ValueTask<bool>(completionSource.Task);

src/TimeScheduler/Testing/TestScheduler.Timer.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ public void Dispose()
7272
{
7373
return;
7474
}
75+
7576
isDisposed = true;
7677
callback = null;
7778
stopTimerCts.Dispose();
@@ -98,6 +99,11 @@ private void TimerElapsed()
9899

99100
private void ScheduleCallback(TimeSpan waitTime)
100101
{
102+
if (isDisposed)
103+
{
104+
return;
105+
}
106+
101107
running = true;
102108
owner.RegisterFutureAction(
103109
owner.GetUtcNow() + waitTime,

src/TimeScheduler/TimeScheduler.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<PropertyGroup>
44
<TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
55
<Title>Scheduler</Title>
6-
<Version>0.7.1</Version>
6+
<Version>0.7.2</Version>
77
<Company>Egil Hansen</Company>
88
<Authors>Egil Hansen</Authors>
99
<Description>

test/TimeScheduler.Tests/System/Testing/ManualTimeProviderPeriodicTimerTests.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,29 @@ public void GetUtcNow_matches_time_when_WaitForNextTickAsync_is_invoked()
149149
startTime + interval * 3);
150150
}
151151

152+
[Fact]
153+
public async void Cancelling_token_after_WaitForNextTickAsync_safe()
154+
{
155+
var sut = new ManualTimeProvider();
156+
var interval = TimeSpan.FromSeconds(3);
157+
using var cts = new CancellationTokenSource();
158+
var periodicTimer = sut.CreatePeriodicTimer(interval);
159+
var cleanCancelTask = CancelAfterWaitForNextTick(periodicTimer, cts);
160+
161+
sut.ForwardTime(interval);
162+
163+
await cleanCancelTask;
164+
165+
static async Task CancelAfterWaitForNextTick(TimeScheduler.PeriodicTimer periodicTimer, CancellationTokenSource cts)
166+
{
167+
while (await periodicTimer.WaitForNextTickAsync(cts.Token))
168+
{
169+
break;
170+
}
171+
cts.Cancel();
172+
}
173+
}
174+
152175
static async Task WaitForNextTickInLoop(TimeProvider scheduler, Action callback, TimeSpan interval)
153176
{
154177
using var periodicTimer = scheduler.CreatePeriodicTimer(interval);

test/TimeScheduler.Tests/System/Testing/ManualTimeProviderTimerTests.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,4 +130,15 @@ public void GetUtcNow_matches_time_at_callback_time()
130130
startTime + interval + interval,
131131
startTime + interval + interval + interval);
132132
}
133+
134+
[Fact]
135+
public void Disposing_timer_in_callback()
136+
{
137+
var interval = TimeSpan.FromSeconds(3);
138+
var sut = new ManualTimeProvider();
139+
ITimer timer = default!;
140+
timer = sut.CreateTimer(_ => timer!.Dispose(), null, interval, interval);
141+
142+
sut.ForwardTime(interval);
143+
}
133144
}

test/TimeScheduler.Tests/Testing/TestSchedulerPeriodicTimerTests.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,29 @@ public void GetUtcNow_matches_time_when_WaitForNextTickAsync_is_invoked()
137137
startTime + interval * 3);
138138
}
139139

140+
[Fact]
141+
public async void Cancelling_token_after_WaitForNextTickAsync_safe()
142+
{
143+
var sut = new TestScheduler();
144+
var interval = TimeSpan.FromSeconds(3);
145+
using var cts = new CancellationTokenSource();
146+
var periodicTimer = sut.CreatePeriodicTimer(interval);
147+
var cleanCancelTask = CancelAfterWaitForNextTick(periodicTimer, cts);
148+
149+
sut.ForwardTime(interval);
150+
151+
await cleanCancelTask;
152+
153+
static async Task CancelAfterWaitForNextTick(TimeScheduler.PeriodicTimer periodicTimer, CancellationTokenSource cts)
154+
{
155+
while (await periodicTimer.WaitForNextTickAsync(cts.Token))
156+
{
157+
break;
158+
}
159+
cts.Cancel();
160+
}
161+
}
162+
140163
static async Task WaitForNextTickInLoop(TimeProvider scheduler, Action callback, TimeSpan interval)
141164
{
142165
using var periodicTimer = scheduler.CreatePeriodicTimer(interval);

test/TimeScheduler.Tests/Testing/TestSchedulerTimerTests.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,4 +130,15 @@ public void GetUtcNow_matches_time_at_callback_time()
130130
startTime + interval + interval,
131131
startTime + interval + interval + interval);
132132
}
133+
134+
[Fact]
135+
public void Disposing_timer_in_callback()
136+
{
137+
var interval = TimeSpan.FromSeconds(3);
138+
var sut = new TestScheduler();
139+
ITimer timer = default!;
140+
timer = sut.CreateTimer(_ => timer!.Dispose(), null, interval, interval);
141+
142+
sut.ForwardTime(interval);
143+
}
133144
}

0 commit comments

Comments
 (0)