Files
felhom-controller/REPORT.md
T

3.1 KiB

REPORT — v0.102.0: async restore family (no proxy-timeout error page on a succeeding restore)

Date: 2026-07-06 · Class: implementation (code + tests + deploy + live acceptance). Baseline: main @ 9d5a588 (v0.101.0) → c529a45. Part B of the pilot-blockers bundle (A = agent v0.73.0 F2, C = catalog healthcheck sweep). Provenance: RERUN F4 re-adjudication.

The bug (F4 re-adjudicated)

All three restore surfaces blocked the HTTP request until completion: /backup/restore (→ RestoreFromRecoveryUnit), /backup/tier2/restore (→ RestoreTier2Files), /backup/offbox/restore (→ RestoreOffbox). Through cloudflared's hard 100s cap + traefik a real customer got an error page while the restore silently succeeded; the off-box handler was worse — it bounded on r.Context(), so a proxy read-timeout canceled the SFTP restore mid-flight.

The fix

  • Async family (mirrors the live offboxRunHandler): each handler fast-path refuses a concurrent op (IsRunning() → "Egy mentési/visszaállítási művelet már fut."), runs the restore in a background goroutine, and redirects immediately with "Visszaállítás elindult…". The offbox restore's context moved from r.Context() to context.Background()+30m (fixes the mid-flight cancel). The restore functions' internal single-flight acquire is unchanged.
  • Op-status (internal/backup/opstatus.go): mutex-guarded in-memory current-op + terminal last{op,stack,ok,message,finished_at} (deep-copy getter); new GET /api/backup/restore-status (distinct from /backup/status, which proxies the agent PBS status). In-memory (lost on restart).
  • UI (backups.html): a banner polls the status every 3s — neutral running (shown even on a fresh page load mid-op), success on completion, red only on failure.

Tests + red-proof

opstatus_test.go (begin→running→terminal, deep-copy, failure); async_restore_test.go (handler returns <500ms while the restore parks in a blocking provider + op-status transitions; double-click refused with no second launch). Red-proof: reverting the handler to synchronous blocks the request indefinitely (killed at 30s) vs <500ms async. Full gate go build/vet/test ./... = PASS.

Deploy + live acceptance

Built + pushed :0.102.0 on 180, deployed to guest 9201 (golden/bootstrap), healthy; GET /api/backup/restore-status live. Restore drill (vaultwarden): planted canary → backup → vandalized → POST /backup/restore returned in 0.018s (302, "elindult" flash); status transitioned running:true → false with terminal {op:restore, ok:true}; canary restored bit-identical. External path (F4): POST https://felhom.demo-felhom.eu/backup/restore (through cloudflared) returned in 0.235s with 302 — no proxy error. Tier-2: POST /backup/tier2/restore async 0.007s; its failure landed in the terminal status (B3 failure-visibility). Double-click refusal covered by the unit test.

Not changed

The restore functions' internal single-flight; /api/backup/status (agent-PBS) semantics; the restore-button CSRF/form mechanism (only the handler tail went async); no op-status persistence (documented).