igneum/infra/build-server/capacity/lib.sh
igneum-labs 6f5787ed71 Build boxes: the measure file is retired; a pinned measurement leases its cores only (lease.sh cores), a whole-box quiet measurement is its own class (refused beside a slot or a lease, 20-minute cap, named owner), bounded suites never take it, every run keeps off leased cores, stopped holders are reaped after 5 minutes by every keeper, every waiter has a label file; the box-side self-tests in the gate; provision installs the lease tool and the headless Chromium libraries
Main, 7 October 2026, 15:07 UK: one global exclusive measure flock across unrelated measurements stalled build-1 at load 120 with
free slots (a stopped probe held it 5.5 h; an exclusive waiter queued every new shared taker) and build-2 behind a one-core VDF
bench. lease.sh (installed at /srv/builds/_bin/lease by provision.sh and by hand on both boxes) takes one flock per core for a
pinned measurement and the quiet file for a whole-box one; remote-run.sh takes quiet shared only for unbounded runs, excludes
leased cores from its set, and its keeper refreshes the holder file and calls lease reap (a STOPPED holder of a lease, quiet or a
slot for 5 minutes is killed with a line in _log/reaped.log). Keepers close the lock descriptors they inherit (an orphaned sleep
held a slot and the worktree lock 20 s past the release; the slot self-test had rotted on that since the worktree lock landed).
tools/ci/box-locks-check.sh runs lease.sh --self-test and remote-run.sh --self-test-slots on build-1. provision.sh also carries
the 16 libraries headless Chromium needs (installed by hand on both boxes at 14:5x UK) and a headless self-test step.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-07 13:30:40 +00:00

122 lines
7.6 KiB
Bash
Executable file

#!/usr/bin/env bash
# Shared helpers for the capacity layer on igneum-build-1 (infra/build-server/capacity). Sourced by run.sh and every
# job in jobs/. Nothing here takes a build slot (the slots are /srv/builds/_locks/build-<k>, owned by remote-run.sh)
# and nothing writes under /srv/builds/<worktree>. All work lives under /srv/capacity.
#
# Paths (overridable by environment for the smoke test and the dry run):
# CAP_ROOT /srv/capacity the layer's own tree
# CAP_SRC $CAP_ROOT/src a clone of /srv/igneum.git (master), the repo the jobs build and run from
# CAP_FORK $CAP_SRC/vendor/igneum-node a clone of /srv/igneum-node.git, the node fork
# CAP_OUT $CAP_ROOT/out/<job>/<date> results, one directory per job per UTC day
# CAP_STATE $CAP_ROOT/state per-job cursors (the continuous jobs advance their seed base here)
# CAP_LOG $CAP_ROOT/log per-job slice logs
# LOCKS /srv/builds/_locks read only: the build slots and the measure hold the layer yields to
# CAP_JSON /srv/workers/capacity.json the one-line-per-job summary the dashboard reads
set -uo pipefail
CAP_ROOT="${CAP_ROOT:-/srv/capacity}"
CAP_SRC="${CAP_SRC:-$CAP_ROOT/src}"
CAP_FORK="${CAP_FORK:-$CAP_SRC/vendor/igneum-node}"
CAP_OUT_ROOT="${CAP_OUT_ROOT:-$CAP_ROOT/out}"
CAP_STATE="${CAP_STATE:-$CAP_ROOT/state}"
CAP_LOG="${CAP_LOG:-$CAP_ROOT/log}"
LOCKS="${IGNEUM_BUILD_SLOTS_DIR:-/srv/builds/_locks}"
BUILDS_ROOT="${IGNEUM_BUILD_ROOT:-/srv/builds}"
export IGNEUM_CAPACITY_JSON="${IGNEUM_CAPACITY_JSON:-/srv/workers/capacity.json}"
REPO_MIRROR="${CAP_REPO_MIRROR:-/srv/igneum.git}"
NODE_MIRROR="${CAP_NODE_MIRROR:-/srv/igneum-node.git}"
NODE_BRANCH_DEFAULT="capacity-probe" # the sync-fuzz branch; falls back to the newest release-*-node on the mirror
REPO_BRANCH="${CAP_REPO_BRANCH:-master}" # the repo branch the layer builds and runs from; master in production, box-capacity until it merges (install.sh writes a drop-in)
HERE_LIB="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SUMMARY="$HERE_LIB/summary.mjs"
NODE_BIN="${NODE_BIN:-/usr/local/bin/node}"
UTC_DATE() { date -u +%Y-%m-%d; }
cap_say() { printf '%s capacity/%s: %s\n' "$(date -u +%H:%M:%S)" "${CAP_JOB:-run}" "$*" >&2; }
# defaults so `set -u` never trips before the first cap_sync_checkout (a standalone job, or a smoke)
CAP_NODE_BRANCH="${CAP_NODE_BRANCH:-}"; CAP_REPO_SHA="${CAP_REPO_SHA:-}"; CAP_FORK_SHA="${CAP_FORK_SHA:-}"; CAP_REPO_BRANCH_USED="${CAP_REPO_BRANCH_USED:-}"
# ---- the build-slot and measure hold the layer yields to --------------------------------------------------------------
# a slot or the measure file is "held" when flock -n cannot take it (another process holds the fd). The same probe the
# collector uses (tools/workers/collect.mjs flockHeld). Returns 0 (true) when ANY build slot, the quiet hold or a core lease is taken.
cap_build_active() {
local slots k f
slots=$(cat "$LOCKS/slots" 2>/dev/null || echo 1); [ "$slots" -ge 1 ] 2>/dev/null || slots=1
if [ -e "$LOCKS/quiet" ] && ! flock -n "$LOCKS/quiet" true 2>/dev/null; then return 0; fi # 7 Oct 2026: the quiet class replaced the measure file
for f in "$LOCKS"/core-*; do [ -e "$f" ] && ! flock -n "$f" true 2>/dev/null && return 0; done
for k in $(seq 0 $((slots - 1))); do
f="$LOCKS/build-$k"
[ -e "$f" ] || continue
if ! flock -n "$f" true 2>/dev/null; then return 0; fi
done
return 1
}
cap_hold_reason() {
local slots k
if [ -e "$LOCKS/quiet" ] && ! flock -n "$LOCKS/quiet" true 2>/dev/null; then echo "quiet hold"; return; fi
for f in "$LOCKS"/core-*; do [ -e "$f" ] && ! flock -n "$f" true 2>/dev/null && { echo "core lease"; return; }; done
slots=$(cat "$LOCKS/slots" 2>/dev/null || echo 1); [ "$slots" -ge 1 ] 2>/dev/null || slots=1
for k in $(seq 0 $((slots - 1))); do
[ -e "$LOCKS/build-$k" ] || continue
if ! flock -n "$LOCKS/build-$k" true 2>/dev/null; then echo "build slot build-$k held"; return; fi
done
echo ""
}
# ---- the capacity checkout (never a /srv/builds worktree) -------------------------------------------------------------
# clone-or-fetch the repo at master and the fork at its branch into $CAP_SRC. Idempotent. The jobs build into target
# directories under this tree, never /srv/builds.
cap_sync_checkout() {
local node_branch="${1:-$NODE_BRANCH_DEFAULT}"
mkdir -p "$CAP_ROOT" "$CAP_STATE" "$CAP_LOG"
if [ ! -d "$CAP_SRC/.git" ]; then git clone -q "$REPO_MIRROR" "$CAP_SRC" || { cap_say "repo clone failed"; return 1; }; fi
git -C "$CAP_SRC" fetch -q origin '+refs/heads/*:refs/remotes/origin/*' || cap_say "repo fetch failed (using the last checkout)"
git -C "$CAP_SRC" checkout -q -- . 2>/dev/null || true
git -C "$CAP_SRC" clean -qfd -e target -e 'target-*' -e vendor -e out 2>/dev/null || true
local rb="$REPO_BRANCH"
git -C "$CAP_SRC" rev-parse -q --verify "origin/$rb" >/dev/null 2>&1 || rb=master
git -C "$CAP_SRC" checkout -q -B capacity-run "origin/$rb" 2>/dev/null || git -C "$CAP_SRC" reset -q --hard "origin/$rb"
CAP_REPO_BRANCH_USED="$rb"; export CAP_REPO_BRANCH_USED
mkdir -p "$CAP_SRC/vendor"
if [ ! -d "$CAP_FORK/.git" ]; then git clone -q --no-checkout "$NODE_MIRROR" "$CAP_FORK" || { cap_say "fork clone failed"; return 1; }; fi
git -C "$CAP_FORK" fetch -q origin '+refs/heads/*:refs/remotes/origin/*' || cap_say "fork fetch failed"
local b="$node_branch"
git -C "$CAP_FORK" rev-parse -q --verify "origin/$b" >/dev/null 2>&1 || b=$(git -C "$CAP_FORK" branch -r --list 'origin/release-*-node' | sed 's|.*/||' | sort -V | tail -1)
[ -n "$b" ] || b=master
git -C "$CAP_FORK" checkout -q -- . 2>/dev/null || true
git -C "$CAP_FORK" clean -qfd -e target -e 'target-*' 2>/dev/null || true
git -C "$CAP_FORK" checkout -q -B "$b" "origin/$b" 2>/dev/null || git -C "$CAP_FORK" reset -q --hard "origin/$b"
CAP_NODE_BRANCH="$b"
CAP_REPO_SHA=$(git -C "$CAP_SRC" rev-parse --short HEAD 2>/dev/null)
CAP_FORK_SHA=$(git -C "$CAP_FORK" rev-parse --short HEAD 2>/dev/null)
export CAP_NODE_BRANCH CAP_REPO_SHA CAP_FORK_SHA
cap_say "checkout: repo master $CAP_REPO_SHA, fork $CAP_NODE_BRANCH $CAP_FORK_SHA"
}
# cargo under the layer's discipline: no build slot, idle IO, SCHED_IDLE, nice 19, a bounded job count (the controller's
# SIGSTOP pauses it the instant a real build takes a slot; this keeps it gentle even while it runs).
cap_cargo() {
( cd "$1" && shift && CARGO_INCREMENTAL=0 CARGO_BUILD_JOBS="${CAP_CARGO_JOBS:-16}" \
nice -n 19 ionice -c3 chrt -i 0 "$HOME/.cargo/bin/cargo" "$@" )
}
# cap_cargo with a timeout: <secs> <dir> <cargo args...>. `timeout` cannot run the cap_cargo shell function, so the
# timeout wraps the external cargo directly (same nice/ionice/chrt). Returns 124 on timeout.
cap_cargo_t() {
local secs="$1" dir="$2"; shift 2
( cd "$dir" && CARGO_INCREMENTAL=0 CARGO_BUILD_JOBS="${CAP_CARGO_JOBS:-16}" \
timeout "$secs" nice -n 19 ionice -c3 chrt -i 0 "$HOME/.cargo/bin/cargo" "$@" )
}
cap_out_dir() { # <job>: make and echo today's out dir
local d="$CAP_OUT_ROOT/$1/$(UTC_DATE)"; mkdir -p "$d"; echo "$d"
}
# cap_summary <job> : read a JSON fragment on stdin and merge it into capacity.json under that job
cap_summary() { CAP_PRIORITY="${CAP_PRIORITY:-}" "$NODE_BIN" "$SUMMARY" job "$1"; }
cap_controller_summary() { "$NODE_BIN" "$SUMMARY" controller; }
# a cursor is a plain number per job (the continuous jobs' seed base); cap_cursor_get / cap_cursor_set
cap_cursor_get() { cat "$CAP_STATE/$1.cursor" 2>/dev/null || echo "${2:-0}"; }
cap_cursor_set() { mkdir -p "$CAP_STATE"; echo "$2" > "$CAP_STATE/$1.cursor"; }
# json_escape a string for a one-line summary (python, always present)
cap_json_str() { "$NODE_BIN" -e 'process.stdout.write(JSON.stringify(process.argv[1]||""))' "$1"; }