Files
felhom-controller/REPORT.md
T
Claude Code f5e106440d REPORT: R-82 Slice B (controller v0.175.0) — unit-proven, deliberately NOT deployed
The multi-tier path only engages against an agent serving /backup/tiers, and the
agent rollout was still mid-flight, so both production controllers remain on
v0.173.0. Names the three things that are therefore unproven live.
2026-07-26 17:56:10 +02:00

5.5 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. NOT live-validated — and NOT deployed

The controller change is unit-proven only. Both production controllers still run v0.173.0; this release was deliberately not deployed, because the multi-tier path only engages against an agent that serves /backup/tiers, and the agent rollout was still mid-flight (demo-hp's first offsite backup in progress). Specifically unproven live:

  1. A real both-due night with exactly one stop/start pair observed on a box.
  2. The degrade path against a genuinely old agent in production.
  3. The overrun defer firing on a real long-running offsite backup.

Deploying is the natural next step once demo-hp's first offsite backup lands and the agent rollout is settled.