The init is asked for start and restart; a launcher does the rest

ADR 0061. Recovery was the most systemd-specific part of the host, and it is
the part that must work on a machine where nothing else does -- which made
unit-file syntax a poor place for it, because syntax cannot be tested and the
one time it runs is the one time nobody can afford it wrong.

So StartLimitBurst and OnFailure move into a launcher script that init starts
instead of the host. The unit drops to start-at-boot and restart-on-exit, which
OpenRC, runit, s6 and an Android init.rc can all express. Everything 0059
decided is kept: two watchdogs, roll back once, recovery is local, the rollback
shares no code with the host.

The counter is the whole mechanism, so it is what the tests are mostly about.
Three real problems came out of writing them:

A counter file holding "1 2" became "12" -- `tr -d [:space:]` concatenates
rather than rejecting -- which is past the limit, so a HEALTHY node rolled
itself back. Now it reads the first field and insists on a plain integer.

The corrupt-counter test used "not-a-number", which shell arithmetic happens to
evaluate to 0, so it passed with the guard removed and proved nothing. Replaced
with values that discriminate: "5x" errors under set -e and kills the launcher,
and "0x10" is read as HEX 16 -- past the limit, so again a healthy node rolls
back.

And the test harness itself was wrong. With `set -e` and a bare launcher call,
removing a guard killed the script at the first corrupt case and silently
skipped everything after -- reporting a full pass over tests that never ran.
Every launcher call now records its failure instead of aborting. Same class as
the placebo assertion found last time, and the reason to keep injecting faults
rather than trusting green.

Both scripts run in `make check`. 27 launcher tests, 9 rollback tests, all
confirmed to bite.
This commit is contained in:
2026-08-27 23:45:57 +02:00
parent f4143806c2
commit 057f34f924
7 changed files with 222 additions and 28 deletions
+5 -7
View File
@@ -46,10 +46,10 @@ touch "$MESH_HOST_PKG_CACHE/nox-mesh-host-1.4.2-1-x86_64.pkg.tar.zst"
"$SCRIPT" >/dev/null 2>&1
check "installs the known-good version" "pacman is asked to install the cached package" \
"$(grep -c 'nox-mesh-host-1.4.2' "$MESH_HOST_STATE_DIR/pacman.calls" 2>/dev/null || echo 0)" "1"
check "resets the start limit before starting" "reset-failed precedes start" \
"$(head -1 "$MESH_HOST_STATE_DIR/systemctl.calls" | cut -d' ' -f1)" "reset-failed"
check "starts the host again" "systemctl start is called" \
"$(grep -c '^start ' "$MESH_HOST_STATE_DIR/systemctl.calls" 2>/dev/null || echo 0)" "1"
# It installs and stops. The launcher execs the host next, and starting it here would run two
# (novox/hq ADR 0061).
check "does not start anything itself" "the launcher owns starting" \
"$([ -f "$MESH_HOST_STATE_DIR/systemctl.calls" ] && echo started || echo not-started)" "not-started"
check "records that it rolled back" "the attempted marker holds the version" \
"$(cat "$MESH_HOST_STATE_DIR/rollback-attempted" 2>/dev/null || echo MISSING)" "1.4.2"
@@ -90,9 +90,7 @@ echo "1.4.2" > "$MESH_HOST_STATE_DIR/known-good"
touch "$MESH_HOST_PKG_CACHE/nox-mesh-host-1.4.2-1-x86_64.pkg.tar.zst"
STUB_PACMAN_FAILS=1 ; export STUB_PACMAN_FAILS
set +e; "$SCRIPT" >/dev/null 2>&1; RC=$?; set -e
check "pacman fails: does not start the host" "starting the broken binary again would loop" \
"$([ -f "$MESH_HOST_STATE_DIR/systemctl.calls" ] && echo started || echo not-started)" "not-started"
check "pacman fails: exits non-zero" "a failed rollback is a failure" "$RC" "1"
check "pacman fails: exits non-zero" "a failed rollback is a failure the launcher must see" "$RC" "1"
printf '\nrollback: %d passed, %d failed\n' "$PASS" "$FAIL"
[ "$FAIL" -eq 0 ]