Repository navigation
fix(PG-956): the run-slot tests take a lock as the Mac does on CI's ubuntu-26.04 - #1076
Merged
Merged
Conversation
…buntu-26.04 The runner's default mkdir is uutils 0.10.0, which reports success when another process made the folder between its check and its own mkdir, so two looks could each hold run-slot.sh's gauges lock and both run top (expected 1, received 6). The fake machine's bin now runs GNU's mkdir, which the image keeps as gnumkdir, wherever it exists; run-slot.sh itself runs on the Mac, whose mkdir is atomic. uutils fixed it in 0.11.0, and .agents/testing.md's Traps names the case.
…flake # Conflicts: # .agents/testing.md
Owner
Author
ReviewBot · round 1: 5/5 at 0005bc9 · 0 blocking, 0 should-fix, 3 nitsOn GitHub's ubuntu-26.04 runner, Spec (the right thing)
Nothing unasked. Standards (built well)No blocking or should-fix finding. Nits, each yours to take or leave:
ThreadsNone opened, none open. Checked
On this 5/5 candidate, the fresh full read found nothing blocking or should-fix. Its nits are listed above for you to take or leave. Not verified
ReviewBot · round 1. Push back on any finding you think is wrong. |
Owner
Author
|
Merged by bun run pr:merge at 0005bc9.
|
This was referenced Nov 5, 2026
nrjdalal
added a commit
that referenced
this pull request
Nov 6, 2026
…ding (#1089) ## TL;DR - 🎛️ When run-slot counts how many runs the machine can take, it now uses one CPU reading for the whole count. Before, a new reading that arrived in the middle could give a count that matched neither the old reading nor the new one. - 🧪 This fixes a test that failed now and then on GitHub's Linux runner. When that test fails now, it shows what each check printed. ## Summary ```diff capacity() # slots, room and every take read it + local LANES=unread for s in run-slot run-slot-2 run-slot-3 run-slot-4 slot_open "$s" - ! lanes_open # slots 2, 3 and 4: each read the gauges again + ! lanes_now # the first check reads them, the rest share its verdict +lanes_now() + open|shut: the count's verdict; unread: lanes_open once and keep it; unset: lanes_open, as before ``` - `run-slot.sh slots` prints `capacity() / 2`. Each of slots 2 to 4 called `lanes_open`, and each `lanes_open` read `gauges()` again. In the dead-lock test the stale reading says 30% idle (shut) and the new one 80% (open). A look whose checks straddled the new reading counted slot 2 shut and slots 3 and 4 open, and printed `3`, or `2` when it landed between slots 3 and 4. - The fix is a `local` in `capacity()`, which bash's dynamic scope hands to `slot_open`'s checks. Outside a count, `LANES` is unset and every check runs `lanes_open` as before, so a waiter still reads the gauges on every poll. - `scripts/ops/README.md`, beside the takeover line: one line on the single verdict. **Decisions:** - **The fix is in run-slot.sh, not the test.** A real look on the Mac can count a mixed reading too: the bug isn't specific to Linux, only easier to hit on a 2-CPU runner. - **The take loop is unchanged.** It calls `slot_open` slot by slot to take the first open one, so a reading that lands in the middle only changes which open slot it takes. Its counts come from `capacity()`, which now reads once. - **The dead-lock test now fails on the list of bad looks, not on a boolean.** `expect(looks.filter(neither 1 nor 4)).toEqual([])` prints each bad look's slot line and stderr, so a CI failure says what a look printed. It stays whatever the cause turns out to be. ## Evidence - **The new test**, "a look counts slots 2 to 4 on one reading, even when a new one lands between their checks". It sets a stale 30% reading, a lock a live refresher holds, and a fake `stat`. `stat` runs on the lock only in `gauges_lock`, once its mkdir finds the lock held, so the fake writes a fresh 80% reading after the look's first lanes check has read the stale one, as a refresher's rename would. | | canary's `run-slot.sh` | this head | | --- | --- | --- | | Mac | 10 pass, 1 fail: expected `["1"]`, received `["3"]` | passes | | `ubuntu:26.04` container | 10 pass, 1 fail: received `["3"]` | passes | - **20 runs in a row** of `bun run test:file tests/scripts/ops/run-slot-gauges-lock.test.ts` on the Mac: 20 of 20, each 11 pass, 0 fail. Of the same file in `ubuntu:26.04` (`--cpus 2 --memory=1g`, Linux bun 1.4.4): 20 of 20, each 11 pass, 0 fail. - **Every run-slot test** (`tests/scripts/ops/run-slot*.test.ts`) on the Mac: 424 pass, 0 fail. `bun run check-types:tests`: no errors. `bun run test:skills`: all 13 passed. - **The first look, which caught nothing:** - the dead-lock case alone in `ubuntu:26.04` went 0 bad rounds in 60 with GNU's mkdir, and 0 in 60 with all 104 `gnu*` tools on PATH; - 60 runs of the whole file with a `bash -x` trace per look went 60 of 60 green. So it isn't a uutils tool, and a run can't be relied on to catch it. The deterministic test above is the evidence. - **Sightings on CI:** #959 at 3006e0a, Suite (5), job 127180040487, ubuntu-26.04: the dead-lock case's `slots.every(1 or 4)` received false. In `ubuntu:26.04` at #1076's head it failed 1 of 20 runs. - **doc-sync:** - `git grep -n -E "lanes_open|capacity\(\)|run-slot.sh slots|one reading"` over `.agents`, `docs`, the READMEs, `AGENTS.md` and the skills finds `scripts/ops/README.md:22, 26, 41`; - lines 22 and 41 name `lanes_open` for what it opens, which is unchanged; - the new line sits after line 28's takeover line. ## Merge Danger **Door:** two-way A revert brings back the per-slot readings and the flake. **Blast Radius:** small `run-slot.sh`'s slot count only. A count reads the gauges once instead of up to three times, and the verdict is the same whenever no reading lands in the middle. Every caller outside `capacity()` is unchanged. ## Links - Closes #1077 - #956 and #1076: the mkdir race in the same file, fixed there - #959: the CI sighting 🤖 Generated with [Claude Code](https://claude.com/claude-code)
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.
TL;DR
mkdircommand, which can tell two programs that each made the same folder. The tests now use the runner's othermkdir, which tells only one, as the Mac's does.Summary
tests/scripts/ops/run-slot-fake.ts, machine() slowChild("python3", bin) + if (GNU_MKDIR) // Bun.which("gnumkdir"): ubuntu-26.04 only + bin/mkdir = exec gnumkdir "$@"ubuntu-latestis ubuntu-26.04, whose/usr/bin/mkdiris uutils coreutils 0.10.0 (rust-coreutils0.10.0-1ubuntu2~26.04.1). Itscreate_dircheckspath.exists(), then maps a failedmkdir(2)to success withErr(_) if path.is_dir() => Ok(()), even without-p. So when two processes race, both exit 0.mkdir: the gauges refresh lock, the run slots and the model slots. On that runner, two looks can each hold the gauges lock and both runtop: "eight looks with no reading yet run top once" got 6.gnumkdir, and each fake machine's bin now runs that one wherever it exists. On the Mac and on GNU images nothing changes..agents/testing.md, Traps: one new trap, the racingmkdirand how a test avoids it.Decisions:
mkdiris atomic: 60 rounds of the eight-look case there each ran onetop. Only CI runs the script on Linux.Evidence
Cause, in an
ubuntu:26.04container: 8 racingmkdir L, 200 rounds each:/usr/bin/mkdir(uutils 0.10.0)gnumkdir(GNU 9.7)No repro without uutils: the eight-look case ran exactly one
topin 60 of 60 rounds on the Mac (BSD mkdir) and 80 of 80 onoven/bun:debian(GNU mkdir, bash 5.2).Before / after in
ubuntu:26.04(--cpus 2, Linux bun 1.4.4):topmore than oncerun-slot-gauges-lock.test.ts, 20 runs, the two "run top once" casesAt this head on the Mac: 20 runs in a row of
bun run test:file tests/scripts/ops/run-slot-gauges-lock.test.ts: 20 of 20 passed, each 10 pass, 0 fail.bun run check-types:tests: no errors.bun run test:skills: all 13 passed.Sightings on CI:
Not this race, and not fixed here. That case still failed 1 of 20 runs at this head in
ubuntu:26.04(run 12, line 125), with GNU'smkdirin the fake. A probe of the case alone found 0 bad rounds in 60 with GNU'smkdirand 0 in 60 with all 104gnu*tools on PATH, so no tool swap is shown to fix it. It is run-slot-gauges-lock.test.ts's dead-lock takeover case fails now and then on ubuntu-26.04 #1077.doc-sync: the change touches the test fake alone.
git grep -n -iE "mkdir"over.agents,scripts/README.md,scripts/ops/README.md,scripts/hooks/README.md,docsand the core skills finds no line that callsmkdiratomic. The new trap in.agents/testing.mdis the one place that says it is not on ubuntu-26.04.Merge Danger
Door: two-way
A revert brings back the uutils
mkdirin the fake, and the flake with it.Blast Radius: small
Tests only: every
run-slot*.test.tsmachine on a runner that hasgnumkdir. run-slot.sh and every other script are unchanged.Links
🤖 Generated with Claude Code