Skip to content

Commit 45e6aed

Browse files
authored
Fix a few downlevel TimeProvider issues (#85346)
- Consistently null check the Timer stored in the state object - Complete the a task canceled due to the CancellationToken with the CancellationToken - Avoid an unnecessary closure in Delay - Make TimeProvider.System.CreateTimer use Timer's ctor that takes duration/period rather than making a separate call to Change
1 parent 4ab5265 commit 45e6aed

File tree

3 files changed

+75
-22
lines changed

3 files changed

+75
-22
lines changed

src/libraries/Common/src/System/TimeProvider.cs

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -73,15 +73,15 @@ public DateTimeOffset GetLocalNow()
7373
/// <remarks>
7474
/// The default implementation returns <see cref="TimeZoneInfo.Local"/>.
7575
/// </remarks>
76-
public virtual TimeZoneInfo LocalTimeZone { get => TimeZoneInfo.Local; }
76+
public virtual TimeZoneInfo LocalTimeZone => TimeZoneInfo.Local;
7777

7878
/// <summary>
7979
/// Gets the frequency of <see cref="GetTimestamp"/> of high-frequency value per second.
8080
/// </summary>
8181
/// <remarks>
8282
/// The default implementation returns <see cref="Stopwatch.Frequency"/>. For a given TimeProvider instance, the value must be idempotent and remain unchanged.
8383
/// </remarks>
84-
public virtual long TimestampFrequency { get => Stopwatch.Frequency; }
84+
public virtual long TimestampFrequency => Stopwatch.Frequency;
8585

8686
/// <summary>
8787
/// Gets the current high-frequency value designed to measure small time intervals with high accuracy in the timer mechanism.
@@ -187,14 +187,27 @@ public SystemTimeProviderTimer(TimeSpan dueTime, TimeSpan period, TimerCallback
187187
#if SYSTEM_PRIVATE_CORELIB
188188
_timer = new TimerQueueTimer(callback, state, duration, periodTime, flowExecutionContext: true);
189189
#else
190-
// We want to ensure the timer we create will be tracked as long as it is scheduled.
191-
// To do that, we call the constructor which track only the callback which will make the time to be tracked by the scheduler
192-
// then we call Change on the timer to set the desired duration and period.
193-
_timer = new Timer(_ => callback(state));
194-
_timer.Change(duration, periodTime);
190+
// We need to ensure the timer roots itself. Timer created with a duration and period argument
191+
// only roots the state object, so to root the timer we need the state object to reference the
192+
// timer recursively.
193+
var timerState = new TimerState(callback, state);
194+
timerState.Timer = _timer = new Timer(static s =>
195+
{
196+
TimerState ts = (TimerState)s!;
197+
ts.Callback(ts.State);
198+
}, timerState, duration, periodTime);
195199
#endif // SYSTEM_PRIVATE_CORELIB
196200
}
197201

202+
#if !SYSTEM_PRIVATE_CORELIB
203+
private sealed class TimerState(TimerCallback callback, object? state)
204+
{
205+
public TimerCallback Callback { get; } = callback;
206+
public object? State { get; } = state;
207+
public Timer? Timer { get; set; }
208+
}
209+
#endif
210+
198211
public bool Change(TimeSpan dueTime, TimeSpan period)
199212
{
200213
(uint duration, uint periodTime) = CheckAndGetValues(dueTime, period);

src/libraries/Common/tests/System/TimeProviderTests.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,36 @@ public static void NegativeTests()
409409
#endif // !NETFRAMEWORK
410410
}
411411

412+
#if TESTEXTENSIONS
413+
[Fact]
414+
public static void InvokeCallbackFromCreateTimer()
415+
{
416+
TimeProvider p = new InvokeCallbackCreateTimerProvider();
417+
418+
CancellationTokenSource cts = p.CreateCancellationTokenSource(TimeSpan.FromSeconds(0));
419+
Assert.True(cts.IsCancellationRequested);
420+
421+
Task t = p.Delay(TimeSpan.FromSeconds(0));
422+
Assert.True(t.IsCompleted);
423+
424+
t = new TaskCompletionSource<bool>().Task.WaitAsync(TimeSpan.FromSeconds(0), p);
425+
Assert.True(t.IsFaulted);
426+
}
427+
428+
class InvokeCallbackCreateTimerProvider : TimeProvider
429+
{
430+
public override ITimer CreateTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period)
431+
{
432+
ITimer t = base.CreateTimer(callback, state, dueTime, period);
433+
if (dueTime != Timeout.InfiniteTimeSpan)
434+
{
435+
callback(state);
436+
}
437+
return t;
438+
}
439+
}
440+
#endif
441+
412442
class TimerState
413443
{
414444
public TimerState()

src/libraries/Microsoft.Bcl.TimeProvider/src/System/Threading/Tasks/TimeProviderTaskExtensions.cs

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,25 @@ public static class TimeProviderTaskExtensions
1515
#if !NET8_0_OR_GREATER
1616
private sealed class DelayState : TaskCompletionSource<bool>
1717
{
18-
public DelayState() : base(TaskCreationOptions.RunContinuationsAsynchronously) {}
19-
public ITimer Timer { get; set; }
18+
public DelayState(CancellationToken cancellationToken) : base(TaskCreationOptions.RunContinuationsAsynchronously)
19+
{
20+
CancellationToken = cancellationToken;
21+
}
22+
23+
public ITimer? Timer { get; set; }
24+
public CancellationToken CancellationToken { get; }
2025
public CancellationTokenRegistration Registration { get; set; }
2126
}
2227

2328
private sealed class WaitAsyncState : TaskCompletionSource<bool>
2429
{
25-
public WaitAsyncState() : base(TaskCreationOptions.RunContinuationsAsynchronously) { }
30+
public WaitAsyncState(CancellationToken cancellationToken) : base(TaskCreationOptions.RunContinuationsAsynchronously)
31+
{
32+
CancellationToken = cancellationToken;
33+
}
34+
2635
public readonly CancellationTokenSource ContinuationCancellation = new CancellationTokenSource();
36+
public CancellationToken CancellationToken { get; }
2737
public CancellationTokenRegistration Registration;
2838
public ITimer? Timer;
2939
}
@@ -66,22 +76,22 @@ public static Task Delay(this TimeProvider timeProvider, TimeSpan delay, Cancell
6676
return Task.FromCanceled(cancellationToken);
6777
}
6878

69-
DelayState state = new();
79+
DelayState state = new(cancellationToken);
7080

71-
state.Timer = timeProvider.CreateTimer(delayState =>
81+
state.Timer = timeProvider.CreateTimer(static delayState =>
7282
{
7383
DelayState s = (DelayState)delayState!;
7484
s.TrySetResult(true);
7585
s.Registration.Dispose();
76-
s?.Timer.Dispose();
86+
s.Timer?.Dispose();
7787
}, state, delay, Timeout.InfiniteTimeSpan);
7888

79-
state.Registration = cancellationToken.Register(delayState =>
89+
state.Registration = cancellationToken.Register(static delayState =>
8090
{
8191
DelayState s = (DelayState)delayState!;
82-
s.TrySetCanceled(cancellationToken);
92+
s.TrySetCanceled(s.CancellationToken);
8393
s.Registration.Dispose();
84-
s?.Timer.Dispose();
94+
s.Timer?.Dispose();
8595
}, state);
8696

8797
// There are race conditions where the timer fires after we have attached the cancellation callback but before the
@@ -153,7 +163,7 @@ public static Task WaitAsync(this Task task, TimeSpan timeout, TimeProvider time
153163
return Task.FromCanceled(cancellationToken);
154164
}
155165

156-
var state = new WaitAsyncState();
166+
WaitAsyncState state = new(cancellationToken);
157167

158168
state.Timer = timeProvider.CreateTimer(static s =>
159169
{
@@ -162,7 +172,7 @@ public static Task WaitAsync(this Task task, TimeSpan timeout, TimeProvider time
162172
state.TrySetException(new TimeoutException());
163173

164174
state.Registration.Dispose();
165-
state.Timer!.Dispose();
175+
state.Timer?.Dispose();
166176
state.ContinuationCancellation.Cancel();
167177
}, state, timeout, Timeout.InfiniteTimeSpan);
168178

@@ -182,7 +192,7 @@ public static Task WaitAsync(this Task task, TimeSpan timeout, TimeProvider time
182192
{
183193
var state = (WaitAsyncState)s!;
184194

185-
state.TrySetCanceled();
195+
state.TrySetCanceled(state.CancellationToken);
186196

187197
state.Timer?.Dispose();
188198
state.ContinuationCancellation.Cancel();
@@ -259,16 +269,16 @@ public static CancellationTokenSource CreateCancellationTokenSource(this TimePro
259269

260270
var cts = new CancellationTokenSource();
261271

262-
ITimer timer = timeProvider.CreateTimer(s =>
272+
ITimer timer = timeProvider.CreateTimer(static s =>
263273
{
264274
try
265275
{
266-
((CancellationTokenSource)s).Cancel();
276+
((CancellationTokenSource)s!).Cancel();
267277
}
268278
catch (ObjectDisposedException) { }
269279
}, cts, delay, Timeout.InfiniteTimeSpan);
270280

271-
cts.Token.Register(t => ((ITimer)t).Dispose(), timer);
281+
cts.Token.Register(static t => ((ITimer)t!).Dispose(), timer);
272282
return cts;
273283
#endif // NET8_0_OR_GREATER
274284
}

0 commit comments

Comments
 (0)