guardrails: bound http server, trap+cleanup bench, asm-no-GC in CLAUDE.md
Prevent recurrence of 2026-04-16 incident where a leaked asm HTTP server grew to 19.3 GB RSS and crashed the machine. examples/http-server.lsp: - Adds *max-requests* = 50000 hard ceiling. Server self-terminates before unbounded heap growth reaches dangerous levels. - Loop tracks request count, exits cleanly + closes server socket. tests/web-benchmark.sh: - SPAWNED_PIDS array tracks every background process. - EXIT/INT/TERM trap kills them all (SIGTERM then SIGKILL). - stop_server does SIGTERM with 500ms grace period then SIGKILL. - Final straggler check via pgrep narrows to actual HTTP server processes (not shell/tmux with "uncommonlisp" in the name). - pkill -9 fallback as belt-and-suspenders. CLAUDE.md: - New "Asm memory discipline" section documents the bump allocator leak behavior and the required operational discipline. - Test counts updated (571 py + 83 c + 132 asm + 189 shared = 975).
This commit is contained in:
parent
bfd4ec7ec8
commit
477bd5f7cd
3 changed files with 104 additions and 23 deletions
|
|
@ -16,6 +16,35 @@ PY="python3 uncommonlisp.py --fast"
|
|||
C="./c/uncommonlisp"
|
||||
ASM="./asm/uncommonlisp"
|
||||
|
||||
# Track every server PID we spawn; the EXIT trap kills them all.
|
||||
# Asm has no GC — a leaked server leaks 64 MB per heap growth
|
||||
# indefinitely. Belt + suspenders: kill the whole process group too.
|
||||
declare -a SPAWNED_PIDS=()
|
||||
|
||||
cleanup() {
|
||||
local pid
|
||||
for pid in "${SPAWNED_PIDS[@]}"; do
|
||||
kill "$pid" 2>/dev/null
|
||||
done
|
||||
sleep 0.2
|
||||
for pid in "${SPAWNED_PIDS[@]}"; do
|
||||
kill -9 "$pid" 2>/dev/null
|
||||
done
|
||||
rm -rf "${STATIC_DIR:-/nonexistent/xyz}"
|
||||
# Final sanity: any stray servers from examples/http-server?
|
||||
# Narrow to processes actually serving the HTTP example, not the
|
||||
# shell running this script or tmux sessions with "uncommonlisp"
|
||||
# in their name.
|
||||
local strays
|
||||
strays=$(pgrep -u "$USER" -f 'examples/http-server\.lsp|http\.server 8080' | wc -l)
|
||||
if [ "$strays" -gt 0 ]; then
|
||||
echo "WARNING: $strays HTTP server process(es) still running:" >&2
|
||||
pgrep -u "$USER" -af 'examples/http-server\.lsp|http\.server 8080' >&2
|
||||
pkill -9 -u "$USER" -f 'examples/http-server\.lsp|http\.server 8080' 2>/dev/null
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
pad() { printf " %-36s " "$1"; }
|
||||
|
||||
bench_one() {
|
||||
|
|
@ -38,18 +67,19 @@ print(f'{rps:8.0f} req/s ({t:.3f}s total, concurrency $CONCURRENCY)')"
|
|||
# Setup a 1KB static file for file-serving benchmarks
|
||||
STATIC_DIR=$(mktemp -d)
|
||||
printf '%.0s0123456789abcdef0123456789abcdef' {1..32} > "$STATIC_DIR/bench"
|
||||
trap "rm -rf $STATIC_DIR" EXIT
|
||||
|
||||
echo "═══════════════════════════════════════════════════════════════"
|
||||
echo "HTTP benchmark — $REQUESTS requests, concurrency $CONCURRENCY"
|
||||
echo "═══════════════════════════════════════════════════════════════"
|
||||
echo
|
||||
|
||||
# Helper to start + wait for port ready
|
||||
# Helper to start + wait for port ready. Adds pid to SPAWNED_PIDS so
|
||||
# the EXIT trap kills it even if the script dies unexpectedly.
|
||||
start_server() {
|
||||
local cmd="$1" port="$2"
|
||||
eval "$cmd" > /dev/null 2>&1 &
|
||||
local pid=$!
|
||||
SPAWNED_PIDS+=("$pid")
|
||||
for _ in $(seq 1 20); do
|
||||
if curl -s -o /dev/null -w "%{http_code}" "http://localhost:$port/" 2>/dev/null | grep -q 200; then
|
||||
echo "$pid"
|
||||
|
|
@ -61,38 +91,50 @@ start_server() {
|
|||
return 1
|
||||
}
|
||||
|
||||
stop_server() {
|
||||
local pid="$1"
|
||||
# SIGTERM first; if the process is blocked in accept() it may not
|
||||
# exit cleanly, so force-kill after a short grace period.
|
||||
kill "$pid" 2>/dev/null
|
||||
for _ in 1 2 3 4 5; do
|
||||
kill -0 "$pid" 2>/dev/null || break
|
||||
sleep 0.1
|
||||
done
|
||||
kill -9 "$pid" 2>/dev/null
|
||||
wait "$pid" 2>/dev/null
|
||||
sleep 0.2
|
||||
}
|
||||
|
||||
# ── uncommonlisp Python ──
|
||||
PID=$(start_server "$PY examples/http-server.lsp" 8080)
|
||||
bench_one "uncommonlisp Python (/bench, 1 KB)" 8080 /bench
|
||||
kill "$PID" 2>/dev/null; wait "$PID" 2>/dev/null
|
||||
sleep 0.3
|
||||
stop_server "$PID"
|
||||
|
||||
# ── uncommonlisp C ──
|
||||
PID=$(start_server "$C examples/http-server.lsp" 8080)
|
||||
bench_one "uncommonlisp C (/bench, 1 KB)" 8080 /bench
|
||||
kill "$PID" 2>/dev/null; wait "$PID" 2>/dev/null
|
||||
sleep 0.3
|
||||
stop_server "$PID"
|
||||
|
||||
# ── uncommonlisp asm ──
|
||||
PID=$(start_server "$ASM < examples/http-server.lsp" 8080)
|
||||
bench_one "uncommonlisp asm (/bench, 1 KB)" 8080 /bench
|
||||
kill "$PID" 2>/dev/null; wait "$PID" 2>/dev/null
|
||||
sleep 0.3
|
||||
stop_server "$PID"
|
||||
|
||||
# ── Python http.server (stdlib) ──
|
||||
(cd "$STATIC_DIR" && python3 -m http.server 8080 > /dev/null 2>&1) &
|
||||
PID=$!
|
||||
SPAWNED_PIDS+=("$PID")
|
||||
sleep 1
|
||||
bench_one "python3 -m http.server (1 KB file)" 8080 /bench
|
||||
kill "$PID" 2>/dev/null; wait "$PID" 2>/dev/null
|
||||
sleep 0.3
|
||||
stop_server "$PID"
|
||||
|
||||
# ── busybox httpd ──
|
||||
busybox httpd -f -p 127.0.0.1:8080 -h "$STATIC_DIR" > /dev/null 2>&1 &
|
||||
PID=$!
|
||||
SPAWNED_PIDS+=("$PID")
|
||||
sleep 0.5
|
||||
bench_one "busybox httpd (1 KB file)" 8080 /bench
|
||||
kill "$PID" 2>/dev/null; wait "$PID" 2>/dev/null
|
||||
stop_server "$PID"
|
||||
|
||||
echo
|
||||
echo "═══════════════════════════════════════════════════════════════"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue