| | | 1 | | namespace AsyncResponse.Testing; |
| | | 2 | | |
| | | 3 | | /// <summary> |
| | | 4 | | /// A deterministic, manually-advanced <see cref="TimeProvider"/>. Registered as the engine clock |
| | | 5 | | /// (which <see cref="AsyncResponseTestHarness"/> does for you), it makes every time-driven part of |
| | | 6 | | /// AsyncResponse — waiter timeouts, execution leases, retry backoff, durable timers, cron |
| | | 7 | | /// schedules — run on virtual time: a three-day sleep completes the instant the test calls |
| | | 8 | | /// <see cref="Advance"/>. |
| | | 9 | | /// <para> |
| | | 10 | | /// <see cref="Advance"/> moves time <b>stepwise</b>: it walks to each armed timer's due instant in |
| | | 11 | | /// order (due time, then creation order), updates "now" to that instant, and fires the callback |
| | | 12 | | /// inline on the calling thread before moving on. Timers armed by a firing callback (a lease renew |
| | | 13 | | /// loop re-arming itself, a chunked wake-up re-publishing) are honored within the same advance, so |
| | | 14 | | /// interleavings match real time — a renew loop beats a lease expiry that sits later on the |
| | | 15 | | /// timeline, never the other way around. |
| | | 16 | | /// </para> |
| | | 17 | | /// <para> |
| | | 18 | | /// Time never moves on its own; <see cref="GetUtcNow"/> is exact and starts at |
| | | 19 | | /// <see cref="DefaultStartTime"/> (2030-01-01T00:00:00Z) unless a start is supplied. Thread-safe: |
| | | 20 | | /// advances are serialized, and an advance made from inside a timer callback nests (see |
| | | 21 | | /// <see cref="AdvanceTo"/>). |
| | | 22 | | /// </para> |
| | | 23 | | /// <para> |
| | | 24 | | /// <see cref="CreateTimer"/> and <see cref="ITimer.Change"/> accept and reject exactly the |
| | | 25 | | /// arguments the system timer does — whole milliseconds, <c>-1</c> for "never", and the |
| | | 26 | | /// 4294967294 ms (~49.7-day) ceiling — so a timer the virtual clock arms is one production arms too. |
| | | 27 | | /// </para> |
| | | 28 | | /// </summary> |
| | | 29 | | public sealed class VirtualTimeProvider : TimeProvider |
| | | 30 | | { |
| | | 31 | | /// <summary>The default virtual epoch: a fixed instant, so tests never depend on the wall clock.</summary> |
| | 2 | 32 | | public static readonly DateTimeOffset DefaultStartTime = new(2030, 1, 1, 0, 0, 0, TimeSpan.Zero); |
| | | 33 | | |
| | | 34 | | // _gate guards the clock state and is never held while a callback runs. _advanceGate |
| | | 35 | | // serializes whole advances and IS held across their callbacks; it is always taken first. |
| | 446 | 36 | | private readonly object _gate = new(); |
| | 446 | 37 | | private readonly object _advanceGate = new(); |
| | 446 | 38 | | private readonly SortedSet<VirtualTimer> _armed = new(VirtualTimerOrder.Instance); |
| | | 39 | | private DateTimeOffset _utcNow; |
| | | 40 | | private long _sequence; |
| | | 41 | | |
| | | 42 | | /// <summary>Creates a provider starting at <see cref="DefaultStartTime"/>.</summary> |
| | | 43 | | public VirtualTimeProvider() |
| | 252 | 44 | | : this(DefaultStartTime) |
| | | 45 | | { |
| | 252 | 46 | | } |
| | | 47 | | |
| | | 48 | | /// <summary>Creates a provider starting at <paramref name="startTime"/>.</summary> |
| | 446 | 49 | | public VirtualTimeProvider(DateTimeOffset startTime) |
| | 446 | 50 | | => _utcNow = startTime.ToUniversalTime(); |
| | | 51 | | |
| | | 52 | | /// <inheritdoc/> |
| | | 53 | | public override DateTimeOffset GetUtcNow() |
| | | 54 | | { |
| | 84873 | 55 | | lock (_gate) |
| | 84873 | 56 | | return _utcNow; |
| | 84873 | 57 | | } |
| | | 58 | | |
| | | 59 | | /// <inheritdoc/> |
| | 2 | 60 | | public override TimeZoneInfo LocalTimeZone => TimeZoneInfo.Utc; |
| | | 61 | | |
| | | 62 | | /// <inheritdoc/> |
| | 14 | 63 | | public override long TimestampFrequency => TimeSpan.TicksPerSecond; |
| | | 64 | | |
| | | 65 | | /// <inheritdoc/> |
| | | 66 | | public override long GetTimestamp() |
| | | 67 | | { |
| | 40 | 68 | | lock (_gate) |
| | 40 | 69 | | return _utcNow.UtcTicks; |
| | 40 | 70 | | } |
| | | 71 | | |
| | | 72 | | /// <summary>The earliest armed timer's due instant, or <c>null</c> when nothing is armed. Diagnostic.</summary> |
| | | 73 | | public DateTimeOffset? NextTimerDueAt |
| | | 74 | | { |
| | | 75 | | get |
| | | 76 | | { |
| | 363069 | 77 | | lock (_gate) |
| | 363069 | 78 | | return _armed.Count == 0 ? null : _armed.Min!.DueAt; |
| | 363069 | 79 | | } |
| | | 80 | | } |
| | | 81 | | |
| | | 82 | | /// <summary> |
| | | 83 | | /// Advances virtual time by <paramref name="delta"/>, firing every timer that falls due, in order. |
| | | 84 | | /// See <see cref="AdvanceTo"/> for how concurrent and re-entrant advances behave. |
| | | 85 | | /// </summary> |
| | | 86 | | public void Advance(TimeSpan delta) |
| | | 87 | | { |
| | 33476 | 88 | | ArgumentOutOfRangeException.ThrowIfLessThan(delta, TimeSpan.Zero); |
| | | 89 | | |
| | | 90 | | // The target is relative to "now", so it is computed INSIDE the advance gate: read before |
| | | 91 | | // it, two threads advancing at once both started from the same instant and landed as one |
| | | 92 | | // step — or one of them found the clock already past its target and threw "backwards". |
| | 33474 | 93 | | lock (_advanceGate) |
| | 33474 | 94 | | AdvanceTo(GetUtcNow() + delta); |
| | 33474 | 95 | | } |
| | | 96 | | |
| | | 97 | | /// <summary> |
| | | 98 | | /// Advances virtual time to <paramref name="target"/>, firing every timer that falls due, in order. |
| | | 99 | | /// <para> |
| | | 100 | | /// One advance runs at a time. A call from another thread waits for the running advance to |
| | | 101 | | /// finish and then performs its own, so callbacks never run side by side and a callback never |
| | | 102 | | /// observes a clock later than its own fire instant. A call from <b>inside a timer callback</b> |
| | | 103 | | /// (re-entrant, same thread) nests: it runs to completion right there, firing everything due up |
| | | 104 | | /// to its own target in order, and the outer advance then carries on from wherever time stands — |
| | | 105 | | /// an outer target the nested advance already passed is simply complete, because time never |
| | | 106 | | /// moves backwards. Callbacks run on the advancing thread with no clock state locked, so they |
| | | 107 | | /// may read the clock and create, change, or dispose timers freely; a callback that blocks on |
| | | 108 | | /// <em>another</em> thread's advance deadlocks, as that advance is waiting for this one. |
| | | 109 | | /// </para> |
| | | 110 | | /// </summary> |
| | | 111 | | /// <exception cref="ArgumentOutOfRangeException"><paramref name="target"/> is earlier than the current virtual time |
| | | 112 | | public void AdvanceTo(DateTimeOffset target) |
| | | 113 | | { |
| | 37350 | 114 | | target = target.ToUniversalTime(); |
| | | 115 | | |
| | | 116 | | // Monitor re-entrancy is the nesting rule above: the advancing thread's own callbacks |
| | | 117 | | // re-enter, every other thread queues behind the whole advance. |
| | 37350 | 118 | | lock (_advanceGate) |
| | | 119 | | { |
| | 37350 | 120 | | var validated = false; |
| | 16062 | 121 | | while (true) |
| | | 122 | | { |
| | | 123 | | VirtualTimer due; |
| | 53412 | 124 | | lock (_gate) |
| | | 125 | | { |
| | 53412 | 126 | | if (target < _utcNow) |
| | | 127 | | { |
| | | 128 | | // Only the caller's own request can be "backwards". Re-checked on every |
| | | 129 | | // iteration, this also threw out of a perfectly valid advance whose |
| | | 130 | | // callback had nested one past its target. |
| | 4 | 131 | | if (!validated) |
| | 2 | 132 | | throw new ArgumentOutOfRangeException(nameof(target), target, "Cannot advance virtual time b |
| | 2 | 133 | | return; |
| | | 134 | | } |
| | | 135 | | |
| | 53408 | 136 | | validated = true; |
| | 53408 | 137 | | var next = _armed.Count == 0 ? null : _armed.Min; |
| | 53408 | 138 | | if (next is null || next.DueAt > target) |
| | | 139 | | { |
| | 37346 | 140 | | _utcNow = target; |
| | 37346 | 141 | | return; |
| | | 142 | | } |
| | | 143 | | |
| | 16062 | 144 | | due = next; |
| | 16062 | 145 | | _armed.Remove(due); |
| | 16062 | 146 | | if (due.DueAt > _utcNow) |
| | 7030 | 147 | | _utcNow = due.DueAt; |
| | | 148 | | |
| | 16062 | 149 | | due.PrepareFire(_utcNow, out var rearmed); |
| | 16062 | 150 | | if (rearmed) |
| | 6 | 151 | | _armed.Add(due); |
| | 16062 | 152 | | } |
| | | 153 | | |
| | | 154 | | // Outside the state lock: the callback may read the clock, create timers, or re-arm this one. |
| | 16062 | 155 | | due.Invoke(); |
| | | 156 | | } |
| | | 157 | | } |
| | 37348 | 158 | | } |
| | | 159 | | |
| | | 160 | | internal void Schedule(VirtualTimer timer) |
| | | 161 | | { |
| | 0 | 162 | | lock (_gate) |
| | 0 | 163 | | _armed.Add(timer); |
| | 0 | 164 | | } |
| | | 165 | | |
| | | 166 | | internal void Unschedule(VirtualTimer timer) |
| | | 167 | | { |
| | 0 | 168 | | lock (_gate) |
| | 0 | 169 | | _armed.Remove(timer); |
| | 0 | 170 | | } |
| | | 171 | | |
| | | 172 | | internal long NextSequence() |
| | | 173 | | { |
| | 0 | 174 | | lock (_gate) |
| | 0 | 175 | | return _sequence++; |
| | 0 | 176 | | } |
| | | 177 | | |
| | | 178 | | /// <inheritdoc/> |
| | | 179 | | public override ITimer CreateTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period) |
| | | 180 | | { |
| | 18017 | 181 | | ArgumentNullException.ThrowIfNull(callback); |
| | 18015 | 182 | | var timer = new VirtualTimer(this, callback, state); |
| | 18015 | 183 | | timer.Change(dueTime, period); |
| | 17997 | 184 | | return timer; |
| | | 185 | | } |
| | | 186 | | |
| | | 187 | | /// <summary>A manually-driven timer; visible only through <see cref="ITimer"/>.</summary> |
| | 18015 | 188 | | internal sealed class VirtualTimer(VirtualTimeProvider _owner, TimerCallback _callback, object? _state) : ITimer |
| | | 189 | | { |
| | | 190 | | /// <summary><c>System.Threading.Timer</c>'s largest due time and period, in milliseconds (~49.7 days).</summary |
| | | 191 | | private const long MaxSupportedTimeoutMilliseconds = 0xFFFFFFFE; |
| | | 192 | | |
| | 912807 | 193 | | internal DateTimeOffset DueAt { get; private set; } |
| | 475773 | 194 | | internal long Sequence { get; private set; } |
| | 18015 | 195 | | private TimeSpan _period = Timeout.InfiniteTimeSpan; |
| | | 196 | | private bool _armed; |
| | | 197 | | private bool _disposed; |
| | | 198 | | |
| | | 199 | | public bool Change(TimeSpan dueTime, TimeSpan period) |
| | | 200 | | { |
| | | 201 | | // The system timer's own check, to the letter (TimeProvider.System.CreateTimer and its |
| | | 202 | | // ITimer.Change): whole milliseconds, truncated; -1 means "never"; 0xFFFFFFFE is the |
| | | 203 | | // ceiling; dueTime is judged first. The laxer check this replaces armed timers the |
| | | 204 | | // BCL rejects — a 50-day due time, any period past the ceiling — so code passed here |
| | | 205 | | // and threw ArgumentOutOfRangeException in production. |
| | 18067 | 206 | | var dueMilliseconds = (long)dueTime.TotalMilliseconds; |
| | 18067 | 207 | | ArgumentOutOfRangeException.ThrowIfLessThan(dueMilliseconds, -1, nameof(dueTime)); |
| | 18059 | 208 | | ArgumentOutOfRangeException.ThrowIfGreaterThan(dueMilliseconds, MaxSupportedTimeoutMilliseconds, nameof(dueT |
| | 18043 | 209 | | var periodMilliseconds = (long)period.TotalMilliseconds; |
| | 18043 | 210 | | ArgumentOutOfRangeException.ThrowIfLessThan(periodMilliseconds, -1, nameof(period)); |
| | 18039 | 211 | | ArgumentOutOfRangeException.ThrowIfGreaterThan(periodMilliseconds, MaxSupportedTimeoutMilliseconds, nameof(p |
| | | 212 | | |
| | 18031 | 213 | | lock (_owner._gate) |
| | | 214 | | { |
| | 18031 | 215 | | if (_disposed) |
| | 2 | 216 | | return false; |
| | | 217 | | |
| | 18029 | 218 | | if (_armed) |
| | | 219 | | { |
| | 4 | 220 | | _owner._armed.Remove(this); |
| | 4 | 221 | | _armed = false; |
| | | 222 | | } |
| | | 223 | | |
| | | 224 | | // Judged on the same truncated values as the check: a period of 0 or -1 whole |
| | | 225 | | // milliseconds is one-shot, a due time of -1 is "never", and a negative |
| | | 226 | | // sub-millisecond due time is 0 (due now) — never an instant before "now". |
| | 18029 | 227 | | _period = periodMilliseconds > 0 ? period : Timeout.InfiniteTimeSpan; |
| | 18029 | 228 | | if (dueMilliseconds == -1) |
| | 56 | 229 | | return true; |
| | | 230 | | |
| | 17973 | 231 | | DueAt = _owner._utcNow + (dueTime > TimeSpan.Zero ? dueTime : TimeSpan.Zero); |
| | 17973 | 232 | | Sequence = _owner._sequence++; |
| | 17973 | 233 | | _owner._armed.Add(this); |
| | 17973 | 234 | | _armed = true; |
| | 17973 | 235 | | return true; |
| | | 236 | | } |
| | 18031 | 237 | | } |
| | | 238 | | |
| | | 239 | | /// <summary>Called under the owner's gate just before firing: re-arms periodic timers.</summary> |
| | | 240 | | internal void PrepareFire(DateTimeOffset now, out bool rearmed) |
| | | 241 | | { |
| | 16062 | 242 | | if (_period > TimeSpan.Zero && _period != Timeout.InfiniteTimeSpan) |
| | | 243 | | { |
| | 6 | 244 | | DueAt = now + _period; |
| | 6 | 245 | | Sequence = _owner._sequence++; |
| | 6 | 246 | | rearmed = true; |
| | 6 | 247 | | return; |
| | | 248 | | } |
| | | 249 | | |
| | 16056 | 250 | | _armed = false; |
| | 16056 | 251 | | rearmed = false; |
| | 16056 | 252 | | } |
| | | 253 | | |
| | 16062 | 254 | | internal void Invoke() => _callback(_state); |
| | | 255 | | |
| | | 256 | | public void Dispose() |
| | | 257 | | { |
| | 17935 | 258 | | lock (_owner._gate) |
| | | 259 | | { |
| | 17935 | 260 | | if (_disposed) |
| | 0 | 261 | | return; |
| | | 262 | | |
| | 17935 | 263 | | _disposed = true; |
| | 17935 | 264 | | if (_armed) |
| | | 265 | | { |
| | 1853 | 266 | | _owner._armed.Remove(this); |
| | 1853 | 267 | | _armed = false; |
| | | 268 | | } |
| | 17935 | 269 | | } |
| | 17935 | 270 | | } |
| | | 271 | | |
| | | 272 | | public ValueTask DisposeAsync() |
| | | 273 | | { |
| | 88 | 274 | | Dispose(); |
| | 88 | 275 | | return ValueTask.CompletedTask; |
| | | 276 | | } |
| | | 277 | | } |
| | | 278 | | |
| | | 279 | | private sealed class VirtualTimerOrder : IComparer<VirtualTimer> |
| | | 280 | | { |
| | 2 | 281 | | public static readonly VirtualTimerOrder Instance = new(); |
| | | 282 | | |
| | | 283 | | public int Compare(VirtualTimer? x, VirtualTimer? y) |
| | | 284 | | { |
| | 261621 | 285 | | if (ReferenceEquals(x, y)) |
| | 17919 | 286 | | return 0; |
| | | 287 | | |
| | 243702 | 288 | | var byDue = x!.DueAt.CompareTo(y!.DueAt); |
| | 243702 | 289 | | return byDue != 0 ? byDue : x.Sequence.CompareTo(y.Sequence); |
| | | 290 | | } |
| | | 291 | | } |
| | | 292 | | } |