Files
felhom-controller/REPORT.md
T
Claude Code 3f0420ff9c REPORT: correct a STALE deployment section — v0.175.0 IS deployed on both boxes
The section read 'NOT deployed'. True when written (17:56 CEST), stale 26 min
later when I deployed (18:22 CEST) and did not update it. REPORT.md is defined
as 'overwrite with the most recent state', so an artifact this project validates
against was left asserting the opposite of reality.

Precision matters on the apparent contradiction: a 17:01 quiesce log 'before' a
17:56 commit is a TIMEZONE artifact (controller logs UTC, git commits CEST), not
evidence of a false claim. In one clock: REPORT 17:56 -> deploy 18:22 -> quiesce
19:01. Consistent. That host=CEST/controller=UTC mismatch is a recorded project
trap and caught me once earlier the same day.

Also records the live multi-tier quiesce evidence and what remains unproven.
2026-07-26 20:43:02 +02:00

8.0 KiB

REPORT — R-82 Slice B: one quiesce window, two backup tiers (v0.175.0) (2026-07-26)

Overwritten per the standing rule. Controller v0.174.0 → v0.175.0. Full cross-repo arc: felhom.eu/REPORT.md.

MinAgent UNCHANGED — deliberately. This release degrades gracefully against ANY older agent. It does not require agent v0.97.0.


1. Why the controller had to change

The agent gained per-target backup tiers (R-82 Slice A): local daily + offsite weekly. But the controller owns quiescing — it polls /backup/due, stops the app stacks, calls POST /backup, and restarts them. So the multi-tier schedule has to be reconciled here, and one night a week both tiers come due at once.

Two quiesce cycles that night would mean two app outages for one night's work — undoing the entire argument for weekly-over-daily, since the quiesce blip was the only real cost of the offsite tier.

2. The dedup rule — specified, not emergent

local due PBS due result
yes no one quiesce, local backup
no yes one quiesce, PBS backup
yes yes ONE quiesce window, BOTH backups inside it
no no no quiesce

3. What shipped

  • quiesce.TieredBackend (optional extension to Backend) + BackupTier, ErrTiersUnsupported. A backend that does not implement it drives the pre-R-82 path unchanged.
  • agentapi: BackupTiers, BackupDueFor, StartBackupFor, BackupStatusFor. targetQuery("") yields an empty suffix, so an untargeted call hits the untargeted route byte-for-byte.
  • Loop.resolveDueTiers — the dedup rule in one place, tiers in agent order.
  • quiesceAndPollTiers + pollTier — one marker, one stop, N sequential backups, one resume.

4. Two decisions worth stating plainly

