Files
BFM-decomp/tools/treelock.sh
T
Drew T f3ec6ef588 fix(phase-30): treelock.sh — an flock MUTEX for tree-writing campaigns (incident 2: a poll is not a mutex)
I gated 8 binaries in parallel while wave-2's propagation loop was still running, then ran
'make clean' on top. check-all 77/140; the corpus denominator moved, so the apparent 91.4% instr
was a half-written tree, not a gain. Reverted to commit:1245 (last R22-verified) — 140/140 restored,
all 58 drafts survived because agents only ever write .run/.

ROOT CAUSE, and it was structural not unlucky: my guard was
  while pgrep -f dedup_propagate; do sleep; done
A CAMPAIGN is a LOOP of short-lived processes (15 sequential invocations), so it has gaps where no
process matches. The poll sampled a gap and started. Presence-of-a-process cannot express 'a
campaign owns the tree'.

treelock.sh holds one flock for the WHOLE campaign, released by the kernel on exit OR kill, with
--status; both drivers refuse to run unlocked. LAW: guard the CAMPAIGN, not the process.
Corollary (twice today): a killed process performs no undo — a fleet-tier write needs a lock ABOVE
it, not cleanup inside it.
2026-07-30 22:00:32 -06:00

49 lines
2.3 KiB
Bash

#!/usr/bin/env bash
# treelock.sh — THE mutex for tree-writing campaigns. Source it or wrap a command with it.
#
# tools/treelock.sh <name> <command…> # acquire, run, release (even on failure/kill)
# tools/treelock.sh --status # who holds it
#
# WHY (P30, twice in one session — the second time I caused it myself):
# `src/`, `config/`, `asm/` and `build/` are ONE shared mutable state. Any two of {gate_stage,
# dedup_propagate, family_sweep, jtbl_family_bank, make clean/extract/check} running at once can
# interleave writes and leave the fleet incoherent — and a killed writer performs NO undo, so the
# damage outlives the process.
#
# The first attempt at a guard was `while pgrep -f dedup_propagate; do sleep; done`. That is
# STRUCTURALLY WRONG and it failed within the hour: a CAMPAIGN is a LOOP of short-lived processes
# (15 sequential dedup_propagate invocations), so between every pair there is a window with no
# matching process. A parallel gate polled during one of those windows, saw "clear", and started —
# then a `make clean` deleted asm/ under the still-running campaign. Result: 63 of 140 binaries
# failed check-all and every uncommitted bank had to be reverted.
#
# The lesson, stated generally: **guard the CAMPAIGN, not the process.** Presence-of-a-process is a
# sampling test on a gappy signal; a lock file is a statement of intent that spans the gaps.
# Uses flock(1) — held for the WHOLE wrapped command, released by the kernel on exit or kill.
set -uo pipefail
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
LOCK="$REPO/.run/tree.lock"
mkdir -p "$(dirname "$LOCK")"
if [ "${1:-}" = "--status" ]; then
if flock -n "$LOCK" true 2>/dev/null; then echo "tree lock: FREE"; else
echo "tree lock: HELD by -> $(cat "$LOCK.owner" 2>/dev/null || echo '(unknown)')"; fi
exit 0
fi
NAME="${1:?usage: treelock.sh <name> <command...>}"; shift
[ $# -gt 0 ] || { echo "treelock: no command given" >&2; exit 2; }
exec 9>"$LOCK"
if ! flock -w "${TREELOCK_WAIT:-14400}" 9; then
echo "treelock: timed out waiting for the tree (held by $(cat "$LOCK.owner" 2>/dev/null))" >&2
exit 75
fi
echo "$NAME pid=$$ since=$(date +%H:%M:%S)" > "$LOCK.owner"
trap 'rm -f "$LOCK.owner"' EXIT
echo "[treelock] acquired by $NAME"
"$@"
rc=$?
echo "[treelock] released by $NAME (rc=$rc)"
exit $rc