fix: narrate and time-box the incus launch — a wedge fails loudly, not forever (#93)
Twice in the 2026-07-19 release drill (Debian 13, Incus 6.x, /dev/kvm present, images cached), the child 'incus launch' under 'box new' wedged with no server-side operation: 'incus operation list' empty, the instance never created, the daemon journal quiet — one wedge ran 56 minutes before being killed by hand, the other was killed by a 540s wrapper. An immediate retry of the identical command succeeded in ~2-3 minutes, both times. box inherited that as an indefinite silent hang, indistinguishable from a cold mint working. The mint now prints "launching instance ..." before the call, and the call rides 'timeout -k 5 $BOX_LAUNCH_TIMEOUT' (seconds, default 600 — generous: the coldest measured mint is minutes, never an hour; overridable the same way BOX_CPU/BOX_MEMORY are), with stdin pinned per drill/RUNS.md trap 13. When the budget fires (124, or 137 when the KILL was needed) the failure says exactly what was measured — the client wedged with no server-side operation, an immediate retry has been observed to succeed — and points at 'box doctor' for host state. A non-timeout launch failure still surfaces incus's own stderr. The --from clone path is untouched: 'incus copy' of a local instance is a different operation and has never been observed to wedge this way. Proven the way the other mint-path guards are (a daemon-free run cannot mint): test/cli.sh greps that the narration orders before the launch, that the launch sits under 'timeout -k' with the BOX_LAUNCH_TIMEOUT budget and pinned stdin, and that the wedge message carries the retry hint, the doctor, and #93 — plus a live shim-incus drive of all three exits (wedge, plain refusal, success) during development. Fixes #93 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
e40d82afa6
commit
8ab9b38ba2
3 changed files with 81 additions and 2 deletions
18
CHANGELOG.md
18
CHANGELOG.md
|
|
@ -204,6 +204,24 @@ which records not just what changed but what each drill run proved.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
- **A wedged `incus launch` fails loudly, not forever — the mint's launch
|
||||||
|
phase is narrated and time-boxed** (#93) — twice in the 2026-07-19
|
||||||
|
release drill (Debian 13, Incus 6.x, /dev/kvm present, images cached),
|
||||||
|
the child `incus launch` under `box new` hung with *no server-side
|
||||||
|
operation*: `incus operation list` empty, the instance never created, the
|
||||||
|
daemon journal quiet — one wedge ran 56 minutes before being killed by
|
||||||
|
hand, and an immediate retry of the identical command succeeded in
|
||||||
|
minutes, both times. `box new` inherited that as an indefinite silent
|
||||||
|
hang, indistinguishable from a cold mint working. It now prints
|
||||||
|
`launching instance …` before the call, and the call rides
|
||||||
|
`timeout -k 5 $BOX_LAUNCH_TIMEOUT` (seconds, default 600 — generous: the
|
||||||
|
coldest measured mint is minutes, never an hour; the same scripting-knob
|
||||||
|
shape as `BOX_CPU`/`BOX_MEMORY`), with stdin pinned per the drill's own
|
||||||
|
trap list. On the budget firing it says exactly what was measured — the
|
||||||
|
client wedged with no server-side operation, an immediate retry has been
|
||||||
|
observed to succeed — and points at `box doctor` for the host. The
|
||||||
|
`--from` clone path is untouched: `incus copy` of a local instance is a
|
||||||
|
different operation and has never been observed to wedge this way.
|
||||||
- **UFW's gateway carve-out converges with the bridge, and the doctor can
|
- **UFW's gateway carve-out converges with the bridge, and the doctor can
|
||||||
see it** (the #86 review's blind spot) — `box-firewall` gated its whole
|
see it** (the #86 review's blind spot) — `box-firewall` gated its whole
|
||||||
UFW block behind "a `DENY on boxnet` rule exists", pinning every UFW host
|
UFW block behind "a `DENY on boxnet` rule exists", pinning every UFW host
|
||||||
|
|
|
||||||
33
bin/box
33
bin/box
|
|
@ -1017,14 +1017,43 @@ cmd_new() {
|
||||||
# The template's identity is stamped ONTO the instance: which template,
|
# The template's identity is stamped ONTO the instance: which template,
|
||||||
# which user. 'incus copy' preserves user.* keys (audit B2), so a clone
|
# which user. 'incus copy' preserves user.* keys (audit B2), so a clone
|
||||||
# knows what it is without ever consulting the template again.
|
# knows what it is without ever consulting the template again.
|
||||||
incus launch "$T_IMAGE" "$instance" --profile box-net \
|
#
|
||||||
|
# The launch is narrated and TIME-BOXED (#93). Twice in the 2026-07-19
|
||||||
|
# release drill the child 'incus launch' wedged before the create was
|
||||||
|
# even accepted — 'incus operation list' empty, the instance never
|
||||||
|
# existed, the daemon journal quiet — once for 56 minutes until killed
|
||||||
|
# by hand. Without a line here that wedge reads exactly like a cold mint
|
||||||
|
# working; without a budget it lasts forever. Ten minutes is generous —
|
||||||
|
# the coldest measured mint (first VM on a fresh pool, see wait_agent)
|
||||||
|
# is minutes, never an hour — and BOX_LAUNCH_TIMEOUT (seconds) overrides
|
||||||
|
# it, the same scripting knob shape as BOX_CPU / BOX_MEMORY. The drill's
|
||||||
|
# lore applies verbatim (RUNS.md trap 13): 'timeout -k' so a launch that
|
||||||
|
# shrugs off TERM still dies, and stdin pinned like every other
|
||||||
|
# non-interactive incus call — only shell/exec/tmux may own the terminal.
|
||||||
|
local budget="${BOX_LAUNCH_TIMEOUT:-600}" rc=0
|
||||||
|
echo "box: launching instance $instance (incus launch, $m mode)..."
|
||||||
|
timeout -k 5 "$budget" incus launch "$T_IMAGE" "$instance" --profile box-net \
|
||||||
--config user.box=1 \
|
--config user.box=1 \
|
||||||
--config user.box.template="$t" \
|
--config user.box.template="$t" \
|
||||||
--config user.box.user="$T_USER" \
|
--config user.box.user="$T_USER" \
|
||||||
--config limits.cpu="$T_CPU" \
|
--config limits.cpu="$T_CPU" \
|
||||||
--config limits.memory="$T_MEMORY" \
|
--config limits.memory="$T_MEMORY" \
|
||||||
--config cloud-init.user-data="$(render_userdata "$root/templates/$t/user-data.yaml")" \
|
--config cloud-init.user-data="$(render_userdata "$root/templates/$t/user-data.yaml")" \
|
||||||
"${extra[@]}"
|
"${extra[@]}" </dev/null || rc=$?
|
||||||
|
# 124 = the budget fired (TERM landed); 137 = the -k KILL was needed.
|
||||||
|
if [ "$rc" -eq 124 ] || [ "$rc" -eq 137 ]; then
|
||||||
|
echo >&2
|
||||||
|
echo "box: 'incus launch' WEDGED — killed after ${budget}s, and the instance was never created." >&2
|
||||||
|
echo "box: this is the #93 failure: the incus client hangs with NO server-side operation" >&2
|
||||||
|
echo "box: ('incus operation list' is empty, the daemon journal is quiet). An immediate" >&2
|
||||||
|
echo "box: retry of the exact same 'box new' has been observed to succeed, both times it" >&2
|
||||||
|
echo "box: was measured. If it persists, diagnose the host: box doctor" >&2
|
||||||
|
echo "box: (a genuinely slower mint can raise the budget: BOX_LAUNCH_TIMEOUT=<seconds>)" >&2
|
||||||
|
die "incus launch wedged with no server-side operation — retry the same command (#93)"
|
||||||
|
elif [ "$rc" -ne 0 ]; then
|
||||||
|
# Not a wedge: incus refused and said why on stderr, right above.
|
||||||
|
die "incus launch failed (exit $rc)"
|
||||||
|
fi
|
||||||
wait_agent "$instance"
|
wait_agent "$instance"
|
||||||
echo "box: waiting for phase-1 (cloud-init)..."
|
echo "box: waiting for phase-1 (cloud-init)..."
|
||||||
echo "box: (its full narration, live: incus exec $name -- tail -f /var/log/cloud-init-output.log)"
|
echo "box: (its full narration, live: incus exec $name -- tail -f /var/log/cloud-init-output.log)"
|
||||||
|
|
|
||||||
32
test/cli.sh
32
test/cli.sh
|
|
@ -373,6 +373,38 @@ check "new: the auto-run sits under the T_BOOTSTRAP_ROLE guard" 0 "" bash -c '
|
||||||
check "new: a failed tenant role names the re-run (the role converges)" 0 "" bash -c '
|
check "new: a failed tenant role names the re-run (the role converges)" 0 "" bash -c '
|
||||||
awk "/^cmd_new\(\) \{/,/^\}/" "'"$ROOT"'/bin/box" \
|
awk "/^cmd_new\(\) \{/,/^\}/" "'"$ROOT"'/bin/box" \
|
||||||
| grep -q "sudo rig bootstrap"'
|
| grep -q "sudo rig bootstrap"'
|
||||||
|
|
||||||
|
# The launch phase, narrated and time-boxed (#93) — grepped the way the other
|
||||||
|
# mint-path guards are (a daemon-free run cannot mint). Twice in the
|
||||||
|
# 2026-07-19 release drill the child 'incus launch' wedged silently before
|
||||||
|
# the create was even accepted, once for 56 minutes. The narration must order
|
||||||
|
# BEFORE the launch call (a wedge after the line is visible at a glance; a
|
||||||
|
# wedge before it is the old silent hang), the call itself must sit under
|
||||||
|
# 'timeout -k' with the BOX_LAUNCH_TIMEOUT override and pinned stdin (RUNS.md
|
||||||
|
# trap 13: bare 'timeout N' cannot kill an incus call that owns a TTY), and
|
||||||
|
# the budget's failure must be LOUD — no server-side operation, the measured
|
||||||
|
# retry-succeeds hint, and the doctor as the next move.
|
||||||
|
# shellcheck disable=SC2016 # the $-strings are literals in the target file
|
||||||
|
check "new: the launch narration orders before incus launch (#93)" 0 "" bash -c '
|
||||||
|
fn="$(awk "/^cmd_new\(\) \{/,/^\}/" "'"$ROOT"'/bin/box")"
|
||||||
|
say="$(printf "%s\n" "$fn" | grep -n "launching instance" | head -1 | cut -d: -f1)"
|
||||||
|
run="$(printf "%s\n" "$fn" | grep -n "timeout -k.*incus launch" | head -1 | cut -d: -f1)"
|
||||||
|
[ -n "$say" ] && [ -n "$run" ] && [ "$say" -lt "$run" ]'
|
||||||
|
check "new: incus launch is time-boxed (timeout -k on the budget)" 0 "" bash -c '
|
||||||
|
awk "/^cmd_new\(\) \{/,/^\}/" "'"$ROOT"'/bin/box" \
|
||||||
|
| grep "timeout -k" | grep "budget" | grep -q "incus launch"'
|
||||||
|
check "new: the budget is BOX_LAUNCH_TIMEOUT, default 600s (the BOX_CPU knob shape)" 0 "" bash -c '
|
||||||
|
awk "/^cmd_new\(\) \{/,/^\}/" "'"$ROOT"'/bin/box" \
|
||||||
|
| grep "budget=" | grep -q "BOX_LAUNCH_TIMEOUT:-600"'
|
||||||
|
check "new: the launch pins stdin (RUNS.md trap 13)" 0 "" bash -c '
|
||||||
|
awk "/^cmd_new\(\) \{/,/^\}/" "'"$ROOT"'/bin/box" \
|
||||||
|
| grep -F "extra[@]" | grep -qF "</dev/null"'
|
||||||
|
# shellcheck disable=SC2016 # the $-strings are literals in the target file
|
||||||
|
check "new: the wedge failure is loud — retry hint, the doctor, and #93" 0 "" bash -c '
|
||||||
|
fn="$(awk "/^cmd_new\(\) \{/,/^\}/" "'"$ROOT"'/bin/box")"
|
||||||
|
printf "%s\n" "$fn" | grep -A6 "WEDGED" | grep -q "observed to succeed" &&
|
||||||
|
printf "%s\n" "$fn" | grep -A6 "WEDGED" | grep -q "box doctor" &&
|
||||||
|
printf "%s\n" "$fn" | grep "incus launch wedged" | grep -q "#93"'
|
||||||
# staging's creds-holding join stays OPERATOR-run: cmd_new may print it as a
|
# staging's creds-holding join stays OPERATOR-run: cmd_new may print it as a
|
||||||
# next step, but no template and no code path auto-runs "rig bootstrap
|
# next step, but no template and no code path auto-runs "rig bootstrap
|
||||||
# workload" — the one absence that keeps box creds-free end to end.
|
# workload" — the one absence that keeps box creds-free end to end.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue