< Summary - AsyncResponse (Release / net8.0+net10.0 / unit+integration)

Information
Class: AsyncResponse.Testing.VirtualTimeProvider
Assembly: AsyncResponse.Testing
File(s): /_/src/AsyncResponse.Testing/VirtualTimeProvider.cs
Line coverage
90%
Covered lines: 100
Uncovered lines: 10
Coverable lines: 110
Total lines: 292
Line coverage: 90.9%
Branch coverage
97%
Covered branches: 37
Total branches: 38
Branch coverage: 97.3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
.ctor(...)100%11100%
.ctor()100%11100%
GetUtcNow()100%11100%
get_LocalTimeZone()100%11100%
get_TimestampFrequency()100%11100%
GetTimestamp()100%11100%
get_NextTimerDueAt()100%22100%
Advance(...)100%11100%
AdvanceTo(...)100%1414100%
Schedule(...)100%210%
Unschedule(...)100%210%
NextSequence()100%210%
CreateTimer(...)100%11100%
.ctor(...)100%11100%
get_DueAt()100%11100%
get_Sequence()100%11100%
Change(...)100%1010100%
PrepareFire(...)100%44100%
Invoke()100%11100%
Dispose()75%4488.88%
DisposeAsync()100%11100%
.cctor()100%11100%
Compare(...)100%44100%

File(s)

/_/src/AsyncResponse.Testing/VirtualTimeProvider.cs

#LineLine coverage
 1namespace 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>
 29public sealed class VirtualTimeProvider : TimeProvider
 30{
 31    /// <summary>The default virtual epoch: a fixed instant, so tests never depend on the wall clock.</summary>
 232    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.
 44636    private readonly object _gate = new();
 44637    private readonly object _advanceGate = new();
 44638    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()
 25244        : this(DefaultStartTime)
 45    {
 25246    }
 47
 48    /// <summary>Creates a provider starting at <paramref name="startTime"/>.</summary>
 44649    public VirtualTimeProvider(DateTimeOffset startTime)
 44650        => _utcNow = startTime.ToUniversalTime();
 51
 52    /// <inheritdoc/>
 53    public override DateTimeOffset GetUtcNow()
 54    {
 8487355        lock (_gate)
 8487356            return _utcNow;
 8487357    }
 58
 59    /// <inheritdoc/>
 260    public override TimeZoneInfo LocalTimeZone => TimeZoneInfo.Utc;
 61
 62    /// <inheritdoc/>
 1463    public override long TimestampFrequency => TimeSpan.TicksPerSecond;
 64
 65    /// <inheritdoc/>
 66    public override long GetTimestamp()
 67    {
 4068        lock (_gate)
 4069            return _utcNow.UtcTicks;
 4070    }
 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        {
 36306977            lock (_gate)
 36306978                return _armed.Count == 0 ? null : _armed.Min!.DueAt;
 36306979        }
 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    {
 3347688        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".
 3347493        lock (_advanceGate)
 3347494            AdvanceTo(GetUtcNow() + delta);
 3347495    }
 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    {
 37350114        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.
 37350118        lock (_advanceGate)
 119        {
 37350120            var validated = false;
 16062121            while (true)
 122            {
 123                VirtualTimer due;
 53412124                lock (_gate)
 125                {
 53412126                    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.
 4131                        if (!validated)
 2132                            throw new ArgumentOutOfRangeException(nameof(target), target, "Cannot advance virtual time b
 2133                        return;
 134                    }
 135
 53408136                    validated = true;
 53408137                    var next = _armed.Count == 0 ? null : _armed.Min;
 53408138                    if (next is null || next.DueAt > target)
 139                    {
 37346140                        _utcNow = target;
 37346141                        return;
 142                    }
 143
 16062144                    due = next;
 16062145                    _armed.Remove(due);
 16062146                    if (due.DueAt > _utcNow)
 7030147                        _utcNow = due.DueAt;
 148
 16062149                    due.PrepareFire(_utcNow, out var rearmed);
 16062150                    if (rearmed)
 6151                        _armed.Add(due);
 16062152                }
 153
 154                // Outside the state lock: the callback may read the clock, create timers, or re-arm this one.
 16062155                due.Invoke();
 156            }
 157        }
 37348158    }
 159
 160    internal void Schedule(VirtualTimer timer)
 161    {
 0162        lock (_gate)
 0163            _armed.Add(timer);
 0164    }
 165
 166    internal void Unschedule(VirtualTimer timer)
 167    {
 0168        lock (_gate)
 0169            _armed.Remove(timer);
 0170    }
 171
 172    internal long NextSequence()
 173    {
 0174        lock (_gate)
 0175            return _sequence++;
 0176    }
 177
 178    /// <inheritdoc/>
 179    public override ITimer CreateTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period)
 180    {
 18017181        ArgumentNullException.ThrowIfNull(callback);
 18015182        var timer = new VirtualTimer(this, callback, state);
 18015183        timer.Change(dueTime, period);
 17997184        return timer;
 185    }
 186
 187    /// <summary>A manually-driven timer; visible only through <see cref="ITimer"/>.</summary>
 18015188    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
 912807193        internal DateTimeOffset DueAt { get; private set; }
 475773194        internal long Sequence { get; private set; }
 18015195        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.
 18067206            var dueMilliseconds = (long)dueTime.TotalMilliseconds;
 18067207            ArgumentOutOfRangeException.ThrowIfLessThan(dueMilliseconds, -1, nameof(dueTime));
 18059208            ArgumentOutOfRangeException.ThrowIfGreaterThan(dueMilliseconds, MaxSupportedTimeoutMilliseconds, nameof(dueT
 18043209            var periodMilliseconds = (long)period.TotalMilliseconds;
 18043210            ArgumentOutOfRangeException.ThrowIfLessThan(periodMilliseconds, -1, nameof(period));
 18039211            ArgumentOutOfRangeException.ThrowIfGreaterThan(periodMilliseconds, MaxSupportedTimeoutMilliseconds, nameof(p
 212
 18031213            lock (_owner._gate)
 214            {
 18031215                if (_disposed)
 2216                    return false;
 217
 18029218                if (_armed)
 219                {
 4220                    _owner._armed.Remove(this);
 4221                    _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".
 18029227                _period = periodMilliseconds > 0 ? period : Timeout.InfiniteTimeSpan;
 18029228                if (dueMilliseconds == -1)
 56229                    return true;
 230
 17973231                DueAt = _owner._utcNow + (dueTime > TimeSpan.Zero ? dueTime : TimeSpan.Zero);
 17973232                Sequence = _owner._sequence++;
 17973233                _owner._armed.Add(this);
 17973234                _armed = true;
 17973235                return true;
 236            }
 18031237        }
 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        {
 16062242            if (_period > TimeSpan.Zero && _period != Timeout.InfiniteTimeSpan)
 243            {
 6244                DueAt = now + _period;
 6245                Sequence = _owner._sequence++;
 6246                rearmed = true;
 6247                return;
 248            }
 249
 16056250            _armed = false;
 16056251            rearmed = false;
 16056252        }
 253
 16062254        internal void Invoke() => _callback(_state);
 255
 256        public void Dispose()
 257        {
 17935258            lock (_owner._gate)
 259            {
 17935260                if (_disposed)
 0261                    return;
 262
 17935263                _disposed = true;
 17935264                if (_armed)
 265                {
 1853266                    _owner._armed.Remove(this);
 1853267                    _armed = false;
 268                }
 17935269            }
 17935270        }
 271
 272        public ValueTask DisposeAsync()
 273        {
 88274            Dispose();
 88275            return ValueTask.CompletedTask;
 276        }
 277    }
 278
 279    private sealed class VirtualTimerOrder : IComparer<VirtualTimer>
 280    {
 2281        public static readonly VirtualTimerOrder Instance = new();
 282
 283        public int Compare(VirtualTimer? x, VirtualTimer? y)
 284        {
 261621285            if (ReferenceEquals(x, y))
 17919286                return 0;
 287
 243702288            var byDue = x!.DueAt.CompareTo(y!.DueAt);
 243702289            return byDue != 0 ? byDue : x.Sequence.CompareTo(y.Sequence);
 290        }
 291    }
 292}