hub v0.76.0 — R-82 Slice C: tier-aware backup thresholds
R-81 merged every backup signal into one 'newest' against a single 26h limit. backupStaleAfter's own comment recorded why that stops being right under a weekly offsite tier. Each tier is now judged against its own threshold; R-81's structure (three verdicts, anchored absence, distinct reasons) and its boundary test are preserved intact. - offsiteBackupStaleAfter = 8d (7d cadence + headroom); backupStaleAfter keeps 26h and now names the HOST tier only - splitTiers / assessTier / newestBackupEvidenceByTier Slice-A.4 rule implemented: a PBS-targeted vzdump appears in BOTH arrays, so classification is by TARGET TYPE (target_id -> storage_targets[].name -> type), never by array membership — otherwise a PBS backup makes a stale host tier look fresh. storage_targets is used rather than pbs_dr.storage_id because the latter is null on a box with a PBS storage but no DR descriptor. A tier is only judged when the box HAS it, else every box without an offsite tier would alarm once the anchor elapsed — R-81's mistake one level down. With neither tier identifiable (old agent) the pre-Slice-C path runs unchanged. Intended behaviour change: a 30h offsite snapshot no longer alarms. Three fixtures asserted the merged threshold; each still asserts an alarm at the correct limit. No assertion was weakened. RECORDED LIMITATION: the hub infers 'PBS => weekly' from storage type. defaultBackupTarget is felhom-pbs, so a box that never sets local_backup_target would run PBS as its DAILY tier and be judged against 8 days — 7 days of blindness. No box is in that shape today; the real fix is the agent reporting per-tier cadences. Own task. Red-proof observed. Replayed live: demo-felhom OK, demo-hp UNKNOWN (defers correctly), drill-r50 MISSED (true positive). No customer email would be sent.
This commit is contained in:
@@ -184,14 +184,16 @@ func TestBackupFreshness_ExistingBehavioursUnchanged(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("2 newest backup >26h still alarms with the same string", func(t *testing.T) {
|
||||
r := `{"pbs_snapshots":[{"backup_time":"` + at(-30*time.Hour) + `","verify_state":"ok"}]}`
|
||||
// Window evidence is ALSO 30h old — nothing fresher exists anywhere.
|
||||
got := assessBackupFreshness(r, evidenceAt(now, 30*time.Hour, anchor), now)
|
||||
t.Run("2 a backup past its tier limit still alarms", func(t *testing.T) {
|
||||
// Slice C: the HOST tier keeps the 26h limit verbatim, so this is the same assertion the
|
||||
// merged code made — now scoped to the tier it actually belongs to.
|
||||
r := `{"backups":[{"target_id":"local","started_at":"` + at(-30*time.Hour) + `","success":true}]}`
|
||||
ev := backupEvidence{newestHost: now.Add(-30 * time.Hour), haveHost: true, firstReportAt: now.Add(-anchor)}
|
||||
got := assessBackupFreshness(r, ev, now)
|
||||
if !got.missed() {
|
||||
t.Fatalf("stale backup must still alarm; got verdict=%d reason=%q", got.verdict, got.reason)
|
||||
t.Fatalf("stale host backup must still alarm; got verdict=%d reason=%q", got.verdict, got.reason)
|
||||
}
|
||||
if got.reason != "newest backup is 30h0m0s old (limit 26h0m0s)" {
|
||||
if got.reason != "host tier: newest backup is 30h0m0s old (limit 26h0m0s)" {
|
||||
t.Fatalf("stale reason string changed: %q", got.reason)
|
||||
}
|
||||
})
|
||||
@@ -202,7 +204,9 @@ func TestBackupFreshness_ExistingBehavioursUnchanged(t *testing.T) {
|
||||
if !got.missed() {
|
||||
t.Fatalf("failed verify must still alarm; got verdict=%d reason=%q", got.verdict, got.reason)
|
||||
}
|
||||
if got.reason != "newest PBS snapshot failed verification" {
|
||||
// Slice C prefixes the tier. The FAILURE MODE is unchanged and still has its own distinct
|
||||
// string; only the tier name was added, which is the point of tier-aware reasons.
|
||||
if got.reason != "offsite tier: newest PBS snapshot failed verification" {
|
||||
t.Fatalf("verify-failed reason string changed: %q", got.reason)
|
||||
}
|
||||
})
|
||||
@@ -236,7 +240,7 @@ func TestBackupFreshness_ReasonStringsAreDistinct(t *testing.T) {
|
||||
|
||||
reasons := map[string]string{
|
||||
"unparseable": assessBackupFreshness(`nope`, backupEvidence{}, now).reason,
|
||||
"stale": assessBackupFreshness(`{"pbs_snapshots":[{"backup_time":"`+at(-40*time.Hour)+`","verify_state":"ok"}]}`, backupEvidence{}, now).reason,
|
||||
"stale": assessBackupFreshness(`{"pbs_snapshots":[{"backup_time":"`+at(-9*24*time.Hour)+`","verify_state":"ok"}]}`, backupEvidence{}, now).reason,
|
||||
"verify failed": assessBackupFreshness(`{"pbs_snapshots":[{"backup_time":"`+at(-2*time.Hour)+`","verify_state":"failed"}]}`, evidenceAt(now, 2*time.Hour, 30*24*time.Hour), now).reason,
|
||||
"absence timed": assessBackupFreshness(`{"backups":[]}`, noEvidence(now, 240*time.Hour), now).reason,
|
||||
"absence unanch": assessBackupFreshness(`{"backups":[]}`, backupEvidence{}, now).reason,
|
||||
|
||||
Reference in New Issue
Block a user