diff --git a/CLAUDE.md b/CLAUDE.md index 22ad014..3bdeb6e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -70,41 +70,51 @@ and failed to verify it was gone before moving on. The first crash added this discipline section; the second proved the discipline needed teeth. Hence the MANDATORY checklist below. -**MANDATORY pattern for any asm server test in a shell block:** +**Shared-machine context:** other agents run on this box. An OOM +crash is not just "my problem" — it takes their state down too. +The rules below are belt, suspenders, AND a parachute so that even +if two safeguards fail, the kernel itself backstops. + +**MANDATORY pattern — every asm/uncommonlisp test in a shell block:** ```bash -# 1. set -e so any failure aborts before leaving a zombie -set -e -# 2. trap ALWAYS fires, even if a later command fails or the script is killed -trap 'pkill -9 -u "$USER" -f "examples/http-server|asm/uncommonlisp.*bench" 2>/dev/null || true' EXIT INT TERM -# 3. wrap the server in `timeout` so even a total trap failure has a hard ceiling -timeout 30 asm/uncommonlisp < server.lsp & +set -e # (1) fail-fast +ulimit -v 524288 # (2) KERNEL CAP: 512 MB virt + # process gets SIGKILL at cap, no matter what +trap 'pkill -9 -u "$USER" -f "examples/http-server|asm/uncommonlisp" 2>/dev/null || true' \ + EXIT INT TERM # (3) cleanup always fires + +timeout 30 asm/uncommonlisp < server.lsp & # (4) wall-clock ceiling SPID=$! -# 4. do the work ... (client requests, RSS measurements, etc.) -# 5. explicit kill + wait, not just "assume the trap handles it" -kill -9 $SPID 2>/dev/null; wait $SPID 2>/dev/null -# 6. VERIFY before ending the block -pgrep -u "$USER" -f 'asm/uncommonlisp|examples/http-server' && exit 1 || true +# ... do the work (curl requests, measurements, etc.) ... +kill -9 $SPID 2>/dev/null; wait $SPID 2>/dev/null # (5) explicit cleanup + +# (6) VERIFY the block is clean before moving on +pgrep -u "$USER" -f 'asm/uncommonlisp|examples/http-server' \ + && { echo "STRAGGLER"; exit 1; } || true ``` +Six layers. Bypass any one and the next still catches. Two crashes +happened when I had only layers 3-5; the kernel cap (2) is what turns +"if I forget" from "fox reboots" into "my one rogue process dies at +512 MB without touching shared RAM." + Additional rules: -- Never start a bg asm process without `timeout N` wrapping it, even - for a "quick test." The mental overhead is zero; the consequence - of forgetting is a crashed machine. -- Bound iterations **inside the .lsp** (e.g., `(if (> n 1000) (exit 0) - (loop (+ n 1)))`) — never rely on external kill signals alone. -- After any block that spawned background processes, run: - `pgrep -u "$USER" -f 'asm/uncommonlisp|examples/http-server'` - and expect NO output. If output, kill with `pkill -9 -f ...` and - investigate. Do not move on. -- Use `examples/http-server.lsp`'s built-in `*max-requests* = 50000` - ceiling (never comment out, never raise for long-running tests). -- Python + C have real GCs; the hazard is asm-specific. But treat - backgrounded Python + C servers with the same hygiene — they - block ports and confuse later tests. +- `ulimit -v` affects only the shell it runs in and its children, so + it cannot degrade anyone else's agents. Always set it before + backgrounding any uncommonlisp process. +- Bound iterations **inside the .lsp** (e.g. `*max-requests* = 50000` + in `examples/http-server.lsp`). Never raise for long-running tests. +- Use `pkill -u "$USER" -f ` not `pkill` alone — others may + have their own processes on this machine. +- Prefer FOREGROUND runs when possible: `timeout 10 asm/uncommonlisp + < test.lsp` with the `.lsp` exiting on its own beats backgrounding. - C has Boehm GC via `GC_MALLOC`. Python has Python's GC. Asm has neither. Risk scales with how long the asm process lives. +- If `ulimit` is unavailable (some container setups), use + `systemd-run --user --scope -p MemoryMax=512M -- asm/uncommonlisp ...` + as the equivalent cgroup-based cap. ## MOAD Scanner