Snapshot projection never refreshes after forget — counts are stale until the next backup #40
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
GET /api/hosts/{id}/snapshots(andhosts.snapshot_count) is a projection refreshed only when the agent sendssnapshots.report. The intended design already refreshes after forget —RunForgetcallsreportSnapshotsatinternal/agent/runner/runner.go:290— but in practice that call never runs, so retention changes are invisible until the next backup happens to refresh the projection.The practical effect: a host whose retention is working perfectly can display a months-old snapshot count, and there is no way to tell the difference between "forget is broken" and "the projection is stale". This cost a full diagnostic session.
Evidence — the refresh never fires after forget
Every host's
refreshed_atequals its backup time. Not one matches its forget time:refreshed_at2026-08-22T01:30:022026-08-22T02:20:092026-08-22T03:10:032026-08-22T03:31:082026-08-22T08:39:10Root cause — interacts with #37
In
RunForget:The nil-pointer panic in #37 kills the process during/just after
sendFinished, so execution never reachesreportSnapshots. Fixing #37 will likely fix this too — but the projection is still fragile by design, because a single missed message leaves it stale indefinitely with no indication.Observed impact
On
host-Athe API reported 102 snapshots while the repository actually held 12. The stale count persisted for hours and only corrected when an unrelated ad-hoc backup forced a refresh. During that window the host looked like it had never applied retention since May.Because #37 also leaves forget jobs stuck at
status: running(neverfailed), no alert fires either — so the dashboard showsOpenAlerts: 0, a stale count, and a job that never ends.Suggested changes
Roughly in order of value for effort:
reportSnapshotsbeforesendFinished, or run it in adefer, so a failure on the reporting path cannot skip the projection update. (Fixing #37 addresses the immediate cause; this makes it robust to the next one.)refreshed_atis already returned — add a derivedstale: true(orrefreshed_age_seconds) when it is older than the host's last completed job, so clients can distinguish "12 snapshots" from "12 snapshots as of three days ago". The UI can then badge it.prunealso rewrites the index.POST /api/hosts/{id}/snapshots/refresh, so an operator can reconcile without running a backup. Today the only way to force a refresh is to mutate the repo, which is a poor diagnostic tool.runningbeyond a threshold. Currently onlyfailedalerts, so a hung job is silent — this is what let the stale projection persist unnoticed.Items 2 and 5 are the ones that would have made this self-diagnosing.
Environment
v1.1.0(commit0f5110f3d9b91b269684453e6b8d14dbcffb93c6)0.18.1, protocol_version 1Scope note after reviewing the current implementation: I suggest keeping this issue focused on snapshot-projection freshness and reconciliation. The stuck-job detection/alerting concern has been split into #41 so it can be designed and verified independently.
Why narrow it this way:
reportSnapshotsbeforesendFinished, or into adefer, would not make delivery crash-proof: both use the connection-scoped context and sender, so neither can report over a lost connection.refreshed_at.A focused resolution here would therefore track host-level snapshot refresh time (including zero-snapshot reports), expose freshness/staleness, and provide a reconciliation path such as explicit or periodic refresh. Server-side detection and alerting for orphaned
runningjobs is now tracked in #41.I have left the original title and description unchanged.