What's wrong
The polling loop decides whether to run with this check (IntervalAction/IntervalAction.cs:228):
DateTimeOffset.Now - lastRunTime > ActionInterval
LastRunTime is set from DateTimeOffset.Now (lines 236 and 246). Wall-clock time is not monotonic: NTP corrections, manual clock changes and VM resume can all move it backwards.
Failure scenario
ActionInterval = 1s, and the action has just run.
- The system clock is set back 1 hour.
Now - LastRunTime is now about −1h, which is never > 1s. The action does not run for about an hour, and nothing is thrown or logged.
A forward jump has the opposite effect: one run happens early. DST changes are not affected, because DateTimeOffset compares UTC instants; only real system clock changes are.
Why it matters
The library exists to run an action every N seconds, for example polling, heartbeats or autosave. A long silent gap after a clock sync is a hard-to-diagnose outage.
Suggested fix
Time intervals with a monotonic source: store Stopwatch.GetTimestamp() (or Environment.TickCount64) for the last run and compare elapsed ticks against ActionInterval. LastRunTime can remain as a wall-clock value for display if it is part of the public surface. It just shouldn't drive scheduling.
Acceptance criteria
The scheduling decision no longer depends on DateTimeOffset.Now. A test that injects a clock (or the monotonic abstraction, e.g. TimeProvider) shows that a backwards wall-clock jump does not delay the next run.
What's wrong
The polling loop decides whether to run with this check (
IntervalAction/IntervalAction.cs:228):LastRunTimeis set fromDateTimeOffset.Now(lines 236 and 246). Wall-clock time is not monotonic: NTP corrections, manual clock changes and VM resume can all move it backwards.Failure scenario
ActionInterval = 1s, and the action has just run.Now - LastRunTimeis now about −1h, which is never> 1s. The action does not run for about an hour, and nothing is thrown or logged.A forward jump has the opposite effect: one run happens early. DST changes are not affected, because
DateTimeOffsetcompares UTC instants; only real system clock changes are.Why it matters
The library exists to run an action every N seconds, for example polling, heartbeats or autosave. A long silent gap after a clock sync is a hard-to-diagnose outage.
Suggested fix
Time intervals with a monotonic source: store
Stopwatch.GetTimestamp()(orEnvironment.TickCount64) for the last run and compare elapsed ticks againstActionInterval.LastRunTimecan remain as a wall-clock value for display if it is part of the public surface. It just shouldn't drive scheduling.Acceptance criteria
The scheduling decision no longer depends on
DateTimeOffset.Now. A test that injects a clock (or the monotonic abstraction, e.g.TimeProvider) shows that a backwards wall-clock jump does not delay the next run.