Files
felhom-agent/internal/backup/store.go
T
2026-10-05 16:56:18 +02:00

80 lines
3.3 KiB
Go

package backup
import (
"context"
"sync"
"gitea.dooplex.hu/admin/felhom-agent/internal/hub"
)
// Store holds the agent's LATEST backup result per target and the latest restore-test
// result — the point-in-time state the host-report surfaces. It is updated by the backup
// runner + the restore-test scheduler/selftest and read by the collector via the hub
// BackupReporter / RestoreTestReporter seams. In-memory and mutex-guarded for the concurrent
// collector vs scheduler access.
//
// **"lost on restart; the cadence re-populates" — that sentence used to be here and it is now
// FALSE for restore-tests (R-189, 2026-08-03).** It was true while a timer re-tested every tier
// daily. Under R-86's per-archive due-check the agent will NOT re-test an archive it has already
// proven, so a proof lost to a restart is not repeated until the next archive generation — a week on
// the offsite tier — and the hub reports that tier unproven throughout. Observed, not predicted: a
// real 14.5 GB offsite restore passed, the agent was restarted 2 m 43 s later for a deploy, and two
// consecutive host-reports carried `0 restore-tests`.
//
// The durable half is `RestoreTestState` (on disk, per tier, with the archive) and the collector
// merges the two — see hub.ProvenRestoreTestReporter. This store remains the ONLY place a FAILURE is
// recorded, and that asymmetry is deliberate: a failing tier stays due and is retried, so a lost
// failure heals itself, while a lost success leaves the system quietly less tested than it believes.
// Backups are NOT unaffected (corrected 2026-10-05, R-348): byTarget is in memory too, so after a restart the
// reported backup LIST reads 0 until the next backup of each tier runs (daily local, weekly offsite) — measured
// 2026-08-20, two consecutive host-reports with `0 backups` while `pvesm list` showed archives on both tiers. What
// is unaffected is the hub's VERDICT: it looks back 7 days over stored reports (felhom.eu hub/internal/monitor/
// deadline.go backupEvidenceLookback) and the storage stays the ground truth (R-84).
type Store struct {
mu sync.Mutex
byTarget map[string]hub.Backup // latest backup per target id
lastTest *hub.RestoreTest
}
// NewStore builds an empty Store.
func NewStore() *Store {
return &Store{byTarget: map[string]hub.Backup{}}
}
// RecordBackup stores the latest backup for its target.
func (s *Store) RecordBackup(b hub.Backup) {
s.mu.Lock()
defer s.mu.Unlock()
s.byTarget[b.TargetID] = b
}
// RecordRestoreTest stores the latest restore-test result.
func (s *Store) RecordRestoreTest(r hub.RestoreTest) {
s.mu.Lock()
defer s.mu.Unlock()
cp := r
s.lastTest = &cp
}
// Backups implements hub.BackupReporter — the latest backup per target (stable order by
// target id is not guaranteed; the hub does not depend on order).
func (s *Store) Backups(context.Context) []hub.Backup {
s.mu.Lock()
defer s.mu.Unlock()
out := make([]hub.Backup, 0, len(s.byTarget))
for _, b := range s.byTarget {
out = append(out, b)
}
return out
}
// RestoreTests implements hub.RestoreTestReporter — the latest restore-test result (0 or 1).
func (s *Store) RestoreTests(context.Context) []hub.RestoreTest {
s.mu.Lock()
defer s.mu.Unlock()
if s.lastTest == nil {
return []hub.RestoreTest{}
}
return []hub.RestoreTest{*s.lastTest}
}