Cap run.sh run at 2G RAM and 2 cores
Some checks are pending
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (push) Waiting to run
runnability / runnability (push) Waiting to run

Running the courses made this machine stutter during a game. That was me:
PORR's suite solves systems up to 10000x10000 and takes every core it is
given, and the pip installs are not cheap either.

A promise to be careful is not a fix, so `run` now executes inside a transient
systemd scope -- MemoryMax=2G, MemorySwapMax=0 (OOM-kill rather than thrash
swap), CPUQuota=200%, nice -n 19, ionice -c 3. Same mechanism the pre-push
hook in testsAndMisc uses. Verified from inside the scope: memory.max reads
2147483648 and cpu.max reads "200000 100000", so the kernel is enforcing it
rather than the script hoping.

RUN_MEMORY_MAX and RUN_CPU_QUOTA override it for a single deliberate run.

Recorded the two courses that still fail: PORR, which will now be OOM-killed
under the cap and needs a smaller default parameter set, and ZPOB, where
latexmk exits 12 from bibtex despite producing the PDF.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0172dj34FDpao6wsix8ZuCzE
This commit is contained in:
Krzysztof Rudnicki 2026-08-28 19:05:24 +02:00
parent a8d873b4fd
commit d76e571400
3 changed files with 44 additions and 2 deletions

View File

@ -33,8 +33,16 @@ Work directly on `main`; commit and push straight there.
./install.sh # deps + pre-commit hooks, then verifies and fails loudly ./install.sh # deps + pre-commit hooks, then verifies and fails loudly
./run.sh check # the full local gate ./run.sh check # the full local gate
./run.sh status # probe every course dir, regenerate DOCS-runnability.md ./run.sh status # probe every course dir, regenerate DOCS-runnability.md
./run.sh run <dir> # 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 <dir>`.
Never `--no-verify`. If a gate fails, fix the cause. Never `--no-verify`. If a gate fails, fix the cause.
## Development Workflow ## Development Workflow

View File

@ -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 will touch hundreds of submitted coursework files, so they want their own
reviewed commit rather than being buried in a config change. 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 ## Standing rule
A gate lands only once the thing it gates is clean, so A gate lands only once the thing it gates is clean, so

28
run.sh
View File

@ -22,6 +22,16 @@ readonly SCRIPT_DIR
readonly RUNNABILITY_DOC="${SCRIPT_DIR}/DOCS-runnability.md" readonly RUNNABILITY_DOC="${SCRIPT_DIR}/DOCS-runnability.md"
# Exit codes a per-directory run.sh uses to report itself. # 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_BLOCKED=78
readonly EXIT_NO_CODE=79 readonly EXIT_NO_CODE=79
@ -142,8 +152,22 @@ cmd_run() {
[[ -n "$target" ]] || { err "run needs a directory"; usage 1; } [[ -n "$target" ]] || { err "run needs a directory"; usage 1; }
local runner="${SCRIPT_DIR}/${target}/run.sh" local runner="${SCRIPT_DIR}/${target}/run.sh"
[[ -x "$runner" ]] || { err "no executable run.sh in $target"; return 1; } [[ -x "$runner" ]] || { err "no executable run.sh in $target"; return 1; }
log "running $target" log "running $target (capped at ${RUN_MEMORY_MAX} RAM, ${RUN_CPU_QUOTA} CPU)"
"$runner" _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() { cmd_sync_vendored() {