browser-slot.sh: stop overwriting the caller's BASE, and let a waiter take any slot that frees
Two defects, both reproduced before the fix and re-measured after. 1. It assigned BASE for its lock path. A variable the caller exported stays exported in bash, so every gate run through the slot saw BASE=/tmp/doxa-maps-browser-gate.lock. Seven gates read process.env.BASE; check:upg-map-data-painted printed "SKIP — nothing served at /tmp/doxa-maps-browser-gate.lock/…" and exited 0. Renamed to SLOT_BASE. Proof: with BASE=/x set, a child printed the lock path before and /x after. 2. With every slot busy, waiters queued on slot 1 alone, so a slot that freed was never taken and two slots behaved as one queue. It now polls every slot with flock -n every 2 s until the deadline and still exits 75 on timeout. Proof, in a private slot dir with slot 1 held 12 s and slot 2 freed at ~3 s: a caller waited 11.5 s before and 4.3 s after. A free slot is still taken at once (66 ms). check-upg-map-data-painted: its unreachable-server SKIP exited 0 and its wrong-tree refusal exited 2. Both now exit 75, the repo's "not measured" code. Its existing tree-identity check is what makes the :4173 default safe, since another agent's tree there is refused.
This commit is contained in:
parent
63083372c8
commit
be3620ef1c
2 changed files with 32 additions and 13 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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:<port>/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 }`
|
||||
|
|
|
|||
Loading…
Reference in a new issue