Skip to content

Stop() called while a RestartAsync() is still pending is ignored, so the action keeps running after the caller stopped it #57

Description

@matt-edmondson

What's wrong

IntervalAction/IntervalAction.cs (around lines 149-179), RestartCoreAsync:

  1. It awaits the previous restart (WaitAndDiscardOutcomeAsync(previousRestart)).
  2. If polling was active, it calls Stop() and awaits the old PollingTask.
  3. 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.

Activity

  1. matt-edmondson commented on Sep 27, 2026

    @matt-edmondson
    ContributorAuthor

    Triage


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions