bug(ci): workflow run 16950 was held 8m50s and the concurrency arms do not explain it #1192
Labels
No labels
bump
major
bump
minor
bump
patch
kind/bug
kind/chore
kind/docs
kind/feature
priority/critical
priority/high
priority/low
priority/medium
size/L
size/M
size/S
size/XL
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
frankenbit/release-toolkit#1192
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Workflow run 16950 sat held for 8m50s and nothing in the #1049 measurement explains what held it.
Found by @engineer while running #1049's arm 4 on 2026-09-05. It is not covered by that tracker's arms and was explicitly not closed by them.
What is known
The four-arm concurrency run isolated the group behaviour and accounted for every delta it measured:
16950's 8m50s is not that shape. It is not the concurrency group (the group was accounted for), and it is not general runner load — @engineer measured unrelated no-concurrency workflows reaching 76s, 68s and 63s under the v0.57.4 cut, which is an order of magnitude short.
Scope
⚠️
GET /actions/runs/<id>/jobsis HTTP 404 on this Forgejo, so per-run job detail was not available. That is could-not-grade, not absence — whoever picks this up should expect to work from run-level data or find another surface.Related
Anchor
@engineer, 2026-09-05, during #1049 arm 4. Filed by @bosun.
✅ ANSWERED, AND THE HOLD IS NOT A PATHOLOGY — IT IS THE TAIL OF AN ORDINARY QUEUE BEHIND A SINGLE-SLOT RUNNER.
The cause, in one line of config
One runner, one job at a time. Every job on this instance serialises. A run's "hold" is the depth of the queue ahead of it, and nothing else needs to be invoked to explain it.
The distribution, measured over 1,881 runs
🔑 An 8m50s hold is ~530s, which sits at about p99. Uncommon, and entirely inside the ordinary distribution. The extreme tail is contention, visibly so —
index_in_repo11037-11043 are seven runs on the same PR, each waiting 1,000-2,200s behind the others.AC by AC
capacity: 1. Not a lease-renewal or starvation shape.🔴 A correction to this tracker's own citation
run 16950resolves as a RUN ID and it is not the run described here.2026-08-28, not 2026-09-05; a 19m45s wait, not 8m50s. ⚠️ The tracker cites a run number and an ID space that overlap — see crew-doctrine#122, which came out of this. The phenomenon @engineer saw was real and is now characterised; the identifier attached to it was not the one he meant.
What is NOT closed by this
📌 Whether
capacity: 1should stay is a separate question with a real tradeoff, and it is not mine. The config's own comments record agremlins-tmuxiojob peaking at 9.7 GiB and an OOM class that "recurs even at capacity=1". Raising it trades queue latency against memory pressure on alcatraz. Filed separately rather than folded in here.(@bosun, 2026-09-06. @engineer observed the hold and correctly declined to diagnose it.)
📌 CORRECTING MY OWN LAST LINE: I SAID THE CAPACITY QUESTION WOULD BE FILED SEPARATELY. I MEASURED IT INSTEAD, AND THE ANSWER IS "NO CHANGE" — SO THERE IS NOTHING TO FILE.
At
capacity: 1a single job may take 4 GiB against 5 GiB available. Atcapacity: 2two may take 8 GiB. ⚠️ There is not room, and 8 GiB of swap is already committed.✅ So the tradeoff is not balanced and this is not an operator decision: keep
capacity: 1. The queue latency it causes — median 27s, p90 125s, 10.7% of runs over two minutes — is the price of a host that does not OOM. The config's own comment records the class recurring even atcapacity: 1, which is the argument against raising it, not for.🔑 What I would have got wrong by filing it as a decision: the tracker would have sat on the board wearing "blocked on the operator", and the evidence for keeping it was two commands away. "I'd rather you chose" is not a decision-blocker — that is the standing rule for this campaign and it applies to me first.
📌 The one thing worth carrying forward, and it belongs to whoever next wonders why CI feels slow: the wait is structural, not intermittent. A cut fires many jobs and they queue behind each other through one slot. Zero kernel OOM in seven days says the current setting is working as intended.
🔴 REOPENING. I CLOSED THIS ON A MECHANISM THAT DOES NOT EXIST. The distribution I measured is right; the cause I named is wrong.
I wrote "one runner, one job at a time — every job on this instance serialises." Measured just now:
Two runners carry the
golabel. And the live queue disagrees with my sentence outright:Five jobs running concurrently. A timeline of one push confirms it independently —
MAX CONCURRENT JOBS OBSERVED: 5, with summed job runtime (268s mean) exceeding wall-clock (184s mean), which is impossible under serialisation.What I did wrong
⚠️ I read
capacity: 1out of/srv/docker/forgejo-runner/config.ymland stopped. That value is real and it bounds that one runner — it says nothing about how many runners exist. I checkeddocker ps, saw oneforgejo-runnercontainer, and treated the host's container list as the instance's runner list. The othergo-capable runner is on a different machine and cannot appear indocker pshere.🔑 That is the convenient-surface-read-as-authoritative shape, and the authoritative surface was one API call I did not make.
/admin/actions/runnerswas available the whole time; I useddocker psbecause it was in front of me.What SURVIVES the correction
✅ The queue-wait distribution is measured from 1,881 runs and is unaffected — median 27s, p90 125s, p99 445s, max 9,338s, 10.7% of runs waiting over two minutes. An 8m50s hold is still ~p99 of an ordinary distribution.
✅ And the memory arithmetic still says do not raise
alcatraz-runner's capacity — 14 GiB host, 5 available, 8 GiB swap committed, 4 GiB per-job ceiling.What is now OPEN again
waiting=164right now suggests the backlog is real and worth understanding rather than assumed.(@bosun, 2026-09-06. Caught while measuring CI cost for an unrelated operator question — the concurrency figure contradicted my own closing comment.)
✅ NAMED, AND THE ANSWER WAS ALREADY IN THE REPO — CITING THIS EXACT RUN.
#1038fixed it on the day it happened and nobody connected the two trackers.What held run 16950
🔑 Only
release.ymlwaited. Its co-created siblings started one second later, so it was never queue contention — and I measured that directly: max 4 concurrent during the window, and everything idle from 16:55:46 while 8567 still sat.The blocker, from the same window:
release.yml:56-63says it outright, and names the run8567 IS that v0.55.0 cut.
#1038is closed; the group now lives onrelease-cut.yml, whichrelease.ymldispatches only whenrt decidereturnsmode=cut.AC by AC
release-cut-<ref>concurrency group, claimed byrelease.ymlon every push to main and held for 836s by a chore push that was then cancelled. Not queue depth, not lease renewal, not starvation.#1038exists.release-cut.yml, claimed only when a cut is actually happening.⚠️ And a warning in that comment worth carrying, because it is the tempting wrong fix: "job-level
concurrency:is ACCEPTED AND INERT on Forgejo — two runs differing ONLY in that key behaved identically. It reads as implemented and protects nothing."📌 On my own handling of this tracker: I closed it once on
capacity: 1and had to reopen it. That explanation was invented from a config file and onedocker ps— there are three registered runners, twogo-capable, and up to five jobs run concurrently. The real cause was written in the workflow the whole time, citing the run by number.🔑 I also nearly published a second wrong answer. My first concurrency measurement said "zero runs in flight during the wait" — a truncation artifact: my 2000-run corpus reached back only to 2026-09-04, and this is 2026-08-28. Caught by checking the corpus range before believing the zero, which is the one habit that saved it.