diff --git a/internal/browser-slot.sh b/internal/browser-slot.sh index 217658959..fc0b8a2e1 100755 --- a/internal/browser-slot.sh +++ b/internal/browser-slot.sh @@ -23,13 +23,16 @@ set -u SLOTS="${BROWSER_SLOTS:-2}" DIR="${BROWSER_SLOT_DIR:-/tmp}" -BASE="$DIR/doxa-maps-browser-gate.lock" +# SLOT_BASE, never BASE: a variable the CALLER exported stays exported in bash, so assigning BASE +# here rewrote the caller's BASE for every gate this runs. Seven gates read process.env.BASE, and +# check:upg-map-data-painted measured /tmp/doxa-maps-browser-gate.lock/... and SKIPPED with exit 0. +SLOT_BASE="$DIR/doxa-maps-browser-gate.lock" FLOOR_MB="${BROWSER_MEM_FLOOR_MB:-2500}" WAIT_S="${BROWSER_LOCK_WAIT:-900}" MEM_WAIT_S="${BROWSER_MEM_WAIT:-300}" BUSY=75 -slot_path() { [ "$1" = "1" ] && echo "$BASE" || echo "$BASE$1"; } +slot_path() { [ "$1" = "1" ] && echo "$SLOT_BASE" || echo "$SLOT_BASE$1"; } mem_available_mb() { awk '/^MemAvailable:/ { printf "%d", $2 / 1024 }' /proc/meminfo 2>/dev/null || echo 999999 @@ -58,10 +61,22 @@ while [ "$i" -le "$SLOTS" ]; do i=$((i + 1)) done -# Everything busy: queue on slot 1, and report a timeout as UNMEASURED rather than as a verdict. -exec 9>"$(slot_path 1)" -if flock -w "$WAIT_S" 9; then - exec "$@" -fi +# Everything busy: keep POLLING EVERY SLOT until the deadline, and report a timeout as UNMEASURED +# rather than as a verdict. This used to queue on slot 1 alone (flock -w on slot 1), so a slot that +# freed up was never taken: measured 2026-09-25 (dx-shepherd), ten waiters were piled on slot 1 while +# slot 2 sat free, and with slot 1 held 12 s and slot 2 freed at ~3 s a caller waited 11.5 s — the +# two-slot semaphore had collapsed into ONE queue. Polling takes the first slot that frees (~4 s in +# the same test). Trade-off, accepted: polling is not first-come-first-served, so under heavy +# contention a waiter can lose a few rounds; an idle slot beside a queue is strictly worse. +deadline=$(( $(date +%s) + WAIT_S )) +while [ "$(date +%s)" -lt "$deadline" ]; do + i=1 + while [ "$i" -le "$SLOTS" ]; do + exec 9>"$(slot_path "$i")" 2>/dev/null || true + if flock -n 9; then exec "$@"; fi + i=$((i + 1)) + done + sleep 2 +done echo "browser-slot: no slot within ${WAIT_S}s (${SLOTS} slots, floor ${FLOOR_MB}MB) — not measuring" >&2 exit "$BUSY" diff --git a/internal/check-upg-map-data-painted.mjs b/internal/check-upg-map-data-painted.mjs index a10ced5f6..325b4273d 100644 --- a/internal/check-upg-map-data-painted.mjs +++ b/internal/check-upg-map-data-painted.mjs @@ -58,9 +58,12 @@ const MIN_ZONE_RATIO = 0.9 let reachable = false try { const r = await fetch(urls[0], { signal: AbortSignal.timeout(5000) }); reachable = r.ok } catch { /* precondition */ } if (!reachable) { - console.log(`UPG-MAP-DATA: SKIP — nothing served at ${urls[0]}.`) - console.log(' Serve the built tree: python3 -m http.server 4173 --directory public (or set BASE)') - process.exit(0) + // NOT MEASURED, exit 75 — never exit 0. Through browser-slot.sh this line once read + // "SKIP — nothing served at /tmp/doxa-maps-browser-gate.lock/…" (the slot had overwritten BASE) + // and EXITED 0, so a wave counted a gate that measured nothing as green. + console.log(`UPG-MAP-DATA: NOT MEASURED — nothing served at ${urls[0]}.`) + console.log(' Serve THIS checkout (internal/serve-strict.mjs on your own port) and pass BASE=http://127.0.0.1:/doxa-maps') + process.exit(75) } // ★ "ANSWERS 200" AND "IS SERVING THIS BUILD" ARE DIFFERENT QUESTIONS. This gate reads the counter @@ -70,15 +73,16 @@ if (!reachable) { // See _serves-this-tree.mjs (bundler-2, BUG-BROWSER-GATES-CAN-MEASURE-ANOTHER-WORKTREES-TREE): // :4173 on this box was once held by a different worktree and a gate reported three confident runs // about code nobody had changed. -// ⚠ NOT MEASURED rather than SKIP, exit 2: "nothing to check here" and "I cannot attribute what I -// saw" are different facts, and only the second one should stop you trusting the numbers. +// ⚠ NOT MEASURED, exit 75 — the repo's EX_TEMPFAIL for "not measured" (a9550858), which the wave +// runner reports as ⊘. It was exit 2 here, a code nothing else in the repo reads that way. +// This check is also what makes the :4173 default safe: another agent's tree there is refused. const TREE_BASE = String(urls[0]).split('/doxa-maps/')[0] const treeCheck = await servesThisTree(TREE_BASE, ROOT) if (!treeCheck.ok) { console.error('UPG-MAP-DATA: NOT MEASURED — the server is not serving this checkout.') console.error(` ${treeCheck.why}`) console.error(' The polygon and pin counts below would describe someone else\'s build.') - process.exit(2) + process.exit(75) } const WALK = `(root) => { const o = []; const rec = (r) => { for (const e of r.querySelectorAll('*')) { o.push(e); if (e.shadowRoot) rec(e.shadowRoot) } }; rec(root); return o }`