Skip to content

GH-6326: fix(executor): a held task is checked again between task runs, not only when the project queue is empty — reset heldThisCycle after each completed run, prune replaced rows, no orphan eviction of a Held or Queued row - #6339

Merged
alekspetrov merged 2 commits into
mainfrom
pilot/GH-6326
Nov 12, 2026

Conversation

@alekspetrov

Copy link
Copy Markdown
Collaborator

Summary

Automated PR created by Pilot for task GH-6326.

Closes #6326

Changes

GitHub Issue GH-6326: fix(executor): a held task is checked again between task runs, not only when the project queue is empty — reset heldThisCycle after each completed run, prune replaced rows, no orphan eviction of a Held or Queued row

Context

A held task is not checked again until the project queue is empty. On 2026-11-10 issue #6267 was held by the base-presence check; the issue body was fixed 12 seconds after the last hold; the task was not checked again for one hour, because another task of the project ran, and 2 hours after its start the stuck-task eviction stalled its queued row. A later log line "Held task skipped in favour of runnable task" still named the task from an old entry.

Cause, in internal/executor/dispatcher.go: processQueue of ProjectWorker creates the maps heldThisCycle (GH-5572) and transitionFailed (GH-6028) once per call. The worker is serial and runs each task inside the loop, so one call spans every runnable task of the queue, and a held row is skipped for the whole call. The maps are keyed by execution ID and never pruned. In internal/alerts/engine.go, evaluateStuckTasks evicts an entry older than the orphan threshold (floor 120 minutes) before the exemption for the Queued phase applies, and a repeated Held progress event does not refresh the clock, so stallOrphanedExecution stalls a row that only waits.

Scope

  1. In processQueue, after a task execution has completed and its result is stored (the end of the loop body, after the current task ID is cleared), empty heldThisCycle. A held row is then checked again once between two task runs. Do not empty it on the continue paths (held, superseded, failed transition), or the starvation of fix(dispatcher): a held task must not re-occupy the project worker every cycle and starve runnable tasks behind it #5572 returns.
  2. Do not empty transitionFailed on the continue paths either; empty it at the same place as item 1 only (fix(review): post-merge minors from PR #6027 for issue #6026 #6028: no busy loop on a row whose move to running failed).
  3. Before each pick, remove from heldThisCycle the execution IDs that are no longer among the queued rows of the project. Keep the limit passed to GetQueuedTasksForProject consistent with the size of the maps.
  4. The log line "Held task skipped in favour of runnable task" lists only tasks whose rows are still queued.
  5. In evaluateStuckTasks, do not orphan-evict an entry whose phase is Held or Queued: such a row waits by design, and the hold counter of the dispatcher already parks it at its cap. Keep the eviction for every other phase.
  6. Tests, in the existing test files of the two packages: a held row, a second runnable task, the hold cause removed while the second task runs -> the held row runs before the queue is empty; a held row never starves a runnable task (the existing test stays green); a replaced row's old ID is not in the log field held_task_ids; a Held entry older than the threshold is not evicted and its row is not stalled; a Running entry older than the threshold is still evicted.

Acceptance

  • go test -race -count=1 ./internal/executor/ ./internal/alerts/ -run 'ProcessQueue|BasePresence|StuckTask' passes; paste the output into the PR body.
  • Remove the reset of item 1 -> the test "held row runs before the queue is empty" fails.
  • Remove the exemption of item 5 -> the Held eviction test fails.
  • Do not re-run make build, make test or make lint as evidence.

Out of scope

The base-presence check itself and its hold counter. A parallel project worker.

Refs

Not verified

  • go test -race -count=1 ./internal/executor/ ./internal/alerts/ -run 'ProcessQueue|BasePresence|StuckTask' passes; paste the output into the PR body.
    Reason: Not verified: shell operators are not allowed in evidence commands

  • Remove the reset of item 1 -> the test "held row runs before the queue is empty" fails.
    Reason: freeform mutation, run manually

  • Remove the exemption of item 5 -> the Held eviction test fails.
    Reason: freeform mutation, run manually

Pre-PR review

Mode: enforce
Verdict: APPROVE-WITH-NOTES
Fix: none

Defects:

  • [minor] at internal/executor/dispatcher.go:145 — The config doc on BasePresenceHoldMaxCycles is now stale. It still says a held task "is retried only when the project's worker is signalled again (a new poll tick, a new task queued…) not on a tight timer". After the clear at dispatcher.go:4440 a held row is also re-checked after every completed run, so each run adds one to the hold count. The issue asks for this behaviour, but the sentence and the in-loop comments at dispatcher.go:3837-3842 and 4159-4166 ("remember it for the rest of this cycle") were not updated.
  • [minor] at internal/alerts/engine.go:1669 — The exemption is placed before both the orphan eviction and the task_stuck alert path. Held entries therefore no longer fire task_stuck alerts at all. Before the diff, a Held entry between 10m and 120m reached e.fireAlert (engine.go:1720), because only Queued was exempted from alerting (engine.go:1701). Scope item 5 asks only to stop orphan eviction, so silencing the alert is a behaviour change nobody asked for.
  • [minor] at internal/executor/dispatcher.go:3859 — Pruning runs only when len(tasks) < limit. When the window is full, stale IDs stay in heldThisCycle and still raise limit at line 3843. Scope item 3 says to prune "before each pick". The clear after each run (line 4440) bounds the effect, and the log filter at line 3891 still keeps item 4.
  • [minor] at internal/executor/dispatcher_test.go:7206 — Scope item 6 says the existing starvation test "stays green", but its assertions were rewritten from 1→3 and 2→4. The base_presence_test.go:411/567 thresholds also changed from 2 to 3. The starvation guard itself (execCount != 2) is kept, and the new counts follow directly from item 1.

Not-verified:

  • Acceptance bullet: pasted go test -race … -run 'ProcessQueue|BasePresence|StuckTask' output. Not traced, because the PR body is not in the checkout.
  • Acceptance mutation checks (removing the reset or the exemption makes the tests fail). Not traced by running anything; this checkout is read-only, so I checked them by reading the test logic only.
  • none further
  • … 2 more not-verified items omitted (capped at 3)

@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 90.47619% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/executor/dispatcher.go 89.47% 1 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

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

Labels

None yet

Projects

None yet

2 participants