Skip to content

fix(runners,tasks): critical runner requeue and finalization bugs#4002

Closed
cursor[bot] wants to merge 2 commits into
developfrom
cursor/critical-bug-investigation-b838
Closed

fix(runners,tasks): critical runner requeue and finalization bugs#4002
cursor[bot] wants to merge 2 commits into
developfrom
cursor/critical-bug-investigation-b838

Conversation

@cursor

@cursor cursor Bot commented Jul 2, 2026

Copy link
Copy Markdown

Bug and impact

Three critical correctness bugs in the runner/task lifecycle on develop:

  1. Duplicate dispatch on ErrAllRunnersBusy — When all runners are busy, tasks were re-enqueued while still in the running set. A concurrent queue tick could ClaimAndDequeue the same task and start a second dispatch, causing duplicate Ansible/Terraform execution.

  2. Stopped → failed on runner client — When the server emergency-stops a job via terminated_jobs, applyTerminatedJobs sets status to stopped, but the subsequent Run() error path overwrote it to failed.

  3. HA pool state leak in reconciler — When failTaskRunnerLost won the finalize lock but the DB already had a terminal status, it returned early without calling finalizeRemoteTaskLocked, leaking running/active Redis state and skipping autorun children.

Root cause

  • Requeue paths (ErrAllRunnersBusy, offline runner, undispatched task) enqueued before releasing running/active bookkeeping, creating a race window.
  • Runner job_pool.go unconditionally set failed on any Run() error without checking if status was already terminal.
  • Reconciler treated "already finished in DB" as a no-op instead of completing cleanup.

Fix and validation

  • Release running/active state via onTaskStop before enqueueing; notify pool synchronously with EventTypeRequeued.
  • Add runningJob.finalizeAfterRun() to preserve already-finished status.
  • Call finalizeRemoteTaskLocked when reconciler observes terminal DB status; harden offline requeue with DB re-check and rollback on persist failure.
  • Move End != nil guard in finalizeRemoteTaskLocked outside HA-only block.

Tests

  • go test ./services/tasks/... ./services/runners/... — all pass
  • New regression tests for each bug path
Open in Web View Automation 

cursoragent and others added 2 commits July 2, 2026 11:04
When every runner is at capacity, ErrAllRunnersBusy requeued tasks by
enqueueing them while they still sat in the running set, then sending
EventTypeRequeued from a defer. A periodic queue tick could
ClaimAndDequeue the task in that window and start a second dispatch on
the same TaskRunner.

Release running/active bookkeeping before enqueueing and notify the pool
synchronously, matching the reconciler requeue paths.

Co-authored-by: Denis Gukov <fiftin@outlook.com>
…finalization

Runner client: when applyTerminatedJobs emergency-stops a job and Run()
unwinds with an error, the error path overwrote stopped with failed.
Add finalizeAfterRun that respects an already-finished status.

HA reconciler: when failTaskRunnerLost wins the finalize lock but the DB
already has a terminal status, complete finalization instead of bailing
so pool/Redis state and autorun children are not leaked. Harden offline
requeue with a pre-mutation DB re-check and rollback on persist failure.

Co-authored-by: Denis Gukov <fiftin@outlook.com>
@fiftin fiftin closed this Jul 8, 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