diff --git a/CLAUDE.md b/CLAUDE.md index eb9860b6..8eb4d7ba 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -33,8 +33,16 @@ Work directly on `main`; commit and push straight there. ./install.sh # deps + pre-commit hooks, then verifies and fails loudly ./run.sh check # the full local gate ./run.sh status # probe every course dir, regenerate DOCS-runnability.md +./run.sh run # build/run one course, capped at 2G RAM and 2 cores ``` +`run` executes inside a transient systemd scope (`MemoryMax=2G`, +`MemorySwapMax=0`, `CPUQuota=200%`, `nice -n 19`, `ionice -c 3`), so a course +build cannot make an interactive machine stutter. This is not caution, it is a +kernel-enforced ceiling -- PORR's suite solves 10000x10000 systems and will +take every core it is given. Raise it deliberately for a single run with +`RUN_MEMORY_MAX=8G RUN_CPU_QUOTA=800% ./run.sh run `. + Never `--no-verify`. If a gate fails, fix the cause. ## Development Workflow diff --git a/TODO-refactor-remaining.md b/TODO-refactor-remaining.md index f25653f1..6632e906 100644 --- a/TODO-refactor-remaining.md +++ b/TODO-refactor-remaining.md @@ -70,6 +70,16 @@ tests exist** -- splitting untested code is the highest-risk action here. will touch hundreds of submitted coursework files, so they want their own reviewed commit rather than being buried in a config change. +### 6. Two courses still fail `run` + +- `Programming/PORR` -- its pytest suite solves systems up to 10000x10000 and + runs for many minutes at full CPU. Under the 2G/2-core cap it will be + OOM-killed rather than finish. It needs either a smaller default parameter + set for `run`, or to be marked as a deliberately heavy target. +- `Programming/ZPOB` -- latexmk exits 12 from bibtex even though `esej.pdf` is + produced. Probably a missing/!stale .bib entry; needs a clean-room rebuild + to see the first real error rather than the cached one. + ## Standing rule A gate lands only once the thing it gates is clean, so diff --git a/run.sh b/run.sh index 1479532c..80b87699 100755 --- a/run.sh +++ b/run.sh @@ -22,6 +22,16 @@ readonly SCRIPT_DIR readonly RUNNABILITY_DOC="${SCRIPT_DIR}/DOCS-runnability.md" # Exit codes a per-directory run.sh uses to report itself. +# Resource ceiling for `run`. A course build is background work and must never +# be what makes an interactive machine stutter -- PORR's suite solves +# 10000x10000 systems and happily eats every core it is given. +# +# Same mechanism as the pre-push hook in testsAndMisc: a transient systemd +# scope, so the limit is enforced by the kernel rather than by good intentions. +# MemorySwapMax=0 means it gets OOM-killed instead of thrashing swap. +readonly RUN_MEMORY_MAX="${RUN_MEMORY_MAX:-2G}" +readonly RUN_CPU_QUOTA="${RUN_CPU_QUOTA:-200%}" + readonly EXIT_BLOCKED=78 readonly EXIT_NO_CODE=79 @@ -142,8 +152,22 @@ cmd_run() { [[ -n "$target" ]] || { err "run needs a directory"; usage 1; } local runner="${SCRIPT_DIR}/${target}/run.sh" [[ -x "$runner" ]] || { err "no executable run.sh in $target"; return 1; } - log "running $target" - "$runner" + log "running $target (capped at ${RUN_MEMORY_MAX} RAM, ${RUN_CPU_QUOTA} CPU)" + _run_capped "$runner" +} + +# Run a command under a memory/CPU cap, niced so it yields to anything +# interactive. Falls back to plain nice if systemd-run is unavailable. +_run_capped() { + if command -v systemd-run > /dev/null 2>&1; then + systemd-run --user --scope --quiet \ + -p "MemoryMax=${RUN_MEMORY_MAX}" \ + -p MemorySwapMax=0 \ + -p "CPUQuota=${RUN_CPU_QUOTA}" \ + nice -n 19 ionice -c 3 "$@" + else + nice -n 19 ionice -c 3 "$@" + fi } cmd_sync_vendored() {