Repository navigation
Expand file tree
/
Copy pathTimeProvider.cs
More file actions
456 lines (378 loc) · 19.3 KB
/
Copy pathTimeProvider.cs
File metadata and controls
456 lines (378 loc) · 19.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics.CodeAnalysis;
#pragma warning disable
namespace System.Threading
{
internal interface ITimer : IDisposable, IAsyncDisposable
{
bool Change(TimeSpan dueTime, TimeSpan period);
}
}
namespace System
{
// Temporary, will be removed
// Copied from https://github.com/dotnet/runtime/blob/main/src/libraries/Common/src/System/TimeProvider.cs and trimmed some fat which is not relevant for internal stuff
[ExcludeFromCodeCoverage]
internal abstract class TimeProvider
{
public static TimeProvider System { get; } = new SystemTimeProvider();
protected TimeProvider()
{
}
public virtual DateTimeOffset GetUtcNow() => DateTimeOffset.UtcNow;
private static readonly long MinDateTicks = DateTime.MinValue.Ticks;
private static readonly long MaxDateTicks = DateTime.MaxValue.Ticks;
public DateTimeOffset GetLocalNow()
{
DateTimeOffset utcDateTime = GetUtcNow();
TimeZoneInfo zoneInfo = LocalTimeZone;
if (zoneInfo is null)
{
throw new InvalidOperationException();
}
TimeSpan offset = zoneInfo.GetUtcOffset(utcDateTime);
long localTicks = utcDateTime.Ticks + offset.Ticks;
if ((ulong)localTicks > (ulong)MaxDateTicks)
{
localTicks = localTicks < MinDateTicks ? MinDateTicks : MaxDateTicks;
}
return new DateTimeOffset(localTicks, offset);
}
public virtual TimeZoneInfo LocalTimeZone => TimeZoneInfo.Local;
public virtual long TimestampFrequency => Stopwatch.Frequency;
public virtual long GetTimestamp() => Stopwatch.GetTimestamp();
public TimeSpan GetElapsedTime(long startingTimestamp, long endingTimestamp)
{
long timestampFrequency = TimestampFrequency;
if (timestampFrequency <= 0)
{
throw new InvalidOperationException();
}
return new TimeSpan((long)((endingTimestamp - startingTimestamp) * ((double)TimeSpan.TicksPerSecond / timestampFrequency)));
}
public TimeSpan GetElapsedTime(long startingTimestamp) => GetElapsedTime(startingTimestamp, GetTimestamp());
public virtual ITimer CreateTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period)
{
if (callback is null)
{
throw new ArgumentNullException(nameof(callback));
}
return new SystemTimeProviderTimer(dueTime, period, callback, state);
}
[ExcludeFromCodeCoverage]
private sealed class SystemTimeProviderTimer : ITimer
{
private readonly Timer _timer;
public SystemTimeProviderTimer(TimeSpan dueTime, TimeSpan period, TimerCallback callback, object? state)
{
(uint duration, uint periodTime) = CheckAndGetValues(dueTime, period);
// We need to ensure the timer roots itself. Timer created with a duration and period argument
// only roots the state object, so to root the timer we need the state object to reference the
// timer recursively.
var timerState = new TimerState(callback, state);
timerState.Timer = _timer = new Timer(static s =>
{
TimerState ts = (TimerState)s!;
ts.Callback(ts.State);
}, timerState, duration, periodTime);
}
private sealed class TimerState
{
public TimerState(TimerCallback callback, object? state)
{
Callback = callback;
State = state;
}
public TimerCallback Callback { get; }
public object? State { get; }
public Timer? Timer { get; set; }
}
public bool Change(TimeSpan dueTime, TimeSpan period)
{
(uint duration, uint periodTime) = CheckAndGetValues(dueTime, period);
try
{
return _timer.Change(duration, periodTime);
}
catch (ObjectDisposedException)
{
return false;
}
}
public void Dispose() => _timer.Dispose();
public ValueTask DisposeAsync()
{
_timer.Dispose();
return default;
}
private static (uint duration, uint periodTime) CheckAndGetValues(TimeSpan dueTime, TimeSpan periodTime)
{
long dueTm = (long)dueTime.TotalMilliseconds;
long periodTm = (long)periodTime.TotalMilliseconds;
const uint MaxSupportedTimeout = 0xfffffffe;
if (dueTm < -1)
{
throw new ArgumentOutOfRangeException(nameof(dueTime));
}
if (dueTm > MaxSupportedTimeout)
{
throw new ArgumentOutOfRangeException(nameof(dueTime));
}
if (periodTm < -1)
{
throw new ArgumentOutOfRangeException(nameof(periodTm));
}
if (periodTm > MaxSupportedTimeout)
{
throw new ArgumentOutOfRangeException(nameof(periodTm));
}
return ((uint)dueTm, (uint)periodTm);
}
}
[ExcludeFromCodeCoverage]
private sealed class SystemTimeProvider : TimeProvider
{
internal SystemTimeProvider()
{
}
}
}
}
namespace System.Threading.Tasks
{
/// <summary>
/// Provide extensions methods for <see cref="Task"/> operations with <see cref="TimeProvider"/>.
/// </summary>
/// <remarks>
/// The Microsoft.Bcl.TimeProvider library interfaces are intended solely for use in building against pre-.NET 8 surface area.
/// If your code is being built against .NET 8 or higher, then this library should not be utilized.
/// </remarks>
[ExcludeFromCodeCoverage]
internal static class TimeProviderTaskExtensions
{
private sealed class DelayState : TaskCompletionSource<bool>
{
public DelayState(CancellationToken cancellationToken)
: base(TaskCreationOptions.RunContinuationsAsynchronously) => CancellationToken = cancellationToken;
public ITimer? Timer { get; set; }
public CancellationToken CancellationToken { get; }
public CancellationTokenRegistration Registration { get; set; }
}
private sealed class WaitAsyncState : TaskCompletionSource<bool>
{
public WaitAsyncState(CancellationToken cancellationToken)
: base(TaskCreationOptions.RunContinuationsAsynchronously) => CancellationToken = cancellationToken;
public readonly CancellationTokenSource ContinuationCancellation = new();
public CancellationToken CancellationToken { get; }
public CancellationTokenRegistration Registration;
public ITimer? Timer;
}
/// <summary>Creates a task that completes after a specified time interval.</summary>
/// <param name="timeProvider">The <see cref="TimeProvider"/> with which to interpret <paramref name="delay"/>.</param>
/// <param name="delay">The <see cref="TimeSpan"/> to wait before completing the returned task, or <see cref="Timeout.InfiniteTimeSpan"/> to wait indefinitely.</param>
/// <param name="cancellationToken">A cancellation token to observe while waiting for the task to complete.</param>
/// <returns>A task that represents the time delay.</returns>
/// <exception cref="System.ArgumentNullException">The <paramref name="timeProvider"/> argument is null.</exception>
/// <exception cref="System.ArgumentOutOfRangeException"><paramref name="delay"/> represents a negative time interval other than <see cref="Timeout.InfiniteTimeSpan"/>.</exception>
public static Task Delay(this TimeProvider timeProvider, TimeSpan delay, CancellationToken cancellationToken = default)
{
if (timeProvider == TimeProvider.System)
{
return Task.Delay(delay, cancellationToken);
}
if (timeProvider is null)
{
throw new ArgumentNullException(nameof(timeProvider));
}
if (delay != Timeout.InfiniteTimeSpan && delay < TimeSpan.Zero)
{
throw new ArgumentOutOfRangeException(nameof(delay));
}
if (delay == TimeSpan.Zero)
{
return Task.CompletedTask;
}
if (cancellationToken.IsCancellationRequested)
{
return Task.FromCanceled(cancellationToken);
}
DelayState state = new(cancellationToken);
state.Timer = timeProvider.CreateTimer(static delayState =>
{
DelayState s = (DelayState)delayState!;
s.TrySetResult(true);
s.Registration.Dispose();
s.Timer?.Dispose();
}, state, delay, Timeout.InfiniteTimeSpan);
state.Registration = cancellationToken.Register(static delayState =>
{
DelayState s = (DelayState)delayState!;
s.TrySetCanceled(s.CancellationToken);
s.Registration.Dispose();
s.Timer?.Dispose();
}, state);
// There are race conditions where the timer fires after we have attached the cancellation callback but before the
// registration is stored in state.Registration, or where cancellation is requested prior to the registration being
// stored into state.Registration, or where the timer could fire after it's been created but before it's been stored
// in state.Timer. In such cases, the cancellation registration and/or the Timer might be stored into state after the
// callbacks and thus left undisposed. So, we do a subsequent check here. If the task isn't completed by this point,
// then the callbacks won't have called TrySetResult (the callbacks invoke TrySetResult before disposing of the fields),
// in which case it will see both the timer and registration set and be able to Dispose them. If the task is completed
// by this point, then this is guaranteed to see s.Timer as non-null because it was deterministically set above.
if (state.Task.IsCompleted)
{
state.Registration.Dispose();
state.Timer.Dispose();
}
return state.Task;
}
/// <summary>
/// Gets a <see cref="Task"/> that will complete when this <see cref="Task"/> completes,
/// when the specified timeout expires, or when the specified <see cref="CancellationToken"/> has cancellation requested.
/// </summary>
/// <param name="task">The task for which to wait on until completion.</param>
/// <param name="timeout">The timeout after which the <see cref="Task"/> should be faulted with a <see cref="TimeoutException"/> if it hasn't otherwise completed.</param>
/// <param name="timeProvider">The <see cref="TimeProvider"/> with which to interpret <paramref name="timeout"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for a cancellation request.</param>
/// <returns>The <see cref="Task"/> representing the asynchronous wait. It may or may not be the same instance as the current instance.</returns>
/// <exception cref="System.ArgumentNullException">The <paramref name="task"/> argument is null.</exception>
/// <exception cref="System.ArgumentNullException">The <paramref name="timeProvider"/> argument is null.</exception>
/// <exception cref="System.ArgumentOutOfRangeException"><paramref name="timeout"/> represents a negative time interval other than <see cref="Timeout.InfiniteTimeSpan"/>.</exception>
public static Task WaitAsync(this Task task, TimeSpan timeout, TimeProvider timeProvider, CancellationToken cancellationToken = default)
{
if (task is null)
{
throw new ArgumentNullException(nameof(task));
}
if (timeout != Timeout.InfiniteTimeSpan && timeout < TimeSpan.Zero)
{
throw new ArgumentOutOfRangeException(nameof(timeout));
}
if (timeProvider is null)
{
throw new ArgumentNullException(nameof(timeProvider));
}
if (task.IsCompleted)
{
return task;
}
if (timeout == Timeout.InfiniteTimeSpan && !cancellationToken.CanBeCanceled)
{
return task;
}
if (timeout == TimeSpan.Zero)
{
Task.FromException(new TimeoutException());
}
if (cancellationToken.IsCancellationRequested)
{
return Task.FromCanceled(cancellationToken);
}
WaitAsyncState state = new(cancellationToken);
state.Timer = timeProvider.CreateTimer(static s =>
{
var state = (WaitAsyncState)s!;
state.TrySetException(new TimeoutException());
state.Registration.Dispose();
state.Timer?.Dispose();
state.ContinuationCancellation.Cancel();
}, state, timeout, Timeout.InfiniteTimeSpan);
_ = task.ContinueWith(static (t, s) =>
{
var state = (WaitAsyncState)s!;
if (t.IsFaulted)
{
state.TrySetException(t.Exception!.InnerExceptions);
}
else if (t.IsCanceled)
{
state.TrySetCanceled();
}
else
{
state.TrySetResult(true);
}
state.Registration.Dispose();
state.Timer?.Dispose();
}, state, state.ContinuationCancellation.Token, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
state.Registration = cancellationToken.Register(static s =>
{
var state = (WaitAsyncState)s!;
state.TrySetCanceled(state.CancellationToken);
state.Timer?.Dispose();
state.ContinuationCancellation.Cancel();
}, state);
// See explanation in Delay for this final check
if (state.Task.IsCompleted)
{
state.Registration.Dispose();
state.Timer.Dispose();
}
return state.Task;
}
/// <summary>
/// Gets a <see cref="Task"/> that will complete when this <see cref="Task"/> completes,
/// when the specified timeout expires, or when the specified <see cref="CancellationToken"/> has cancellation requested.
/// </summary>
/// <param name="task">The task for which to wait on until completion.</param>
/// <param name="timeout">The timeout after which the <see cref="Task"/> should be faulted with a <see cref="TimeoutException"/> if it hasn't otherwise completed.</param>
/// <param name="timeProvider">The <see cref="TimeProvider"/> with which to interpret <paramref name="timeout"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for a cancellation request.</param>
/// <returns>The <see cref="Task"/> representing the asynchronous wait. It may or may not be the same instance as the current instance.</returns>
/// <exception cref="System.ArgumentNullException">The <paramref name="task"/> argument is null.</exception>
/// <exception cref="System.ArgumentNullException">The <paramref name="timeProvider"/> argument is null.</exception>
/// <exception cref="System.ArgumentOutOfRangeException"><paramref name="timeout"/> represents a negative time interval other than <see cref="Timeout.InfiniteTimeSpan"/>.</exception>
public static async Task<TResult> WaitAsync<TResult>(this Task<TResult> task, TimeSpan timeout, TimeProvider timeProvider, CancellationToken cancellationToken = default)
{
await ((Task)task).WaitAsync(timeout, timeProvider, cancellationToken).ConfigureAwait(false);
return task.Result;
}
/// <summary>Initializes a new instance of the <see cref="CancellationTokenSource"/> class that will be canceled after the specified <see cref="TimeSpan"/>. </summary>
/// <param name="timeProvider">The <see cref="TimeProvider"/> with which to interpret the <paramref name="delay"/>. </param>
/// <param name="delay">The time interval to wait before canceling this <see cref="CancellationTokenSource"/>. </param>
/// <exception cref="ArgumentOutOfRangeException"> The <paramref name="delay"/> is negative and not equal to <see cref="Timeout.InfiniteTimeSpan" />
/// or greater than maximum allowed timer duration.</exception>
/// <returns><see cref="CancellationTokenSource"/> that will be canceled after the specified <paramref name="delay"/>.</returns>
/// <remarks>
/// <para>
/// The countdown for the delay starts during the call to the constructor. When the delay expires,
/// the constructed <see cref="CancellationTokenSource"/> is canceled if it has
/// not been canceled already.
/// </para>
/// <para>
/// If running on .NET versions earlier than .NET 8.0, there is a constraint when invoking <see cref="CancellationTokenSource.CancelAfter(TimeSpan)"/> on the resultant object.
/// This action will not terminate the initial timer indicated by <paramref name="delay"/>. However, this restriction does not apply on .NET 8.0 and later versions.
/// </para>
/// </remarks>
public static CancellationTokenSource CreateCancellationTokenSource(this TimeProvider timeProvider, TimeSpan delay)
{
if (timeProvider is null)
{
throw new ArgumentNullException(nameof(timeProvider));
}
if (delay != Timeout.InfiniteTimeSpan && delay < TimeSpan.Zero)
{
throw new ArgumentOutOfRangeException(nameof(delay));
}
if (timeProvider == TimeProvider.System)
{
return new CancellationTokenSource(delay);
}
var cts = new CancellationTokenSource();
ITimer timer = timeProvider.CreateTimer(static s =>
{
try
{
((CancellationTokenSource)s!).Cancel();
}
catch (ObjectDisposedException)
{
// ok
}
}, cts, delay, Timeout.InfiniteTimeSpan);
cts.Token.Register(static t => ((ITimer)t!).Dispose(), timer);
return cts;
}
}
}