docs: REPORT/CHANGELOG/README — v0.72.0 FileBrowser boot-recreate convergence + live x2 reboot acceptance

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-16 18:09:58 +02:00
parent 6ea25388d7
commit 0d159d7e34
3 changed files with 57 additions and 131 deletions
+5 -2
View File
@@ -10,8 +10,11 @@ triggers `go s.SyncFileBrowserMounts()` so FileBrowser converges against the now
runs unconditionally so FileBrowser reflects the current bind state even if no app needed recreating). runs unconditionally so FileBrowser reflects the current bind state even if no app needed recreating).
Refactored the recreate loop into a pure, testable `recreateDriveBackedApps(stacks, present, recreate, Refactored the recreate loop into a pure, testable `recreateDriveBackedApps(stacks, present, recreate,
syncFB)`. Tests: FileBrowser sync invoked once, AFTER every recreate (red-proofed companion: pre-fix path syncFB)`. Tests: FileBrowser sync invoked once, AFTER every recreate (red-proofed companion: pre-fix path
never synced); and synced even when nothing was recreated. Pairs with felhom-agent's host-reboot never synced); and synced even when nothing was recreated. Pairs with felhom-agent v0.37.0's host-reboot
remount-by-UUID fix; live-accepted with a real `felhom-pve` reboot. remount-by-UUID fix. **Live-accepted with TWO real `felhom-pve` reboots:** both fired the boot-recreate
(`live bind confirmed — recreating … → re-syncing FileBrowser mounts → FileBrowser mounts synced — 3
storage path(s)`), all drive-backed apps recovered, FileBrowser non-stale — while the agent tolerated a
`/dev/sdb``/dev/sdc` letter swap by mounting each drive by UUID.
### v0.71.0 — fix guest-reboot recovery of drive-backed apps (boot-race + the agent-path blocker) (2026-06-16) ### v0.71.0 — fix guest-reboot recovery of drive-backed apps (boot-race + the agent-path blocker) (2026-06-16)
+43 -129
View File
@@ -1,144 +1,58 @@
# REPORT — controller v0.71.0: fix guest-reboot recovery of drive-backed apps (2026-06-16) # REPORT — controller v0.72.0: FileBrowser converges on boot-recreate (2026-06-16)
**Deployed:** controller **v0.71.0** on guest 9201 / felhom-pve (bootstrap-managed; healthy). **Deployed:** controller **v0.72.0** on guest 9201 / felhom-pve (bootstrap-managed; healthy).
**Commits (trunk):** `25e5cb5` (boot-race fix, first cut) `e2de234` (full fix: + agent-path blocker + periodic retry + docs/tests). **Commit (trunk):** `6ea2538` (Task B) + `c8d…`-era REPORT update.
**Live-accepted:** two `pct reboot 9201` cycles, all 8 drive-backed apps recovered automatically, zero manual starts. **Live-accepted:** two REAL host reboots of felhom-pve — FileBrowser re-synced after the drive-backed
apps were recreated, both times; all drive-backed apps recovered with zero manual intervention.
## Phase A — diagnosis (pinned live, not guessed): THREE sub-causes ## Task B — the gap
A guest `pct reboot` strands drive-backed apps because in-guest dockerd auto-starts the `unless-stopped` Follow-up to v0.71.0's guest-reboot recovery. A host-reboot test found `processGuestBootChange`
apps ~18s BEFORE the agent re-binds the drive, so the create-time volume bind fails recreated the drive-backed app stacks (so their `${HDD_PATH}` binds re-resolved against the now-live
(`mkdir /mnt/felhom-drives/<drive>/userdata: permission denied`) and `RestartCount=0` means it is never drives) but **never re-synced FileBrowser**. FileBrowser is base-infra: it binds each drive's
retried → stuck `Exited`. The intended recovery (`processGuestBootChange`) did **not** fire. Live repro `userdata` directory but has no `HDD_PATH`, so it is **not** in the boot-recreate set — its mounts went
(`paperless-webserver`: `exit=128, RestartCount=0, State.Error=mkdir…permission denied`) was the fixture. stale after a reboot (the early first-boot bring-up bound them before the drives were live).
Diagnosis pinned three distinct sub-causes:
1. **AGENT-PATH BLOCKER (the live root cause).** `/api/disks``{"error":"agent not configured"}`: ## Fix
`agentClient()` requires `cfg.LocalAPI.Endpoint`, which was **empty**, so `processGuestBootChange`
(and the **entire** drive gate) returned at its first guard — never reaching any boot-id/bind logic.
The authoritative `bootstrap.json` *had* a complete `local_api` block (`endpoint/fingerprint/token`),
but `bootstrap.MaybeIngest` (bootstrap.go:101) returned immediately on "already configured"
(`cfg.Customer.ID != ""`), so a controller.yaml seeded before `local_api` existed never got it merged.
Evidence: `LastGuestBootID` was stuck at the guest's first-boot value across every reboot (it was never
updated because the function bailed before reaching the persist).
2. **BOOT-RACE READINESS GATE.** `processGuestBootChange` sampled the agent's `BoundUnderParent` **once**
during fast startup (racing the ~18s rebind), recreated nothing, and **persisted the new boot-id**
burning its one-shot. (Confirmed by manually wiring `local_api`: the *old* sample-once would still have
missed; the new poll caught it.)
3. **SINGLE-SHOT FRAGILITY.** `processGuestBootChange` ran only once at startup; right after a guest reboot
the agent's local API can be briefly unreachable/stale, so the single attempt bailed with no retry.
(The periodic drive-gate never recovered them either: its first observation was *after* the rebind → In `processGuestBootChange` (`internal/web/intermediary.go`), **after** `pollLiveBinds` confirms the
present + not-disconnected → no transition; settings showed `felhom-flash disconnected=None`.) live binds and the drive-backed apps are recreated, the boot-recreate path now triggers
`go s.SyncFileBrowserMounts()` so FileBrowser converges against the now-live drives. The recreate loop
was refactored into a pure, seam-testable helper:
## Phase B — fix (harden the existing mechanism, no parallel one) ```go
func recreateDriveBackedApps(stacks []bootStack, presentStable map[string]bool,
recreate func(bootStack), syncFB func()) (recreated, skipped int)
```
1. **`ensureLocalAPI`** (`internal/bootstrap/bootstrap.go`): `MaybeIngest` now calls it on the `syncFB` is invoked exactly once, AFTER all recreates — the FileBrowser sync can never run before the
already-configured path — when `cfg.LocalAPI.Endpoint` is empty it merges `local_api` from drive-backed apps are back.
bootstrap.json into the existing controller.yaml in place (no hub re-pull, existing config preserved),
idempotent + fail-safe.
2. **`driveBindLive` + `pollLiveBinds`** (`internal/web/intermediary.go`): `processGuestBootChange` now
gates on the **real live in-guest bind**`driveBindLive` checks whether `/mnt/felhom-drives/<drive>`
is an actual mountpoint in the controller's own `/mnt` (rslave) `/proc/self/mountinfo` (true only once
the agent's bind propagated, exactly when docker can recreate the app); `pollLiveBinds` waits for it
(bounded ~120s, poll 2s) before recreating via the normal pipeline (`compose down``up -d`).
`shouldRecreateOnBoot` is unchanged and state-independent → stuck-`Exited` create-time-failure apps are
included. Drives that never go live in the window are left to the gate.
3. **Periodic retry** (`driveGateLoop`): `processGuestBootChange` now runs on every periodic tick too —
idempotent (boot-id gated) — so a momentarily-unreachable agent right after a reboot no longer
permanently strands recovery.
Both reboot paths share this code, the same agent dependency, and the same boot-race — so both were ## Tests (non-hollow, seam = the FB sync)
broken by the regression and both are fixed here (see the regression analysis + host-reboot drill below).
## Phase C — tests (non-hollow, pre-fix companions, red-proofed) - `TestRecreateDriveBackedApps_SyncsFileBrowserAfterRecreate` — records the call sequence; asserts
`syncFB` runs exactly once and strictly AFTER every `recreate`. Red-proofed (stubbing out the
`syncFB()` call makes it fail).
- `TestRecreateDriveBackedApps_SyncsEvenWithNoRecreate` — FileBrowser still converges when nothing
needed recreating (e.g. binds already present).
- `internal/web/intermediary_test.go`: `pollLiveBinds` waits through the rebind window then reports live `go build`, `go vet`, `go test ./...` all green on the build server (192.168.0.180).
(recreate fires); a never-live drive stays absent (no spurious recreate); an explicit companion that a
**single early sample misses** the not-yet-live bind. Red-proofed against a no-wait single-sample.
- `internal/bootstrap/bootstrap_test.go`: `ensureLocalAPI` merges `local_api` into an already-configured
controller.yaml that lacks it (companion: pre-fix `MaybeIngest` left `LocalAPI.Endpoint` empty —
red-proofed) and no-ops when already present. Full controller suite green; `go vet` clean.
## Phase D — live acceptance (the real gate) ## Live acceptance real host reboot ×2 on felhom-pve
Built + deployed `felhom-controller:0.71.0` to guest 9201 (the redeploy itself validated `ensureLocalAPI`: Both reboots: `processGuestBootChange` fired on the new boot-id, confirmed the live binds, recreated
the container recreate reset controller.yaml, the code re-merged `local_api`, `/api/disks` → 200). Then every drive-backed app, **then** ran the FileBrowser sync. Captured controller logs:
**two** `pct reboot 9201` cycles (zero manual intervention):
| reboot | boot-id | gate log | result | ```
|---|---|---|---| [gate] boot 1781625729-1612: live bind confirmed — recreating drive-backed app … onto /mnt/felhom-drives/felhom-flash (×8 apps)
| #1 | `…7348791` | "waiting (≤2m0s) for live drive bind(s) … → live bind confirmed — recreating" ×8 | all 8 `Up` | [gate] boot 1781625729-1612: re-syncing FileBrowser mounts against the live binds
| #2 | `…7367438` | same full sequence ×8 | all 8 `Up` | [web] FileBrowser mounts synced — 3 storage path(s), config updated
```
Both recovered all 8 drive-backed apps automatically. (komga reports its container healthcheck (reboot #2 identical on boot-id `1781625955-1516`.) Post-reboot FileBrowser binds all three drives
"unhealthy" but is up and serving — a pre-existing, unrelated issue.) non-stale — `felhom-usb`, `felhom-flash`, `felhom-data``/srv/felhom-*` — and the underlying agent
tolerated a `/dev/sdb``/dev/sdc` reshuffle by mounting each drive by UUID (agent v0.37.0, Task A).
The earlier first-boot `Failed to recreate FileBrowser` line (drives not yet live) is the exact pre-fix
symptom; the boot-recreate path now recovers it.
**HOST reboot (re-drilled after the follow-up challenge — the guest reboots only prove the guest path):** Demo dashboard has no password set → controller API is open on the in-guest path; no secrets committed.
two `systemctl reboot` of felhom-pve itself.
| host reboot | host btime | boot-id | result |
|---|---|---|---|
| #1 | `1781545822``1781620637` | `1781620637-1443` — recreate ×8 (apps were `state=stopped`) | all 8 `Up` |
| #2 | `1781620637``1781620928` | `1781620928-1516` — recreate ×8 | all 8 `Up` |
Both recovered automatically, zero manual starts. Two findings: (a) the **host-btime prefix advances**, so
the host-reboot path triggers `processGuestBootChange` (the persisted `LastGuestBootID` now tracks each
boot — it was frozen at the first-boot value before the fix); (b) the **boot-race manifests on host
reboots too** (the gate recreated `state=stopped` apps), so the host path is **not** immune — it has the
same race and the same agent-path dependency as the guest path.
## Regression analysis — "how did the earlier (v0.68) host-reboot sweep pass?"
It genuinely passed and genuinely exercised `processGuestBootChange`:
`documentation/audits/storage-lifecycle-acceptance-2026-06-15.md` records the host reboot **surfacing a
real bug** (5 apps stayed exited because the recreate filtered on container state, fixed in v0.68.1). If
`agentClient()` had been failing then, `processGuestBootChange` would have bailed and that bug could never
have appeared. **So the agent path worked at v0.68 and regressed afterward** — "boot-id determinism was
never exercising" is false; it was.
The regression: `controller.yaml` is reset to the golden's "configured-but-no-`local_api`" baseline on
every container recreate (each deploy), and the old `MaybeIngest` returned immediately on "already
configured" → `local_api` was never re-merged → `agentClient()` → "agent not configured" → the whole
drive gate + boot recovery silently died, on **both** reboot paths. (My v0.70.0 config-apply round-trip is
**exonerated**: `GET /api/config` returns the file verbatim — `router.go:1150`, no redaction — so it
preserved `local_api`.) `ensureLocalAPI` re-merges `local_api` on every startup, closing the regression
permanently.
## Follow-up — FileBrowser was NOT recovered (host-reboot recovery was incomplete)
After the host-reboot drills, **FileBrowser was found down** (`state=created, exit=128, RestartCount=0`,
`mkdir /mnt/felhom-drives/felhom-usb/userdata: permission denied`). My "all recovered" claim above was
**incomplete**: it only checked the 8 app stacks, which are all on **felhom-flash**. Two distinct gaps:
- **(A) Agent-side — felhom-usb did not re-mount after the host reboot.** The host reboot swapped the USB
device letters (`sdb↔sdc`); felhom-flash re-mounted (now `sdc1`), but **felhom-usb (`sdb1`,
`da9e7089`) did not** — its systemd mount unit (`mnt-felhom\x2dusb.mount`) existed and the by-uuid
symlink resolved, yet the unit was **not active**, so the agent reported the (present) drive as
`enrolled drive not present (durable-id absent)` every reconcile and never bound it. So the
host-reboot recovery covered felhom-flash apps but left **felhom-usb entirely unmounted** — a
**felhom-agent** bug (it must re-activate/retry the mount once the USB enumerates after a host reboot,
not give up on "absent"). **Out of the controller's scope.**
- **(B) Controller-side — FileBrowser is not covered by the boot-recovery.** FileBrowser binds all three
drives' `userdata` but is **base-infra (no `HDD_PATH`)**, so `processGuestBootChange`/`shouldRecreateOnBoot`
skip it. Its recovery relies on `SyncFileBrowserMounts`, which runs once at controller startup — racing
the bind exactly like the app boot-race — and is not retried after the binds go live. So FileBrowser is
stranded by a guest/host reboot independently of (A). **Recommended fix (separate, small): have
`processGuestBootChange` call `SyncFileBrowserMounts` after the live-bind poll confirms the binds, so
FileBrowser converges with the apps.** Not done here (it's a code change + version bump; flagged for a
follow-up slice). Note it would only fully help once (A) is fixed — on this reboot felhom-usb was absent,
so FB could not bind it regardless.
**Recovered live (non-destructive):** started the felhom-usb mount unit (`systemctl start /mnt/felhom-usb`)
→ agent bound it into the guest → restarted the controller → `SyncFileBrowserMounts` synced 3 storage
paths → **FileBrowser `Up (healthy)`**. felhom-usb's real `userdata` (`drwxrwsr-x root:1000`) was intact
all along — only hidden behind the unmounted placeholder.
## Notes
- The Komga healthcheck quirk is pre-existing and out of scope.
- The `local_api`-merge gap affected the **whole drive gate** on this golden, not just boot recovery —
`ensureLocalAPI` restores the agent path generally.
- **Trust caveat:** the reboot drills prove the **felhom-flash app** recovery path (guest ×2, host ×2).
They do **not** yet prove a clean felhom-usb host-reboot remount (gap A) or FileBrowser auto-recovery
(gap B) — both are now documented and recommended for follow-up.
+9
View File
@@ -663,6 +663,15 @@ not just those with HDD data. Non-HDD apps can configure destination, method, an
> path — merging `local_api` from `bootstrap.json` into an existing controller.yaml that lacks it (seeded > path — merging `local_api` from `bootstrap.json` into an existing controller.yaml that lacks it (seeded
> before `local_api` existed) — because without it `agentClient()` returns "agent not configured" and the > before `local_api` existed) — because without it `agentClient()` returns "agent not configured" and the
> entire gate + boot recovery silently die. > entire gate + boot recovery silently die.
> **v0.72.0 — FileBrowser convergence on boot-recreate:** FileBrowser is base-infra (it binds each
> drive's `userdata` but has no `HDD_PATH`, so it is NOT in the drive-backed recreate set) — after a
> host reboot its mounts could be stale (the early first-boot bring-up bound them before the drives went
> live). `processGuestBootChange` now, **after** `pollLiveBinds` confirms the binds and the apps are
> recreated, triggers `go s.SyncFileBrowserMounts()` so FileBrowser converges against the now-live drives.
> The sync runs unconditionally (FileBrowser reflects the current bind state even if no app needed
> recreating). The recreate loop is a pure `recreateDriveBackedApps(stacks, present, recreate, syncFB)`
> that calls `syncFB` exactly once, after every recreate. Live-accepted over two real `felhom-pve`
> reboots (FileBrowser non-stale, all drive-backed apps recovered, agent tolerated a `/dev/sdX` swap by UUID).
> >
> **⚠️ Rebuilt on the agent-delegated disk model (v0.43.0), made ROLE-AWARE in v0.44.0, UX-polished in > **⚠️ Rebuilt on the agent-delegated disk model (v0.43.0), made ROLE-AWARE in v0.44.0, UX-polished in
> v0.45.0.** After the 8C > v0.45.0.** After the 8C