The app stays quiesced until the LAST tier snapshots. Resuming after tier 1's snapshot would leave the following tier capturing a running app — losing app-consistency on exactly the DR tier we most want it on. The consequence is user-visible and documented: on the both-due night downtime is (first tier's full backup) + (last tier's snapshot), not one snapshot. Tiers therefore run fast-first — vzdump holds a guest lock so they are necessarily sequential, and the agent advertises primary (local) first. The reverse order would make downtime ≈ (offsite backup) + (local snapshot), far worse.

A manual "Mentés most" covers EVERY tier, in one window, due-ness ignored. A manual run that silently skipped the DR tier would be the same applied-and-empty fault in a different costume.

5. Capability detection

GET /backup/tiers 404 ⇒ pre-R-82 agent. This is the project's documented route-probe mechanism (internal/agentapi/features.go). It is deliberately not a featureProbes row: that table answers a yes/no at a UI entry point, whereas the loop needs the tier list, so a row would be a second probe of the same route for no gain. The degrade logs exactly once per process — once because it is a steady state during a rollout, never zero times because a silent degrade is indistinguishable from multi-tier working.

6. v0.175.0 — a tier that overruns the quiesce bound defers the rest

Operator ruling 2026-07-26: "let the first backup run as long as needed; other backups shouldn't start until finished."

A first FULL offsite snapshot legitimately runs for hours. When max_quiesce elapses the app resumes — correct, unchanged — but the loop then started the next tier while the first was still uploading. That is now a break.

Why it matters: vzdump still holds the guest lock, so the second start would be refused by the agent (409, v0.99.0) or fail on the lock — and a failed backup never satisfies a cadence, so the tier would stay permanently due and retry into the same wall every poll.

7. Tests

go build ./... && go vet ./... && go test ./...rc=0, 27 packages (vet run unpiped; note grep FAIL returns rc=1 when it finds nothing, which is not a failure signal — that idiom caused a red commit elsewhere in this arc).

+12 tests in internal/quiesce/tiers_test.go. Red-proofs observed and restored:

  • Both-due night — a per-tier cycle instead of one window fails with want EXACTLY 1 stop and 1 start, got stops=2 starts=2. The COUNT is the assertion; asserting only "both backups ran" would pass against a double-quiesce implementation.
  • New controller ↔ old agent — treating ErrTiersUnsupported as "nothing due" fails with OLD AGENT: a backup MUST still be taken via the untargeted path; got started=[]. The hollow version of this test asserts only "no error", which passes while silently skipping the backup.
  • Overrun defer — dropping the break fails with the second tier MUST NOT start while the first is still running; started=[local felhom-pbs].

8. Deployment — DEPLOYED 2026-07-26 18:22 CEST (this section was stale; corrected)

Live on both boxes: felhom-controller:0.175.0, healthy.

The correction, and why the record disagreed with itself

This section previously read "NOT live-validated — and NOT deployed". That was true when written and went stale 26 minutes later:

CEST event
17:56:10 this REPORT committed (f5e1064) — "not deployed", accurate at that moment
18:22:56 controller 0.175.0 deployed to both boxes (container StartedAt 16:22:56 UTC)
19:01:39 the multi-tier quiesce cycle ran (controller log line 17:01:39UTC)

The defect is mine and it is real: REPORT.md is defined as "overwrite with the most recent state", and I deployed without updating it. An artifact this project validates against was left asserting the opposite of reality for the rest of the session.

One thing to be precise about, because it nearly became a second wrong conclusion: the apparent contradiction — a 17:01 quiesce log "before" a 17:56 commit — is a timezone artifact, not evidence of a false claim. The controller logs UTC; git commit timestamps here are CEST. Converted to one clock the ordering is consistent and unremarkable: REPORT 17:56 → deploy 18:22 → quiesce 19:01. That host=CEST / controller=UTC mismatch is already recorded as a project trap and it caught me once earlier the same day.

So: a stale artifact (real, mine) — not a claim contradicted by its own evidence.

9. Live validation

The multi-tier quiesce ran on demo-hp, driven through the real UI endpoint (POST /api/guest-backup/trigger, session auth + CSRF — the exact call "Mentés most" makes):

17:01:39Z  manual backup requested — quiescing now
17:01:39Z  backup due on 2 tier(s) — quiescing 1 stack(s): [paperless-ngx]      <- ONE stop
17:01:46Z  tier local: backup job ... started
17:02:56Z  tier local: ... done — next tier may start (app still quiesced)      <- app stays DOWN
17:02:56Z  tier felhom-pbs: backup job backup-9201-felhom-pbs-... started
17:03:06Z  tier felhom-pbs: ... snapshotted — resuming app early (8B.2)
17:03:06Z  unquiescing (snapshotted (early resume, last tier)): restarting 1 stack(s)   <- ONE start

Exactly one stop/start pair with both backups inside it — the assertion that matters, since "both backups ran" would also pass against an implementation that quiesces twice. Tier order local-first/PBS-last as designed; the app stayed quiesced through the non-last tier, preserving app-consistency on the DR tier; it resumed at the last tier's snapshot rather than its upload. Total app downtime 1m27s for both tiers, and paperless came back healthy.

10. Still NOT live-validated

  1. A scheduled both-due night (this was the manual TriggerNow path; the scheduled path shares quiesceAndPollTiers but has not been observed firing on its own).
  2. The degrade path against a genuinely pre-R-82 agent in production — both boxes now run an agent that serves /backup/tiers, so the fallback has not been exercised live.
  3. The overrun defer firing on a real long-running offsite backup.