Make apt fail fast, so the retry has something to retry - #9260
Merged
Conversation
#9256 bounded the stalls. This says why they happened and stops them. Reading the four logs, every one ends the same way: 04:47:02 Get:5 https://archive.ubuntu.com/ubuntu noble-security InRelease [126 kB] 05:16:29 ##[error]The operation was canceled. Twenty-nine minutes of silence mid-fetch of a 126 kB index file, and two of the four hung on that same file. What came before it is the other half: azure.archive.ubuntu.com was Ign'd four times over thirty seconds, so apt had already failed over through /etc/apt/apt-mirrors.txt to the public archive, which is not provisioned for this fleet. apt did not treat any of that as an error. Acquire::http::Timeout defaults to 120s and is an idle timeout, so a socket that is open and trickling never trips it. Cutting it to 20s with internal retries turns a 29 minute hang into a ~1 minute failure, and that matters beyond speed: the wall-clock kill is what orphans the dpkg lock, so an apt that gives up on its own is one we never have to kill. Not pinning a mirror. The evidence does not support it: in one stall Azure was dead and the public archive hung, in another Azure was serving fine and the transfer stalled at a 13.6 MB package. Neither is reliably better, and the mirrorlist failover is already the right mechanism. It just needed to be allowed to give up. The written config is validated against apt itself (apt-config dump round-trips all four options), and the helper's retry, timeout, giving up, recovery and no-command paths are exercised end to end.
Two bugs, both mine, both caught by CI on this branch.
1. apt-get update takes /var/lib/apt/lists/lock and nothing else. The
helper waited on /var/lib/dpkg/lock-frontend only, so after an attempt
was killed mid-update the wait saw a free lock, retried immediately,
and produced
E: Could not get lock /var/lib/apt/lists/lock. It is held by process 2420 (apt-get)
twice in under two seconds. Three attempts, one real one -- exactly the
'a retry that cannot succeed is worse than none' failure the header
warns about, in the one case it was written for. It now waits on all
four: dpkg's two, lists, and archives.
2. 150s per attempt was racing the mirror, not bounding it. The mirror
was degraded rather than dead, so every attempt got killed at the same
point and none finished. Two attempts of 360s instead of three of 150:
the bound exists to stop an infinite hang, not to beat a slow server.
studio-update-smoke's job budget goes 15 to 25 to fit the new worst
case; the guard checks that arithmetic.
The mirror outage is still live. From this branch's own GGUF smoke: 06:55:33 Ign:4 http://azure.archive.ubuntu.com/ubuntu noble-backports InRelease 06:55:35 Get:5 https://archive.ubuntu.com/ubuntu noble-security InRelease [126 kB] 07:01:11 (nothing, for 5m36s, then the attempt's cap) Azure is unreachable and the public archive it fails over to is not provisioned for this fleet. Acquire::http::Timeout does not save us there: it is an idle timeout, and a server dribbling a byte every few seconds never trips it, so the wall-clock bound is what ends the attempt. The bound is working as intended -- the step now fails in 14 minutes with the reason printed, where before it spent 30 and was reported as "cancelled" with nothing said. But a bounded failure is still a failure, and the operation that fails is one we mostly do not need. `apt-get update` refreshes every index for every suite. The runner image already ships populated lists, and these steps install a handful of ordinary packages. So try the install first, and refresh only when it misses: apt-get install -y X || { apt-get update && apt-get install -y X; } In the common case the slowest and most failure-prone apt operation is never performed at all. When the image's lists really are too stale -- the "Unable to locate package" case the runner-images maintainers warn about -- the update still runs and the install is retried, so nothing is traded away. Not applied to clean-machine-install-ci: a fresh WSL image genuinely has no lists, so install-first would always miss, and a bare machine is that leg's whole premise. The three outcomes were checked as shell rather than reasoned about: resolvable (update never reached, rc=0), not resolvable (update runs, rc=0), and both failing (rc=1, so the failure still propagates under the `bash -e` GitHub runs steps with).
This was referenced Aug 19, 2026
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.
The answer to "is installing packages broken?"
No. Nothing about this repo's dependencies. GitHub's Azure Ubuntu mirror was degraded, and apt has no timeout that stops it waiting on a dead connection.
Every one of the four stalled jobs ends the same way:
Twenty-nine minutes and twenty-seven seconds of total silence, mid-fetch of a 126 kB index file. Two of the four hung on that exact file. What comes immediately before it is the other half of the story:
The Azure mirror was already gone, so apt failed over through
/etc/apt/apt-mirrors.txtto the public archive, which is not provisioned for this fleet.apt did not consider any of that an error.
Acquire::http::Timeoutdefaults to 120s and is an idle timeout, so a socket that is open and trickling never trips it. This is a long-running, well-documented condition on GitHub's runners, tracked upstream in actions/runner-images#675, #6486, #6894 and #7048.What this changes
The helper now writes
/etc/apt/apt.conf.d/99-unsloth-ci-fail-fastbefore its first attempt:A 29-minute hang becomes a ~1 minute failure. That matters for a reason beyond speed: the wall-clock kill is what orphans the dpkg lock, so an apt that gives up on its own is an apt we never have to kill, and the retry is retrying a failed command rather than resuming a half-killed one.
It is written by the helper rather than assumed of the runner image so that it also covers
playwright install --with-deps, which shells out to apt without ever naming it.Not pinning a mirror, and the evidence is why: in one stall Azure was dead and the public archive hung; in another Azure was serving fine and the transfer stalled partway through a 13.6 MB package. Neither is reliably better. The mirrorlist failover is already the right mechanism, it just needed to be allowed to give up.
Verification
The generated config is validated against apt itself, not by eye.
apt-config dumpround-trips all four options:The helper's behaviour is exercised end to end with shims for
sudoandfuser, so the best-effort fallbacks are covered rather than assumed: success on the first attempt, a command that always fails (3 attempts, all reported as "exited with status 1" rather than as a stall), a command that hangs (killed and reported as "did not finish within 2s"), recovery on attempt 2, the no-command case, and an unwritable config warning and continuing rather than aborting. All as expected.Guard:
test_the_helper_makes_apt_fail_fastasserts the four options are in the config the helper actually writes, that the timeout stays well under apt's useless 120s default, that it is applied before the retry loop (configured inside or after it, the first attempt is the one that stalls and it would run unconfigured), and that an unwritable config does not abort. Four mutants reintroduced and confirmed red.One of those mutants initially survived, which is worth recording: the first version of the test searched the whole file, so it matched the option names in the header comment that explains them, and deleting the line that actually set one went unnoticed. It now parses the generated config.
tests/studio: 4457 passed, 4 skipped.