C9-F1 + C9-F2: a restore that restored nothing, and a crash loop nobody saw (v0.183.0)
Both are the system reporting healthy while the customer is not, and both live in the same status-derivation code. Neither is fixed by making the system quieter. C9-F1 (HIGH) — Tier-2 writes recovery-unit/ on EVERY run and RestoreTier2Files has never read it (tier2_restore.go:101-104 reads hdd/ + userdata/ only). Phase 0 enumerated all 53 catalog templates against both demo boxes: 43 apps have NO readable subtree, so the button stopped the app, restored 0 files, restarted it and said "Nincs hiányzó fájl — minden fájl megvan a helyén." — at the moment the customer pressed it because files were missing, with 156 MB of BookStack's data unread in the same copy. 9 apps have file legs but never their DB or volumes, so the same sentence was also a clean bill of health over data never opened (immich: 1.3 GB Postgres unit). Honesty half shipped: a pre-flight coverage check refuses UP FRONT without stopping the app and NAMES the action that works; a run that proceeds claims only what it EXAMINED and discloses that the database and volumes are not covered. Completeness is filed as C9-F1b — routing to the Tier-1 unit restore puts a destructive operation behind a non-destructive button, so its confirm copy has to carry that difference. C9-F4 filed: nothing reads the Tier-2 recovery-unit/ mirror, so the second local copy that exists for drive loss is unreachable by any customer action. C9-F2 (HIGH) — a crash loop was counted as working. StateRestarting is deliberately NOT added to IsDownState (that alarms on every deploy fleet-wide, the over-correction F-A1 nearly cost us); a sustained run becomes down after crashLoopAfter = 5m, set above the 120s deploy timeout, Mealie's 60s start_period and R-97b's 180s grace. The dashboard counter uses the same predicate, so it no longer contradicts the alarm on the same screen. README's claim that faults "still surface as restarting" was a wish with no test — corrected in place; it is the seventh such instance. Six red-proofs observed, including the one that matters most: adding StateRestarting to IsDownState fails the brief-restart test with "every deploy and update would page the operator". go test ./... rc=0, 27 packages, run and read separately from this commit.
This commit is contained in:
+45
-5
@@ -853,10 +853,31 @@ customer edit after the last copy wins) and **nothing is ever deleted** — this
|
||||
`CrossDriveBackup.DestinationPath` (never a fresh target selection). Single-flight with
|
||||
backup/restore; refusals (no copy / never ran / copy dir gone / either drive disconnected /
|
||||
decommissioned) happen before the app is stopped, with customer-readable Hungarian reasons;
|
||||
stop → copy → start → health-wait; zero files copied is a success ("Nincs hiányzó fájl…"). Out of
|
||||
scope by design: overwrite/point-in-time restore (offbox + operator paths), per-file selection,
|
||||
`recovery-unit/`. Apps that index their data dir (e.g. Nextcloud) may need a rescan (occ
|
||||
files:scan) before restored files appear in their own UI.
|
||||
stop → copy → start → health-wait. Out of scope by design: overwrite/point-in-time restore (offbox +
|
||||
operator paths) and per-file selection. Apps that index their data dir (e.g. Nextcloud) may need a
|
||||
rescan (occ files:scan) before restored files appear in their own UI.
|
||||
|
||||
> **COVERAGE — read this before assuming an app is protected by this button (C9-F1, v0.183.0).**
|
||||
> This restore reads `hdd/` and `userdata/` **only**. It has never read `recovery-unit/`, which every
|
||||
> Tier-2 run also writes and which holds the app's DB dumps and named-volume tarballs. Enumerated
|
||||
> across all 53 catalog templates: **43 apps have no readable subtree at all** (their data is entirely
|
||||
> in named volumes — BookStack, Docmost, Vaultwarden, Gitea, …), **9** have file legs but never their
|
||||
> database or volumes, 1 is stateless. So the button is a guaranteed no-op for 81% of the catalog and
|
||||
> only ever partial for the rest.
|
||||
>
|
||||
> Since v0.183.0 it is HONEST about that instead of silently reporting success:
|
||||
> `Tier2RestoreCoverage` is consulted **before** anything starts, an app with no readable subtree is
|
||||
> refused **without being stopped** and told which action does work („…Használd a Visszaállítás
|
||||
> indítása gombot a Biztonsági mentés → Visszaállítás oldalon."), and a run that does proceed claims
|
||||
> only what it **examined** („Minden vizsgált fájl megvan a helyén.") plus a disclosure that the
|
||||
> database and internal volumes are not part of this restore.
|
||||
>
|
||||
> The action that DOES cover those apps is the keep-side recovery-unit restore
|
||||
> (`POST /backup/restore` → `RestoreFromRecoveryUnit`), which replays volume tarballs and DB dumps.
|
||||
> Routing customers there from the Tier-2 card is filed as **C9-F1b** — it puts a destructive
|
||||
> operation behind a button reached via a non-destructive one, so the confirm copy must carry that
|
||||
> difference. **C9-F4** is filed separately: nothing reads the Tier-2 copy's `recovery-unit/` mirror,
|
||||
> so the second local copy that exists precisely for drive loss is unreachable by any customer action.
|
||||
|
||||
**Per-app Tier-2 config panel (v0.57.0)** — `GET/POST /stacks/{name}/backup`
|
||||
(`internal/web/tier2_config_handler.go` + `templates/tier2_config.html`). The "2. mentés" row's
|
||||
@@ -1762,7 +1783,26 @@ display, not alarms, and are unchanged). This rests on two invariants: **I1**
|
||||
at `StateStopped` (compose down removes the containers); **I2** — the P2 restart-policy census
|
||||
(53 templates / 78 services, all `unless-stopped`) means a crashing app never comes to rest at
|
||||
`stopped`, so faults still surface as `exited`/`degraded`/`restarting`/`unhealthy`. If either
|
||||
invariant changes, revisit the suppression. `IsDownState` itself is deliberately UNCHANGED (other
|
||||
invariant changes, revisit the suppression.
|
||||
|
||||
> **C9-F2 (v0.183.0) — the `restarting` half of that sentence was a wish, not a fact.** `restarting`
|
||||
> was named above as a state through which faults "still surface", but it was in no down set at all:
|
||||
> `IsDownState` excludes it, so a crash-looping app raised no banner, no `app_start_failed`, no email
|
||||
> and no hub event — and `unless-stopped` means Docker retries forever, so the silence was permanent.
|
||||
> Campaign 9 watched docmost loop for nine minutes while the F-OBS heartbeat printed
|
||||
> „180 scans since boot, 4 deployed app(s) evaluated, **0 currently down**".
|
||||
>
|
||||
> The fix does **not** add `StateRestarting` to `IsDownState` — that alarms on every deploy and update
|
||||
> fleet-wide. A SUSTAINED restarting run becomes down after `stacks.crashLoopAfter` (**5 min**), chosen
|
||||
> above the deploy flow's 120 s health timeout, Mealie's 60 s `start_period` and R-97b's 180 s quiesce
|
||||
> grace, so the suppression windows compose into one bounded delay rather than leaving a gap. Carried
|
||||
> by `Stack.RestartingSince` (stamped in `refreshStatusLocked`, cleared on any other state, not
|
||||
> persisted) and read via `Stack.CrashLooping(now)` — used by BOTH the alarm and the dashboard
|
||||
> "how many of my apps work" counter, which previously counted `restarting` as running and so
|
||||
> contradicted the alarm on the same screen. Pinned by `crashloop_classify_test.go`; the test that a
|
||||
> brief restart stays silent is the one that fails against the naive fix.
|
||||
|
||||
`IsDownState` itself is deliberately UNCHANGED (other
|
||||
callers rely on stopped counting as down). An out-of-band `docker compose stop` leaves the containers
|
||||
present → `StateExited` → still alerts, which is correct (out-of-band tampering is reportable).
|
||||
|
||||
|
||||
Reference in New Issue
Block a user