Files
agent-skills/skills/tracker-common/scripts/gate.sh
T
Miguel Palhas 0900e5fea1
ci / nix (push) Successful in 10s
ci / lint (push) Successful in 12s
perf(gate): widen per-slot jobs to (cores-2)/slots
The old cores/SLOTS/2 left a full test suite on 2 of 10 cores even with
the box idle. Reserve two cores for the agent sessions and split the
rest across slots.

Also retire the CPU framing on the subagent fan-out cap. The number
stays 3, but the binding constraints are memory per worktree and the
shared rate-limit window; subagents are mostly idle waiting on the API.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 13:56:55 +01:00

103 lines
3.2 KiB
Bash
Executable File

#!/usr/bin/env bash
# Machine-wide semaphore for heavy verification (test suites, builds).
# Many autonomous sessions share one box; without this they all run the full
# suite at once and OOM the machine. Exit 75 means "did not run, hand to CI".
set -uo pipefail
LOCKDIR="${AGENT_GATE_DIR:-${XDG_RUNTIME_DIR:-/tmp}/agent-gate}"
SLOTS="${AGENT_GATE_SLOTS:-}"
WAIT="${AGENT_GATE_WAIT:-600}"
MEM_FLOOR_MB="${AGENT_GATE_MEM_FLOOR:-2000}"
MEM_MAX="${AGENT_GATE_MEM_MAX:-3G}"
CPU_QUOTA="${AGENT_GATE_CPU_QUOTA:-}"
JOBS="${AGENT_GATE_JOBS:-}"
cores=$(nproc 2>/dev/null || echo 4)
[ -n "$SLOTS" ] || SLOTS=$(( cores / 4 )); [ "$SLOTS" -lt 1 ] && SLOTS=1
# Leave 2 cores for the agent sessions themselves; the rest is split across slots.
[ -n "$JOBS" ] || JOBS=$(( (cores - 2) / SLOTS )); [ "$JOBS" -lt 1 ] && JOBS=1
[ -n "$CPU_QUOTA" ] || CPU_QUOTA="$(( JOBS * 100 ))%"
avail_mb() { awk '/MemAvailable/ {print int($2/1024); exit}' /proc/meminfo 2>/dev/null || echo 99999; }
usage() {
cat <<'EOF'
gate.sh — run a heavy command under a machine-wide concurrency + memory cap.
gate.sh [-w SECS] [-s SLOTS] [-m MEMMAX] -- <command...>
gate.sh --status
Exit codes: command's own status, or 75 if no slot / not enough free RAM
within the wait budget (caller should skip the check and let CI cover it).
Env: AGENT_GATE_SLOTS AGENT_GATE_WAIT AGENT_GATE_MEM_FLOOR
AGENT_GATE_MEM_MAX AGENT_GATE_CPU_QUOTA AGENT_GATE_JOBS AGENT_GATE_DIR
EOF
}
status() {
mkdir -p "$LOCKDIR"
local free=0 busy=0 i
for i in $(seq 1 "$SLOTS"); do
if flock -n "$LOCKDIR/slot.$i" true 2>/dev/null; then free=$((free+1)); else busy=$((busy+1)); fi
done
echo "slots: $busy busy / $SLOTS total (${free} free)"
echo "mem: $(avail_mb) MB available (floor ${MEM_FLOOR_MB} MB)"
[ -s "$LOCKDIR/holders" ] && { echo "recent holders:"; tail -5 "$LOCKDIR/holders"; }
return 0
}
while [ $# -gt 0 ]; do
case "$1" in
-w|--wait) WAIT="$2"; shift 2 ;;
-s|--slots) SLOTS="$2"; shift 2 ;;
-m|--mem-max) MEM_MAX="$2"; shift 2 ;;
--status) status; exit 0 ;;
-h|--help) usage; exit 0 ;;
--) shift; break ;;
*) break ;;
esac
done
[ $# -gt 0 ] || { usage; exit 2; }
mkdir -p "$LOCKDIR"
deadline=$(( $(date +%s) + WAIT ))
fd=""
while :; do
if [ "$(avail_mb)" -ge "$MEM_FLOOR_MB" ]; then
for i in $(seq 1 "$SLOTS"); do
exec {try}>"$LOCKDIR/slot.$i"
if flock -n "$try"; then fd="$try"; break; fi
exec {try}>&-
done
fi
[ -n "$fd" ] && break
left=$(( deadline - $(date +%s) ))
[ "$left" -le 0 ] && {
echo "gate: no slot within ${WAIT}s (avail $(avail_mb) MB) — skipping, CI covers this" >&2
exit 75
}
sleep $(( left < 10 ? left : 10 ))
done
echo "$(date -Iseconds) pid=$$ cwd=$PWD cmd=$*" >>"$LOCKDIR/holders"
export CARGO_BUILD_JOBS="$JOBS" RUST_TEST_THREADS="$JOBS" \
VITEST_MAX_THREADS="$JOBS" VITEST_MAX_FORKS="$JOBS" \
MAKEFLAGS="-j$JOBS" GOMAXPROCS="$JOBS" \
NODE_OPTIONS="${NODE_OPTIONS:-} --max-old-space-size=2048"
if command -v systemd-run >/dev/null 2>&1 && \
systemd-run --user --scope -q -p MemoryMax=64M true >/dev/null 2>&1; then
systemd-run --user --scope -q --collect \
-p "MemoryMax=$MEM_MAX" -p "MemorySwapMax=0" -p "CPUQuota=$CPU_QUOTA" \
-- "$@"
else
"$@"
fi
rc=$?
exec {fd}>&-
exit $rc