Files
felhom.eu/documentation/audits/v0262-live-2026-09-22/liveB2.py
T
admin 22439b0e43
gates / gates (push) Successful in 30s
v0.262.0 + v0.262.1 live on 9202: four scenarios proven, R-630/R-633/R-621/R-614 closed
A (R-630, the P1): paperless-ngx, same app same button - failed at +313.0s with the app stopped
under v0.261.0, done at +53.4s now, with no "no probe container" warning because the explicit
healthcheck.container resolved the target.

B (R-633): the busy guard fires - RemoveStack REFUSED (busy): a backup or restore is running - and
the live proof caught it answering HTTP 500, because router.go maps remove errors by grepping the
error TEXT. v0.262.1 makes it a typed error and a 409; re-proven live.

C (R-634 half): a half-state with app.yaml on disk and deployed=false answered 200, leftovers NONE.
Under v0.261.0 the same call said "not deployed".

F (R-614): phase done before the remove, no phase at all after redeploying the same name.

Also: the first B run proved NOTHING and nearly went down as a pass - the refusal came from the
pre-existing "still running" check, not the new guard. Recorded.

09 6.1 and 8.8, the capability map, and STATUS updated. R-625 and R-634's mechanism are named as
owed, not half-done. Register 325.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-22 21:53:53 +02:00

40 lines
1.8 KiB
Python

#!/usr/bin/env python3
"""B2 — prove the BUSY guard specifically (R-633).
B's first run refused the remove with `409 still running — stop it first`, which is the PRE-EXISTING
running check, not v0.262.0's new guard: the restore had already finished. To reach the new guard the
app must be STOPPED (so the running check passes) while the backup side is busy.
"""
import json, os, sys, time
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.join(os.path.dirname(HERE), "update-night-2026-09-21"))
import walk as w # noqa: E402
OUT = json.load(open(os.path.join(HERE, "live262.json")))
app, sub = "privatebin", "paste"
rec = {"scenario": "B2 (R-633) — the BUSY guard, reached deliberately", "app": app}
w.login()
w.deploy(app, sub)
w.wait_app(sub, "/", tries=30)
# stop it, so the pre-existing "still running" check cannot answer first
w.ctl("POST", f"/api/stacks/{app}/stop")
for _ in range(24):
time.sleep(5)
if w.stack(app).get("state") != "running":
break
rec["state_before"] = w.stack(app).get("state")
# box-wide backup: `POST /api/backup/run` holds the single-flight the guard consults
code, d = w.ctl("POST", "/api/backup/run")
rec["backup_started"] = {"http": code, "answer": str(d)[:120]}
time.sleep(3)
code, d = w.ctl("POST", f"/api/stacks/{app}/remove", {"remove_hdd_data": False, "remove_backups": False})
rec["remove_during_backup"] = {"http": code, "answer": d}
rec["refused_by_busy_guard"] = (code == "409" and "ment" in json.dumps(d, ensure_ascii=False).lower())
rec["controller_says"] = w.guest(
f"docker logs --since 5m felhom-controller 2>&1 | grep -iE 'RemoveStack {app}.*(REFUSED|busy)' | tail -3").strip()
OUT["B2"] = rec
json.dump(OUT, open(os.path.join(HERE, "live262.json"), "w"), ensure_ascii=False, indent=2)
print(json.dumps(rec, ensure_ascii=False, indent=2))