fix(ci-e2e): budget queueing apart from running, and stop swallowing failed cancels - #1449
Open
zoroyihan7 wants to merge 2 commits into
Open
zoroyihan7 wants to merge 2 commits into
zoroyihan7 wants to merge 2 commits into
Conversation
…failed cancels A single poll counter covered both waiting for a GPU and running the optimizer, so a saturated pool spent the run budget on the queue and then reported the result as "not terminal after 13200s" — a message that reads as a hang in the tested commit. Run 34190618492 spent 166 of its 220 polls in phase=Queued and was failed 54 minutes into a workload that went on to finish successfully 115 minutes later (k8s Job ci-pr-1414-34190618492-a7f21d57-a1: succeeded=1, 171m, crash_count 0). Queueing and running now carry separate budgets and separate verdicts, in both the ci-e2e and forge-e2e dispatchers, so a red check says whether the commit was ever given a GPU. A queue timeout cancels unconditionally: there is no on-cluster state to keep for triage when nothing was ever dispatched, and the workload would otherwise take a GPU for a full run once a slot freed, with the GitHub run already over. The cancel path hid the reason the pool was saturated. cleanup() discarded the DELETE response entirely, and the response is a 40x: the workload survives, the backend dispatches it hours after its GitHub run ended, and it holds a GPU slot for a full run nobody is watching. Of 93 jobs dispatched between 2026-09-07 and 2026-09-08, 49 started after their run had already finished, taking 96 of 204 GPU-slot-hours; at one point all eight GPUs in the pool were held by workloads whose run had ended. cleanup() now checks the status code, retries once, and reports an unreclaimed workload. Both scripts are bounded so the cancel can outlive neither the runner's grace period nor a hanging backend: --connect-timeout/--max-time on the DELETE and on post_status, a shorter retry, and no retry at all from the signal trap, which now disarms itself so a SIGTERM arriving mid-handler cannot re-enter it. Measured against a stubbed hanging backend, the cancel path costs 6.4s wall inside the ~10s SIGINT-to-SIGKILL window. jq exits 0 printing nothing on empty input, so a poll whose curl failed left the phase empty rather than Unknown; it fell through the dispatched latch and pinned the run budget on a workload still waiting for a GPU — the very reading the split budgets exist to prevent, on the first transient read of the several hundred a run makes. This makes the leak visible; it does not stop it. The DELETE has to start succeeding, which is a backend change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The orchestration facade answers 202 when it settled the row but nothing confirmed the workload was stopped, and cleanup() accepted any 2xx as reclaimed. That is the exact reading the backend change exists to remove: a workload that may still hold its GPUs reported as a clean cancel. 202 is not retried — the answer will not change — but it is now said out loud in the step summary, so a leak shows up on the run that caused it rather than in the pool an hour later. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Hyperloom Forge E2E — ❌ Queue timeout
|
CI E2E report — ❌ Queue timeout
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What was wrong
One counter for two different waits.
POLL_MAXcovered both waiting for a GPU and running the optimizer, so a saturated pool spent the run budget on the queue and then reportednot terminal after 13200s— which reads as a hang in the tested commit.Run 34190618492 spent 166 of its 220 polls in
phase=Queuedand was failed 54 minutes into a workload that went on to finish successfully 115 minutes later (k8s Jobci-pr-1414-34190618492-a7f21d57-a1:succeeded=1, 171m,crash_count 0). The check never gave a verdict on the code.The cancel path hid why the pool was saturated.
cleanup()discarded the DELETE response, and that response is a 40x. The workload survives, the backend dispatches it hours after its GitHub run ended, and it holds a GPU for a full run nobody is watching.Of 93 jobs dispatched 2026-09-07 → 09-08, 49 started after their run had already finished, taking 96 of 204 GPU-slot-hours. At 12:19Z all eight GPUs in the pool were held by workloads whose run had ended — none of them belonging to a live check.
What this changes
ci-e2eandforge-e2edispatchers. A red check now says whether the commit was ever given a GPU.cleanup()checks the status code, retries once, and reports an unreclaimed workload instead of returning success.--connect-timeout/--max-timeon the DELETE and onpost_status, a shorter retry, no retry at all from the signal trap, and the trap disarms itself so a SIGTERM mid-handler cannot re-enter it. Measured against a stubbed hanging backend: 6.4s wall, inside the ~10s SIGINT→SIGKILL window.curlfailed left the phase empty, notUnknown—jqexits 0 printing nothing on empty input, so neither the//default nor the|| echofallback fired. The empty phase fell through thedispatchedlatch and pinned the run budget on a workload still waiting for a GPU, on the first transient read of the several hundred a run makes.Verification
No bash test suite covers these scripts, so each behaviour was driven against a stubbed
curl:NOT reclaimedsummary, rc=1--max-time; pre-fix the stub was handed--max-time=NONEand slept 600squeue=3/3 phase=Unknown, verdictqueue timeout, DELETE sentPlus
bash -non both scripts andyaml.safe_loadon both workflows.What this does not fix
The leak itself. The DELETE still returns 40x and the workload still survives — this makes that loud and bounded rather than silent. Reclaiming the GPU needs the backend cancel to start succeeding; that is tracked separately.
Note this only takes effect for PRs whose branch carries it:
pull_requestloads the workflow from the PR head, so every other open PR keeps the old single budget until this is onmain.🤖 Generated with Claude Code