Skip to content

fix(daemon): keep one timer per worker type - #4412

Merged
ruvnet merged 1 commit into
ruvnet:mainfrom
Tong-bit-art:fix/worker-daemon-duplicate-timers
Oct 23, 2026
Merged

ruvnet merged 1 commit into
ruvnet:mainfrom
Tong-bit-art:fix/worker-daemon-duplicate-timers

Conversation

@Tong-bit-art

Copy link
Copy Markdown
Contributor

Problem

The automation scan in #4410 found a timer-ownership bug in WorkerDaemon:

  • setWorkerEnabled(type, true) called scheduleWorker() even when a timer for that type already existed.
  • this.timers keeps only one handle per type, so the previous timeout leaked.
  • The leaked timer survived stop() because stop() only clears handles currently in the map.
  • setWorkerEnabled(type, false) removed the current timer but left the type in pendingWorkers, so a deferred run could still execute.
  • A run already in flight rescheduled itself after completion even if the worker had been disabled meanwhile.

Net effect: a redundant enable can produce two concurrent runs of one worker type, and a disabled/stopped worker can fire once more untracked.

Change

  • scheduleWorker() clears and removes any existing timer for the type before scheduling a new one.
  • setWorkerEnabled(true) does nothing when a live timer or an in-flight run already owns that type.
  • setWorkerEnabled(false) also removes the type from pendingWorkers.
  • The post-run reschedule is gated on this.running && workerConfig.enabled, so disabling during a run cannot resurrect it.

Tests

New worker-daemon-duplicate-timers-4410.test.ts uses fake timers:

  • three consecutive setWorkerEnabled('map', true) calls leave 1 pending timer;
  • disabling a queued worker removes it from pendingWorkers.

Baseline (source stashed, --no-cache): both tests fail — 3 timers after three enables, and the disabled worker remains queued. Candidate: 2/2 passed.

No changes to worker intervals, scheduling order, or worker business logic.

Compatibility

  • Repeated enable is now idempotent instead of creating duplicate timer chains.
  • A worker that is already running or scheduled keeps its existing run; disabling cancels future runs.
  • No public API change.

setWorkerEnabled(type, true) called scheduleWorker() without clearing an existing timeout. this.timers kept only the newest handle, so each redundant enable leaked the previous timer; the leaked timer survived stop() (which only clears current map entries) and could run the worker again untracked. Disabling also left the type in pendingWorkers, and an in-flight run rescheduled itself even after disable (ruvnet#4410 automation scan).\n\nClear any existing timer at schedule time, avoid scheduling when a timer or in-flight run already owns the type, remove disabled types from the pending queue, and only reschedule after a run while the worker is still enabled. Add fake-timer regressions; baseline leaks three timers after three enables and keeps the disabled pending worker.
@Tong-bit-art
Tong-bit-art requested a review from ruvnet as a code owner October 23, 2026 07:59
@ruvnet
ruvnet merged commit e0bc9b3 into ruvnet:main Oct 23, 2026
178 of 179 checks passed
@ruvnet ruvnet mentioned this pull request Oct 23, 2026
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.

2 participants