live: heartbeat sync 2026-10-18T16:47:59-07:00 (Refs #11)

This commit is contained in:
Frank committed 2026-10-18 16:47:59 -07:00
1 parent a550db6027
commit 979e9ef711
173 files changed
+3844 -57

No files matched your search

@@ -0,0 +1,63 @@
---
name: service-preflight-refusal-triage
description: "Diagnose a systemd service crash-looping in activating (auto-restart) with a fast exit: separate a preflight refusal from an engine crash, and make a runtime sysfs/kernel knob raise survive the reboot that resets it."
---
# Preflight refusal vs engine crash (systemd services with env gates)
Symptom that decides the branch: unit `activating (auto-restart)`, `Main process
exited, code=exited, status=1/FAILURE` about 50 ms after start, restart counter
in the thousands. No container/app process, no app log.
## Triage sequence
1. `systemctl status <svc> --no-pager -n 0` → note `Exec` path, `status=`, CPU-seconds
(`0.0xx ms` = the script never got to work).
2. `journalctl -u <svc> --no-pager -n 60` → the refusal line is printed by the
serve script itself and NAMES the gate. On frank-archon:
`ERROR: ttm pages cap is 125988 MiB, the pinned ledger needs >= 150000`.
`journalctl -u <svc> | grep -m1 "<gate phrase>"` gives the FIRST occurrence:
that timestamp is the incident start (correlate with `uptime -b`, `last reboot`).
3. Check the app's own state: `podman ps -a` showing the container name with a
zero/absurd start time = the container was never created → the gate, not the
engine.
4. Read the gate's code path in the repo (`scripts/serve-<svc>.sh`) before
touching the host: it prints exactly what it requires.
Do NOT reach for `journalctl -k` GPU/OOM theories, gdb, or a payload checksum
until the exit latency is above ~1 s. A 50 ms exit is a shell gate.
## The reboot law
Runtime kernel knobs reset at every boot. `ttm.pages_limit` (caps GPU-mappable
pinned pages) defaults to HALF OF RAM, so any manual `echo N > /sys/...` raise is
a countdown to the next reboot — a power failure included. Measured on dankserver
2026-10-12 23:17: raise to 52,428,800 pages (200 GiB) -> power failure -> kernel
default 32,253,039 pages (125,988 MiB) -> the pinned-ledger gate (floor 150,000)
refused the start 6,919 times in ~20 h.
So: never prescribe a manual sysfs write as the recovery procedure for a service
that can boot before a human reaches it. Make the START raise the knob.
## Making the raise privileged
The service is `User=dank`, the knob is root-owned 0644 → `[ -w "$knob" ]` is
FALSE in the service process and a plain `echo` fails silently. A "root unit"
with `User=` is NOT a root process.
- Canonical: `ExecStartPre=+/path/to/gate.sh --raise` — systemd's `+` prefix runs
as root even under `User=`, before ExecStart, so `Restart=always` can never
outrun it.
- Fallback for a human running the gate from a shell: `sudo -n sh -c "echo N > '$knob'"`.
- Gate order: read → if below floor, raise → RE-READ → refuse by name only after
a write that was refused. Reading = min over the knobs that exist (an absent
knob is skipped, never read as 0).
- Belt for the boot path: the module option (`/etc/modprobe.d/*.conf`,
`options ttm pages_limit=N`) — Dankclaw persists its raise that way.
## Verification of a knob-raise fix
- `cat /sys/module/ttm/parameters/pages_limit` after the boot equals the target.
- Journal shows the hook's own pid line (`ttm-preflight.sh[...]: ... raising the
knob to ...`) BEFORE `Started <svc>.service`.
- Confirm the live shape from `podman inspect <ctr> --format '{{json .Config.Cmd}}'`
— the restart succeeding proves nothing about the flags.
- Test the gate against a temp knob dir (env-overridable sysfs path): writable
below-floor knob → raised + exit 0; unwritable → cap unchanged + exit 1. Run the
refusal case with a PATH containing `cat`+`bash` and NO `sudo`, or root writes
the 444 file and the case tests nothing.