-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
fix(runners,tasks): prevent duplicate dispatch and preserve terminal task status #4039
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -191,9 +191,7 @@ func (t *TaskRunner) run() { | |
|
|
||
| defer func() { | ||
| if requeued { | ||
| // Task is being re-queued, don't mark as finished | ||
| log.Info("Task " + strconv.Itoa(t.Task.ID) + " re-queued (waiting for available runner)") | ||
| t.pool.queueEvents <- PoolEvent{EventTypeRequeued, t} | ||
| // EventTypeRequeued was sent synchronously before return. | ||
| return | ||
| } | ||
|
|
||
|
|
@@ -282,9 +280,16 @@ func (t *TaskRunner) run() { | |
|
|
||
| if err != nil { | ||
| if errors.Is(err, ErrAllRunnersBusy) { | ||
| // No runners available right now, put task back in waiting state | ||
| // No runners available right now, put task back in waiting state. | ||
| // Release running/active bookkeeping before enqueueing so a concurrent | ||
| // queue tick cannot ClaimAndDequeue the task while it is still in the | ||
| // running set (duplicate dispatch). Notify the pool synchronously so | ||
| // the current queue pass skips an immediate retry. | ||
| t.SetStatus(task_logger.TaskWaitingStatus) | ||
| t.pool.onTaskStop(t) | ||
| t.pool.state.Enqueue(t) | ||
| log.Info("Task " + strconv.Itoa(t.Task.ID) + " re-queued (waiting for available runner)") | ||
| t.pool.queueEvents <- PoolEvent{EventTypeRequeued, t} | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Requeue races queue sweepHigh Severity Requeue paths call Additional Locations (2)Reviewed by Cursor Bugbot for commit be92c89. Configure here. |
||
| requeued = true | ||
| return | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -200,6 +200,11 @@ func (p *TaskPool) failTaskRunnerLost(tsk *TaskRunner, runner *db.Runner, reason | |
| } | ||
|
|
||
| if tsk.Task.Status.IsFinished() { | ||
| // Another node (or the runner report on this node) already persisted a | ||
| // terminal status. Complete finalization instead of bailing: if we won | ||
| // the finalize lock over FinalizeRemoteTask, that path will not run and | ||
| // pool/Redis state (running set, claims, End, autorun) would leak. | ||
| p.finalizeRemoteTaskLocked(tsk, runner) | ||
| return | ||
| } | ||
|
|
||
|
|
@@ -269,6 +274,22 @@ func (p *TaskPool) requeueTaskRunnerOffline(tsk *TaskRunner, runnerID int, reaso | |
|
|
||
| tsk.Logf("Runner #%d lost the task: %s. Returning task to queue.", runnerID, reason) | ||
|
|
||
| // Re-check the DB immediately before mutating: another node may have | ||
| // received a concurrent "running" report while we held the finalize lock. | ||
| if util.HAEnabled() { | ||
| p.refreshTaskStatusFromDB(tsk) | ||
| if tsk.Task.Status != task_logger.TaskStartingStatus && | ||
| tsk.Task.Status != task_logger.TaskWaitingStatus { | ||
| return | ||
| } | ||
| if tsk.Task.RunnerID == nil || *tsk.Task.RunnerID != runnerID { | ||
| return | ||
| } | ||
| } | ||
|
|
||
| prevRunnerID := tsk.Task.RunnerID | ||
| prevStatus := tsk.Task.Status | ||
|
|
||
| tsk.Task.RunnerID = nil | ||
| tsk.SetStatus(task_logger.TaskWaitingStatus) | ||
|
|
||
|
|
@@ -279,12 +300,17 @@ func (p *TaskPool) requeueTaskRunnerOffline(tsk *TaskRunner, runnerID int, reaso | |
| "task_id": tsk.Task.ID, | ||
| "context": "runner_reconciler", | ||
| }).Error("failed to persist requeued task") | ||
| // Roll back in-memory changes so the next reconcile tick retries via | ||
| // the runner-liveness path instead of mis-routing as undispatched. | ||
| tsk.Task.RunnerID = prevRunnerID | ||
| tsk.Task.Status = prevStatus | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rollback ignores SetStatus persistenceMedium Severity When offline requeue clears Reviewed by Cursor Bugbot for commit be92c89. Configure here. |
||
| return | ||
| } | ||
|
|
||
| // Same flow as the ErrAllRunnersBusy requeue in TaskRunner.run: | ||
| // put the task back into the queue, then let the pool release its | ||
| // running/active bookkeeping (EventTypeRequeued -> onTaskStop). | ||
| // release running/active bookkeeping before enqueueing, then notify the | ||
| // pool so the current queue pass skips an immediate retry. | ||
| p.onTaskStop(tsk) | ||
| p.state.Enqueue(tsk) | ||
| p.queueEvents <- PoolEvent{EventTypeRequeued, tsk} | ||
| } | ||
|
|
@@ -339,8 +365,9 @@ func (p *TaskPool) requeueUndispatchedTask(tsk *TaskRunner) { | |
| return | ||
| } | ||
|
|
||
| // EventTypeRequeued -> onTaskStop releases the running/active bookkeeping and | ||
| // the claim, so the task can be re-claimed from the queue by any live node. | ||
| // Release running/active bookkeeping before enqueueing, then notify the | ||
| // pool so the task can be re-claimed from the queue by any live node. | ||
| p.onTaskStop(tsk) | ||
| p.state.Enqueue(tsk) | ||
| p.queueEvents <- PoolEvent{EventTypeRequeued, tsk} | ||
| } | ||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In HA, this cleanup removes the old claim before the task is visible in the shared queue, but the
EventTypeRequeuedsent just below is still handled byhandleQueuewith anotheronTaskStop. If another node claims the requeued task in the window afterEnqueueand before this node receives the event, that later cleanup can delete the new owner's claim/running record, leaving an actively running task without HA ownership and subject to duplicate/orphan recovery. Keep the cleanup in only one place, or make the requeue event skip cleanup after pre-cleaning.Useful? React with 👍 / 👎.