From be398f92e8acd9f14171b7051ae2d4a3020c52be Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Tue, 6 Oct 2026 11:24:13 +0200 Subject: [PATCH] =?UTF-8?q?R-99:=20the=20PBS=20phantom=20WARN=20names=20th?= =?UTF-8?q?e=20cleanup=20runbook=20(09=20=C2=A73=20decision=20140)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS --- internal/backup/archive_completeness_test.go | 20 ++++++++++++++++++++ internal/backup/runner.go | 17 ++++++++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/internal/backup/archive_completeness_test.go b/internal/backup/archive_completeness_test.go index 563283b..2c0f796 100644 --- a/internal/backup/archive_completeness_test.go +++ b/internal/backup/archive_completeness_test.go @@ -241,3 +241,23 @@ func TestNewestArchiveTime_DistinctPhantomsEachAnnounced(t *testing.T) { t.Errorf("got %d rejection lines for 2 distinct phantoms across 3 polls, want 2:\n%s", n, buf.String()) } } + +// R-99 (`09` §3 decision 140): the WARN for a PBS phantom ends with the cleanup runbook, so whoever sees it knows the +// one sanctioned way to remove it; a tiny archive on a dir storage is not a PBS phantom and gets no pointer. +// RED-PROOF: drop the `msg += phantomCleanupPointer` line → "the PBS phantom WARN does not end with the runbook pointer". +func TestRejectedArchiveWarnNamesTheCleanupRunbook(t *testing.T) { + var buf bytes.Buffer + r := runnerWithContent(t, &buf, []proxmox.StorageContent{phantomEntry(), goodPBSEntry()}) + if _, _, err := r.NewestArchiveTime(context.Background(), 9201); err != nil { + t.Fatal(err) + } + const want = "INCOMPLETE archive when computing tier freshness — it is not a successful backup — a phantom leftover; delete it by felhom.eu documentation/runbooks/pbs-phantom-cleanup.md (09 §3 decision 140)" + if !strings.Contains(buf.String(), want) { + t.Errorf("the PBS phantom WARN does not end with the runbook pointer:\n%s", buf.String()) + } + local := phantomEntry() + local.Format, local.VolID = "tar.zst", "local:backup/vzdump-lxc-9201-2026_07_28-05_31_14.tar.zst" + if got := rejectedArchiveMessage(local); strings.Contains(got, "pbs-phantom-cleanup") { + t.Errorf("a dir-storage archive got the PBS runbook pointer: %s", got) + } +} diff --git a/internal/backup/runner.go b/internal/backup/runner.go index faf27ff..c9767b0 100644 --- a/internal/backup/runner.go +++ b/internal/backup/runner.go @@ -518,10 +518,25 @@ func (r *BackupRunner) warnRejectedArchiveOnce(e proxmox.StorageContent, why str if seen { return } - r.logger.Warn("backup: ignoring an INCOMPLETE archive when computing tier freshness — it is not a successful backup", + r.logger.Warn(rejectedArchiveMessage(e), "target", r.target, "vmid", e.VMID, "volid", e.VolID, "size_bytes", e.Size, "reason", why) } +// phantomCleanupPointer names the runbook that removes a PBS phantom (R-99, `09` §3 decision 140: a leftover of an +// aborted upload is deleted on the backup server, by a runbook, when one is seen — never automatically). +const phantomCleanupPointer = " — a phantom leftover; delete it by felhom.eu documentation/runbooks/pbs-phantom-cleanup.md (09 §3 decision 140)" + +// rejectedArchiveMessage is the WARN text for a rejected archive. Only a PBS entry (format pbs-ct / pbs-vm) gets the +// cleanup pointer: the runbook deletes on a PBS datastore, and a tiny archive on a dir storage is not a PBS phantom. +// Pinned by TestRejectedArchiveWarnNamesTheCleanupRunbook. +func rejectedArchiveMessage(e proxmox.StorageContent) string { + msg := "backup: ignoring an INCOMPLETE archive when computing tier freshness — it is not a successful backup" + if strings.HasPrefix(e.Format, "pbs-") { + msg += phantomCleanupPointer + } + return msg +} + // demo-felhom in a single afternoon of deploys (2026-07-26). // // Asking the STORAGE rather than persisting the store is deliberate: