From 654414fce3ba8885404335a3012c8f5904ec637c Mon Sep 17 00:00:00 2001 From: "fizzy-linux[bot]" <346940526+fizzy-linux[bot]@users.noreply.github.com> Date: Sun, 25 Oct 2026 04:20:39 -0500 Subject: [PATCH 1/4] scripts: test-zb waits for zb's lines instead of sleeping Poll for the second build's waiting line and the load line with a 20s limit that fails showing the output, so a slow CI runner cannot fail the test. Agent-Team: linux Worker: claude Co-Authored-By: Claude Sonnet 5.5 --- scripts/test-zb.sh | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/scripts/test-zb.sh b/scripts/test-zb.sh index 1ec2266bf..d439fcdb7 100755 --- a/scripts/test-zb.sh +++ b/scripts/test-zb.sh @@ -16,15 +16,25 @@ export FIZZY_BUILD_ZIG=$T/zig FIZZY_BUILD_SLOTS=2 FIZZY_BUILD_MAX_LOAD=100000 \ FIZZY_BUILD_SLOTS_DIR=$T/slots FIZZY_BUILD_POLL=0.2 fail() { echo "FAIL: $1"; exit 1; } alive() { kill -0 "$1" 2>/dev/null; } +# poll : poll for the pattern in the file (a slow runner is not a failure), +# failing after 20s with what the file held. +poll() { + i=0 + while ! grep -q "$3" "$2" 2>/dev/null; do + i=$((i + 1)) + [ $i -le 200 ] || fail "$1 never showed '$3' in $2 after 20s: $(cat "$2" 2>/dev/null)" + sleep 0.1 + done +} # Two builds hold the two slots; the third waits, then runs when they finish. ./zb build a >"$T/o1" 2>&1 & echo $! >>"$T/pids" ./zb build b >"$T/o2" 2>&1 & echo $! >>"$T/pids" -sleep 1 +poll "builds a and b" "$T/ran" '^build b' +poll "builds a and b" "$T/ran" '^build a' ./zb build c >"$T/o3" 2>&1 & p3=$!; echo $p3 >>"$T/pids" -sleep 1 +poll "third build" "$T/o3" 'zb: waiting for a build slot' alive $p3 || fail "third build did not wait" -grep -q 'zb: waiting for a build slot' "$T/o3" || fail "no waiting line: $(cat "$T/o3")" grep -q '^build c' "$T/ran" && fail "third build ran while both slots were held" echo 3 >"$T/status" touch "$T/go" @@ -60,9 +70,8 @@ grep -q '^build i' "$T/ran" || fail "build i did not run" # Over the load ceiling, a build waits (and says so) until it is lifted. FIZZY_BUILD_MAX_LOAD=-1 ./zb build e >"$T/o5" 2>&1 & p5=$!; echo $p5 >>"$T/pids" -sleep 1 +poll "build above the load ceiling" "$T/o5" 'waiting for load' alive $p5 || fail "build ran above the load ceiling" -grep -q 'waiting for load' "$T/o5" || fail "no load line: $(cat "$T/o5")" kill $p5 # Light commands pass straight through, and FIZZY_BUILD_SKIP bypasses the wait. From 0b59e352b99a5d5b8f74e3f53f6b6198f2260d89 Mon Sep 17 00:00:00 2001 From: "fizzy-linux[bot]" <346940526+fizzy-linux[bot]@users.noreply.github.com> Date: Sun, 25 Oct 2026 07:00:59 -0500 Subject: [PATCH 2/4] scripts: test-zb holds both slots before the third build, dumps state on failure Agent-Team: linux Worker: claude Co-Authored-By: Claude Sonnet 5.5 --- scripts/test-zb.sh | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/scripts/test-zb.sh b/scripts/test-zb.sh index d439fcdb7..4dbcc51bf 100755 --- a/scripts/test-zb.sh +++ b/scripts/test-zb.sh @@ -12,6 +12,7 @@ while [ ! -e "$T/go" ]; do sleep 0.1; done exit \$(cat "$T/status" 2>/dev/null || echo 0) FAKE chmod +x "$T/zig" +unset FIZZY_BUILD_SKIP FIZZY_BUILD_WAIT export FIZZY_BUILD_ZIG=$T/zig FIZZY_BUILD_SLOTS=2 FIZZY_BUILD_MAX_LOAD=100000 \ FIZZY_BUILD_SLOTS_DIR=$T/slots FIZZY_BUILD_POLL=0.2 fail() { echo "FAIL: $1"; exit 1; } @@ -22,7 +23,7 @@ poll() { i=0 while ! grep -q "$3" "$2" 2>/dev/null; do i=$((i + 1)) - [ $i -le 200 ] || fail "$1 never showed '$3' in $2 after 20s: $(cat "$2" 2>/dev/null)" + [ $i -le 200 ] || fail "$1 never showed '$3' in $2 after 20s: $(cat "$2" 2>/dev/null) ran: $(cat "$T/ran" 2>/dev/null) slots: $(cat "$T"/slots/*/pid 2>/dev/null) env: $(env | grep FIZZY_BUILD | tr '\n' ' ')" sleep 0.1 done } @@ -32,10 +33,12 @@ poll() { ./zb build b >"$T/o2" 2>&1 & echo $! >>"$T/pids" poll "builds a and b" "$T/ran" '^build b' poll "builds a and b" "$T/ran" '^build a' +poll "slot 0 held" "$T/slots/slot.0/pid" '^[0-9]' +poll "slot 1 held" "$T/slots/slot.1/pid" '^[0-9]' ./zb build c >"$T/o3" 2>&1 & p3=$!; echo $p3 >>"$T/pids" poll "third build" "$T/o3" 'zb: waiting for a build slot' -alive $p3 || fail "third build did not wait" -grep -q '^build c' "$T/ran" && fail "third build ran while both slots were held" +alive $p3 || fail "third build did not wait: $(cat "$T/o3") ran: $(cat "$T/ran")" +grep -q '^build c' "$T/ran" && fail "third build ran while both slots were held: $(cat "$T/ran")" echo 3 >"$T/status" touch "$T/go" wait $p3; [ $? = 3 ] || fail "zig's exit status not passed on" From 6cdd200a9afd88221d0c89e40962ca88ce6c8f0c Mon Sep 17 00:00:00 2001 From: "fizzy-linux[bot]" <346940526+fizzy-linux[bot]@users.noreply.github.com> Date: Sun, 25 Oct 2026 09:17:27 -0500 Subject: [PATCH 3/4] scripts: zb publishes a slot with its pid, so a taker is never reclaimed zb took a slot with mkdir and wrote the pid afterwards, leaving a pid-less window in which another zb could judge the slot reclaimable. The merge queue (run 40194938550) showed the result: two builds ran, one slot held a pid. A slot is now built privately with its pid inside and renamed into place, and the taker runs zig only if the slot's pid is its own. Agent-Team: linux Worker: claude Co-Authored-By: Claude Sonnet 5.5 --- scripts/zb | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/scripts/zb b/scripts/zb index 8135277a5..1fc30b0b4 100755 --- a/scripts/zb +++ b/scripts/zb @@ -56,18 +56,27 @@ alive() { kill -0 "$1" 2>/dev/null; } slot_pid() { cat "$1/pid" 2>/dev/null; } -# Take a free slot, reclaiming one whose holder died. Sets $mine. A slot with no pid file yet is -# its taker between `mkdir` and the write, so it counts as held until it is a minute old. +# Take a free slot, reclaiming one whose holder died. Sets $mine. A slot appears with its pid +# already inside (a rename of a directory made in private), so a live holder is never seen without +# one and is never reclaimed by age; a slot with no pid file is a taker that died mid-way, held +# until it is a minute old. A rename onto a slot taken meanwhile nests inside it instead of +# failing, so the take counts only when the slot's pid is ours. mine= try_slot() { i=0 while [ "$i" -lt "$slots" ]; do s=$dir/slot.$i i=$((i + 1)) - if mkdir "$s" 2>/dev/null; then - echo "$$" >"$s/pid" - mine=$s - return 0 + if [ ! -e "$s" ]; then + rm -rf "$dir/take.$$" + mkdir "$dir/take.$$" && echo "$$" >"$dir/take.$$/pid" || return 1 + mv "$dir/take.$$" "$s" 2>/dev/null + if [ "$(slot_pid "$s")" = "$$" ]; then + mine=$s + return 0 + fi + rm -rf "$dir/take.$$" "$s/take.$$" + continue fi p=$(slot_pid "$s") if [ -z "$p" ]; then From 3771ce53b711a3eafe79f73f90686c2fa751f905 Mon Sep 17 00:00:00 2001 From: "fizzy-linux[bot]" <346940526+fizzy-linux[bot]@users.noreply.github.com> Date: Sun, 25 Oct 2026 10:45:04 -0500 Subject: [PATCH 4/4] scripts: zb reclaim moves the pid file, never a live holder's slot Agent-Team: linux Worker: claude Co-Authored-By: Claude Sonnet 5.5 --- scripts/zb | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/scripts/zb b/scripts/zb index 1fc30b0b4..5758b374b 100755 --- a/scripts/zb +++ b/scripts/zb @@ -59,8 +59,9 @@ slot_pid() { cat "$1/pid" 2>/dev/null; } # Take a free slot, reclaiming one whose holder died. Sets $mine. A slot appears with its pid # already inside (a rename of a directory made in private), so a live holder is never seen without # one and is never reclaimed by age; a slot with no pid file is a taker that died mid-way, held -# until it is a minute old. A rename onto a slot taken meanwhile nests inside it instead of -# failing, so the take counts only when the slot's pid is ours. +# until it is a minute old, then claimed by an exclusive create of its pid. A rename onto a slot +# taken meanwhile nests inside it instead of failing, so the take counts only when the slot's pid +# is ours. mine= try_slot() { i=0 @@ -81,13 +82,25 @@ try_slot() { p=$(slot_pid "$s") if [ -z "$p" ]; then [ -n "$(find "$s" -maxdepth 0 -mmin +1 2>/dev/null)" ] || continue + # Exclusive create: it fails if a taker has put its pid there since we looked. + if (set -C && echo "$$" >"$s/pid") 2>/dev/null; then + mine=$s + return 0 + fi + continue elif alive "$p"; then continue fi - # Rename is atomic, so only one waiter wins a dead slot; it retries the take next pass. - if mv "$s" "$dir/dead.$$" 2>/dev/null; then - rm -rf "$dir/dead.$$" - i=$((i - 1)) + # Moving the pid file away leaves a young pid-less slot that every other waiter holds, and + # fails if another reclaimer got there first. What was moved is checked: if the slot was + # taken after we read the pid, it holds a live pid, which goes back. + if mv "$s/pid" "$s/pid.$$" 2>/dev/null; then + if [ "$(cat "$s/pid.$$")" = "$p" ] && ! alive "$p"; then + rm -rf "$s" + i=$((i - 1)) + else + mv "$s/pid.$$" "$s/pid" + fi fi done return 1