Repository navigation
Description
What's wrong
Start() rejects PollingInterval <= TimeSpan.Zero (IntervalAction/IntervalAction.cs:110), and the comment above that check says zero is rejected because it "would spin a core". The polling loop then calls await Task.Delay(PollingInterval) (IntervalAction.cs:201), and Task.Delay(TimeSpan) has two limits the check doesn't cover:
- It truncates to whole milliseconds. A positive interval under 1 ms, such as
TimeSpan.FromTicks(1)orTimeSpan.FromMicroseconds(500), becomesTask.Delay(0). That returns a task that has already completed, so theawaitcontinues synchronously and thewhileloop never yields. This is the same core-spinning case that the zero check was added to prevent (cc6aaad, Non-positive or infinite PollingInterval silently kills the loop or makes Restart()/Stop() hang forever #53). - It has a maximum. That maximum is
int.MaxValuems (about 24.8 days) on netstandard2.0/2.1 anduint.MaxValue - 1ms (about 49.7 days) on .NET 5 and later. A largerPollingIntervalpassesStart()and the action runs once. After that,Task.DelaythrowsArgumentOutOfRangeException, the polling task faults, and the action never runs again.
Repro (net10.0)
static double CpuFor(TimeSpan polling) {
var p = Process.GetCurrentProcess(); p.Refresh(); var before = p.TotalProcessorTime;
var ia = IntervalAction.Start(new() { Action = () => { }, ActionInterval = TimeSpan.FromHours(1), PollingInterval = polling });
Thread.Sleep(2000); ia.Stop(); Thread.Sleep(100); p.Refresh();
return (p.TotalProcessorTime - before).TotalMilliseconds;
}
Console.WriteLine(CpuFor(TimeSpan.FromMilliseconds(1))); // ~223 ms CPU over 2 s
Console.WriteLine(CpuFor(TimeSpan.FromMicroseconds(500))); // ~851 ms CPU: busy loop
Console.WriteLine(CpuFor(TimeSpan.FromTicks(1))); // ~830 ms CPU: busy loop
Console.WriteLine(Task.Delay(TimeSpan.FromTicks(1)).IsCompleted); // True
int runs = 0;
var big = IntervalAction.Start(new() { Action = () => Interlocked.Increment(ref runs), PollingInterval = TimeSpan.FromDays(60) });
Thread.Sleep(500);
Console.WriteLine(runs); // 1
big.RethrowExceptions(); // ArgumentOutOfRangeException (Parameter 'delay')For comparison, a bare while (true) loop in the same throttled sandbox used about 1250 ms of CPU over 2 s.
Why it matters
A caller who asks for a fine-grained interval, for example FromMicroseconds(100) for a tight poll, gets a hot loop instead of an error. A caller who asks for a long interval, for example a monthly check, gets exactly one run and then silence, and only finds out if they call RethrowExceptions().
Suggested fix
In Start(), validate against the range Task.Delay can actually honor on every target framework:
- Reject
PollingInterval < TimeSpan.FromMilliseconds(1)with the sameArgumentOutOfRangeExceptionused for zero, or round it up to 1 ms. - Reject
PollingInterval.TotalMilliseconds > int.MaxValue, which is the limit common to all targets including netstandard, or clamp it and loop the delay. - Add tests for
TimeSpan.FromTicks(1)andTimeSpan.FromDays(60), and update the comment above the check.
Related but distinct: #64 (Restart blocks for a full PollingInterval).
Activity
matt-edmondson commentedon Sep 27, 2026
Triage
RethrowExceptions(). Both need an unusual configuration.PollingIntervalvalidation inIntervalAction.Start()PollingInterval) and Setting the system clock backwards silently stops the action for that long (interval timed with wall-clock DateTimeOffset.Now) #54 (wall-clock timing), which are in the same polling loop. Open PRs Keep the action's stack trace when RethrowExceptions rethrows [patch] #62 (RethrowExceptions() throws the action's exception with its stack trace wiped, so the trace points at RethrowExceptions instead of the code that failed #58) and Let a Stop() during a pending RestartAsync() win [patch] #63 (Stop() called while a RestartAsync() is still pending is ignored, so the action keeps running after the caller stopped it #57) don't touch interval validation.[1 ms, int.MaxValue ms]is the simplest fix and consistent with the existing zero check. Clamping and looping the delay would support very long intervals if that is ever wanted.Generated by Claude Code
Reject polling intervals Task.Delay cannot honor [patch]