Skip to content

fix: make lifecycle calls on a destroyed task safe no-ops#600

Merged
merencia merged 2 commits into
mainfrom
fix/lifecycle-after-destroy
Jul 2, 2026
Merged

fix: make lifecycle calls on a destroyed task safe no-ops#600
merencia merged 2 commits into
mainfrom
fix/lifecycle-after-destroy

Conversation

@merencia

@merencia merencia commented Jul 2, 2026

Copy link
Copy Markdown
Member

Summary

Two related bugs where calling a lifecycle method after destroy() did the wrong thing instead of being a safe no-op:

  • Inline tasks: task.destroy(); task.start(); re-armed the runner's heartbeat before the state machine rejected the transition. The timer never died, and every scheduled slot logged an "invalid transition from destroyed to running" error forever. Since the task is already removed from the registry at that point, cron.shutdown() cannot reach it either.
  • Background tasks: await task.destroy(); await task.stop(); rejected with "invalid transition from destroyed to stopped", because stop() emits task:stopped even without a fork process, and the constructor's handler for that event unconditionally tried to move the state machine out of destroyed. Since cron.shutdown() calls stop() on every registered task, a manual destroy() followed by shutdown() could hit this. Background start() after destroy() also re-forked a fresh daemon for an already torn-down task.

Both start() and stop() are now safe no-ops on a destroyed task (inline and background), matching the inline stop() behavior that was already correct. destroy() itself was already idempotent, so no change was needed there.

merencia added 2 commits July 2, 2026 13:14
Calling start() right after destroy() re-armed the runner's heartbeat
before the state machine rejected the transition, so the timer kept
firing forever and every slot logged an invalid-transition error. The
task is already out of the registry at that point, so shutdown() can't
reach it either. start() now returns early on a destroyed task instead
of touching the runner.
destroy() followed by stop() rejected with "invalid transition from
destroyed to stopped": stop() emits task:stopped even without a fork
process, and the constructor's handler for that event unconditionally
tried to move the state machine out of destroyed. start() had the same
gap and would fork a fresh daemon for a task that was already torn
down. Both now resolve immediately without touching the fork, and the
task:stopped handler no longer attempts a transition once destroyed.
@merencia
merencia merged commit 7fa9795 into main Jul 2, 2026
9 checks passed
@merencia
merencia deleted the fix/lifecycle-after-destroy branch July 2, 2026 16:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant