fix: the timeout path probes the instance, tells the two stories apart, and cleans up

Round-1 consensus on #94: timeout only proves the CLIENT overran the
budget. incus launch is create-then-start, so a slow-but-progressing
launch may already have registered the instance — the old message claimed
'never created' unconditionally and the advised retry would collide with
'Instance already exists'. The 124/137 path now probes 'incus info',
narrates the branch it found (true #93 wedge vs slow-launch overrun),
best-effort 'incus delete --force's either way so the retry is clean in
both worlds, hedges 137 as possibly an outside kill, and BOX_LAUNCH_TIMEOUT
is documented in 'box help new' beside the other knobs. Both branches
driven live against a shim incus; four new grep-proof checks pin the probe,
the cleanup, the overrun story, and the help text.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
dan-claude-bot 2026-07-19 12:52:41 +00:00
parent 8ab9b38ba2
commit 4da487e388
2 changed files with 48 additions and 9 deletions

36
bin/box
View file

@ -267,6 +267,9 @@ template's box.env, then defaults. Flags shape a fresh mint only — a --from
clone carries its source's resources. Resources are all a flag can touch: clone carries its source's resources. Resources are all a flag can touch:
there is no flag for a network or a security key, on purpose. there is no flag for a network or a security key, on purpose.
BOX_LAUNCH_TIMEOUT=<seconds> (default 600) bounds the 'incus launch' call —
a launch that overruns it fails loudly instead of hanging forever (#93).
box new --name scratch # blank, the default box new --name scratch # blank, the default
box new --name work --template claude box new --name work --template claude
box new --name lean --template claude --cpu 2 --memory 3GiB box new --name lean --template claude --cpu 2 --memory 3GiB
@ -1040,16 +1043,35 @@ cmd_new() {
--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[@]}" </dev/null || rc=$? "${extra[@]}" </dev/null || rc=$?
# 124 = the budget fired (TERM landed); 137 = the -k KILL was needed. # 124 = the budget fired (TERM landed); 137 = the -k KILL was needed —
# or, on 137, something external (an OOM kill) beat the budget to it.
if [ "$rc" -eq 124 ] || [ "$rc" -eq 137 ]; then if [ "$rc" -eq 124 ] || [ "$rc" -eq 137 ]; then
echo >&2 echo >&2
echo "box: 'incus launch' WEDGED — killed after ${budget}s, and the instance was never created." >&2 # timeout only proves the CLIENT overran the budget. 'incus launch' is
echo "box: this is the #93 failure: the incus client hangs with NO server-side operation" >&2 # create-then-start, so a slow-but-progressing launch (first mint
echo "box: ('incus operation list' is empty, the daemon journal is quiet). An immediate" >&2 # pulling an uncached image, say) may already have REGISTERED the
echo "box: retry of the exact same 'box new' has been observed to succeed, both times it" >&2 # instance — in which case "never created, retry" would be exactly
echo "box: was measured. If it persists, diagnose the host: box doctor" >&2 # wrong: the retry collides with 'Instance already exists'. Probe, say
# which case this is, and best-effort delete either way (a no-op on
# the true #93 wedge, the cleanup on an overrun; also covers a create
# that lands in the race between the probe and the delete) so the
# retry advice below is safe in BOTH worlds. Review consensus on the
# first round of #94: all three reviewers converged on this hole.
if timeout -k 5 15 incus info "$instance" </dev/null >/dev/null 2>&1; then
echo "box: 'incus launch' OVERRAN its ${budget}s budget (killed) — but the instance WAS" >&2
echo "box: registered: this looks like a slow launch, not the #93 client wedge. Removing" >&2
echo "box: the partial instance so a retry starts clean..." >&2
else
echo "box: 'incus launch' WEDGED — killed after ${budget}s (or killed from outside), and" >&2
echo "box: the instance was never created. This is the #93 failure: the incus client" >&2
echo "box: hangs with NO server-side operation ('incus operation list' is empty, the" >&2
echo "box: daemon journal is quiet). An immediate retry of the exact same 'box new' has" >&2
echo "box: been observed to succeed, both times it was measured." >&2
fi
timeout -k 5 30 incus delete --force "$instance" </dev/null >/dev/null 2>&1 || true
echo "box: if it persists, diagnose the host: box doctor" >&2
echo "box: (a genuinely slower mint can raise the budget: BOX_LAUNCH_TIMEOUT=<seconds>)" >&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)" die "incus launch did not finish inside ${budget}s — retry the same command (#93)"
elif [ "$rc" -ne 0 ]; then elif [ "$rc" -ne 0 ]; then
# Not a wedge: incus refused and said why on stderr, right above. # Not a wedge: incus refused and said why on stderr, right above.
die "incus launch failed (exit $rc)" die "incus launch failed (exit $rc)"

View file

@ -403,8 +403,25 @@ check "new: the launch pins stdin (RUNS.md trap 13)" 0 "" bash -c '
check "new: the wedge failure is loud — retry hint, the doctor, and #93" 0 "" bash -c ' check "new: the wedge failure is loud — retry hint, the doctor, and #93" 0 "" bash -c '
fn="$(awk "/^cmd_new\(\) \{/,/^\}/" "'"$ROOT"'/bin/box")" 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 "observed to succeed" &&
printf "%s\n" "$fn" | grep -A6 "WEDGED" | grep -q "box doctor" && printf "%s\n" "$fn" | grep -q "box doctor" &&
printf "%s\n" "$fn" | grep "incus launch wedged" | grep -q "#93"' printf "%s\n" "$fn" | grep "did not finish inside" | grep -q "#93"'
# timeout proves only that the CLIENT overran the budget: launch is
# create-then-start, so a slow launch may have REGISTERED the instance and a
# blind "never created, retry" would send the operator into 'Instance already
# exists' (#94 round-1, all three reviewers). The timeout path must probe the
# instance, tell the two stories apart, and best-effort delete either way so
# the retry advice is safe in both worlds.
check "new: the timeout path probes before claiming never-created (#94 r1)" 0 "" bash -c '
awk "/^cmd_new\(\) \{/,/^\}/" "'"$ROOT"'/bin/box" \
| grep "incus info" | grep -q "\$instance"'
check "new: the timeout path best-effort deletes, so retry is always clean" 0 "" bash -c '
awk "/^cmd_new\(\) \{/,/^\}/" "'"$ROOT"'/bin/box" \
| grep "incus delete --force" | grep -q "|| true"'
check "new: the overran-but-registered branch says so (not the wedge story)" 0 "" bash -c '
awk "/^cmd_new\(\) \{/,/^\}/" "'"$ROOT"'/bin/box" \
| grep "OVERRAN" | grep -q "budget"'
check "new: BOX_LAUNCH_TIMEOUT is documented in box help new" 0 "" bash -c '
"'"$ROOT"'/bin/box" help new | grep "BOX_LAUNCH_TIMEOUT" | grep -q 600'
# 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.