gate: the boot bind gate honours a customer's Stop (R-55, v0.157.0)
shouldRecreateOnBoot keyed on Deployed+drive-present alone. Deployed stays true across a Stop, so a drive-backed app the customer switched off was silently restarted on every guest reboot (proven live: immich). Requires len(Containers)>0 as well - R-52's existing-Exited vs absent distinction. A UI Stop is compose down and removes the containers; a guest that went down under a running app leaves them. Container STATE is still deliberately NOT a filter: that would miss a not-yet-restarted or stuck-Exited app, which is the bug the boot-id path exists to fix. Evidence sampled before any recreate - recreate's own StopStack erases it. Honoured Stops counted and logged separately from no-live-bind skips.
This commit is contained in:
+42
-1
@@ -1,6 +1,47 @@
|
||||
## Changelog
|
||||
|
||||
### v0.156.0 — a dead primary alerts (R-51); a boot orphan restarts itself (R-52) (2026-07-21)
|
||||
### v0.157.0 — the boot bind gate honours a customer's Stop (R-55) (2026-07-21)
|
||||
|
||||
**Your Stop now means Stop across a guest reboot for drive-backed apps too** — the guarantee R-52
|
||||
already gave every other app. Found by STOP-1's R-52 leg on 2026-07-21, which was designed to prove
|
||||
the opposite: immich, stopped from the UI seconds earlier, came back running after the reboot.
|
||||
|
||||
The boot bind gate (`internal/web/intermediary.go`) keyed its recreate on
|
||||
`Deployed && HDD_PATH && drive-present` alone. `Deployed` is a deploy-lifecycle flag — it stays true
|
||||
across a Stop — so the gate had no way to tell "the guest went down under this app" from "the
|
||||
customer switched this off", and it resurrected both. R-52 was never implicated: its own gate behaved
|
||||
exactly as specified (immich, at zero containers, was never a candidate for it). The gate simply
|
||||
reaches every drive-backed app first.
|
||||
|
||||
**The fix is R-52's own predicate, translated.** `shouldRecreateOnBoot` now also requires
|
||||
`len(Stack.Containers) > 0` (from `docker ps -a`, so `Exited` containers count):
|
||||
|
||||
- containers EXIST but are down → the guest went down under the app; docker's records survive the
|
||||
reboot → boot orphan → recreate, as before.
|
||||
- ZERO containers → a UI Stop is `compose down`, which REMOVES the containers → deliberate → leave it.
|
||||
|
||||
**What deliberately did NOT change: container STATE is still not a filter.** That is the original
|
||||
design's load-bearing part — a `State != stopped` filter misses an app that simply hasn't been
|
||||
auto-restarted yet after the boot, or is stuck `Exited` on a create-time bind failure with
|
||||
`RestartCount=0`. `hasContainers` is a different question ("does docker still have records of it")
|
||||
and, unlike liveness, it survives a reboot as a statement of intent. `TestShouldRecreateOnBoot` now
|
||||
pins both axes at once — they pull in opposite directions, which is the whole difficulty of this gate.
|
||||
|
||||
- **Ordering trap, handled:** the evidence is sampled into the `bootStack` snapshot BEFORE any
|
||||
recreate runs, because `recreate` calls `StopStack` (`compose down`) and so destroys the very
|
||||
signal the decision needs.
|
||||
- **The drive-absent gate is not regressed.** Apps it stopped are also at zero containers, so this
|
||||
path now skips them — correctly: they are recorded in `StoragePath.StoppedStacks` and restarted by
|
||||
`ReconcileDriveGates`' `Return` branch, which runs on the same `driveGateLoop` tick.
|
||||
- **Honoured Stops are observable.** `leftStopped` is counted and logged separately from `skipped`
|
||||
at INFO (`… left stopped — zero containers means the customer stopped them on purpose`).
|
||||
Conflating them would have fired a WARN about a missing drive bind for an app behaving exactly as
|
||||
asked, and a silent correct path is how an inert seam hides.
|
||||
- **Red-proof (run):** dropping `hasContainers` from the predicate makes
|
||||
`TestRecreateDriveBackedApps_HonoursCustomerStop` fail with `recreated=[romm immich]` — the live
|
||||
defect, by name.
|
||||
|
||||
## v0.156.0 — a dead primary alerts (R-51); a boot orphan restarts itself (R-52) (2026-07-21)
|
||||
|
||||
**No new agent coupling — MinAgent stays 0.90.0.** Two independent failures from the same live
|
||||
audit, both unattended-resilience holes: the box was broken and nobody was told, then the box could
|
||||
|
||||
Reference in New Issue
Block a user