d83316326e
gates / gates (push) Successful in 22s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
80 lines
3.3 KiB
Go
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}
|
|
}
|