diff --git a/scripts/test-zb.sh b/scripts/test-zb.sh index 1ec2266bf..4dbcc51bf 100755 --- a/scripts/test-zb.sh +++ b/scripts/test-zb.sh @@ -12,20 +12,33 @@ 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; } 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) 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 +} # 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' +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" -sleep 1 -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" +poll "third build" "$T/o3" 'zb: waiting for a build slot' +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" @@ -60,9 +73,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. diff --git a/scripts/zb b/scripts/zb index 8135277a5..5758b374b 100755 --- a/scripts/zb +++ b/scripts/zb @@ -56,29 +56,51 @@ 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, 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 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 [ -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