What's wrong
IntervalAction/IntervalAction.cs (around lines 149-179), RestartCoreAsync:
- It awaits the previous restart (
WaitAndDiscardOutcomeAsync(previousRestart)).
- If polling was active, it calls
Stop() and awaits the old PollingTask.
- Then it runs
lock (Lock) { ShouldPoll = true; … } unconditionally and starts a new polling loop.
If the caller calls Stop() while either await is pending, its ShouldPoll = false is simply overwritten in step 3. The instance ends up running even though the caller's most recent call was Stop(). The caller has no way to prevent this: Restart/RestartAsync take no cancellation token, and nothing records that a stop happened in the meantime.
Failure scenario
Setup: PollingInterval = 200 ms, ActionInterval = 0.
ia.Start();
await Task.Delay(50);
Task r = ia.RestartAsync();
ia.Stop(); // caller wants it stopped
await r;
await Task.Delay(300);
// count action runs over the next second
- Actual:
ShouldPoll == true, and the action ran 5 times during the second after Stop().
- Expected: 0 runs.
This happens in real use. A UI "Restart" click followed quickly by app shutdown, or a Dispose that calls Stop(), leaves a loop running against disposed state.
Related but different: #52 covers the opposite order (Stop() then Restart() leaving two loops alive). I applied the RestartCoreAsync change from open PR #55 and ran the same repro: it still fails with the same numbers. This needs its own fix.
Suggested fix / acceptance criteria
- Keep a stop/restart generation counter under
Lock:
Stop() increments it.
RestartAsync captures the value it expects when it is called.
RestartCoreAsync sets ShouldPoll = true and starts the loop only if no Stop() has happened since.
- Alternatively, have
Stop() cancel pending restarts through a CancellationTokenSource that RestartCoreAsync observes.
- Add a test:
RestartAsync(); Stop(); await restart; leaves ShouldPoll == false and no action runs afterwards.
What's wrong
IntervalAction/IntervalAction.cs(around lines 149-179),RestartCoreAsync:WaitAndDiscardOutcomeAsync(previousRestart)).Stop()and awaits the oldPollingTask.lock (Lock) { ShouldPoll = true; … }unconditionally and starts a new polling loop.If the caller calls
Stop()while either await is pending, itsShouldPoll = falseis simply overwritten in step 3. The instance ends up running even though the caller's most recent call wasStop(). The caller has no way to prevent this:Restart/RestartAsynctake no cancellation token, and nothing records that a stop happened in the meantime.Failure scenario
Setup:
PollingInterval = 200 ms,ActionInterval = 0.ShouldPoll == true, and the action ran 5 times during the second afterStop().This happens in real use. A UI "Restart" click followed quickly by app shutdown, or a Dispose that calls
Stop(), leaves a loop running against disposed state.Related but different: #52 covers the opposite order (
Stop()thenRestart()leaving two loops alive). I applied theRestartCoreAsyncchange from open PR #55 and ran the same repro: it still fails with the same numbers. This needs its own fix.Suggested fix / acceptance criteria
Lock:Stop()increments it.RestartAsynccaptures the value it expects when it is called.RestartCoreAsyncsetsShouldPoll = trueand starts the loop only if noStop()has happened since.Stop()cancel pending restarts through aCancellationTokenSourcethatRestartCoreAsyncobserves.RestartAsync(); Stop(); await restart;leavesShouldPoll == falseand no action runs afterwards.