You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
A sub-millisecond PollingInterval still spins a core, and one over ~24.8 days faults the loop after the first run #65
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) or TimeSpan.FromMicroseconds(500), becomes Task.Delay(0). That returns a task that has already completed, so the await continues synchronously and the while loop 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.MaxValue ms (about 24.8 days) on netstandard2.0/2.1 and uint.MaxValue - 1 ms (about 49.7 days) on .NET 5 and later. A larger PollingInterval passes Start() and the action runs once. After that, Task.Delay throws ArgumentOutOfRangeException, the polling task faults, and the action never runs again.
Repro (net10.0)
staticdoubleCpuFor(TimeSpanpolling){varp=Process.GetCurrentProcess();p.Refresh();varbefore=p.TotalProcessorTime;varia=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 sConsole.WriteLine(CpuFor(TimeSpan.FromMicroseconds(500)));// ~851 ms CPU: busy loopConsole.WriteLine(CpuFor(TimeSpan.FromTicks(1)));// ~830 ms CPU: busy loopConsole.WriteLine(Task.Delay(TimeSpan.FromTicks(1)).IsCompleted);// Trueintruns=0;varbig=IntervalAction.Start(new(){Action=()=>Interlocked.Increment(refruns),PollingInterval=TimeSpan.FromDays(60)});Thread.Sleep(500);Console.WriteLine(runs);// 1big.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 same ArgumentOutOfRangeException used 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) and TimeSpan.FromDays(60), and update the comment above the check.
Related but distinct: #64 (Restart blocks for a full PollingInterval).
Notes: Rejecting values outside [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.
What's wrong
Start()rejectsPollingInterval <= 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 callsawait Task.Delay(PollingInterval)(IntervalAction.cs:201), andTask.Delay(TimeSpan)has two limits the check doesn't cover: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).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)
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 callRethrowExceptions().Suggested fix
In
Start(), validate against the rangeTask.Delaycan actually honor on every target framework:PollingInterval < TimeSpan.FromMilliseconds(1)with the sameArgumentOutOfRangeExceptionused for zero, or round it up to 1 ms.PollingInterval.TotalMilliseconds > int.MaxValue, which is the limit common to all targets including netstandard, or clamp it and loop the delay.TimeSpan.FromTicks(1)andTimeSpan.FromDays(60), and update the comment above the check.Related but distinct: #64 (Restart blocks for a full
PollingInterval).