fix(archive): BAK-05 Report liefert existing_in_storage fuer BAK-08

Report enthielt bisher nur Abweichungen. BAK-08 braucht eine
deterministisch sortierte Liste bestaetigt existierender Objekte als
Stichprobengrundlage, nicht nur "keine Abweichung". Ergaenzt vor
BAK-08-Start, real neu getestet (9/9) und auf 131 erneut ausgeloest.
This commit is contained in:
sysops
2026-08-29 23:41:36 +02:00
parent ae214f1731
commit 8100fa3d14
3 changed files with 61 additions and 7 deletions
+17 -6
View File
@@ -12,11 +12,22 @@ echtes Dateisystem). Beide Seiten liefern nur SCHLÜSSEL niemals Inhalt
dadurch bleibt BAK-05 sauber getrennt von BAK-08 (Inhalts-/Prüfsummen- dadurch bleibt BAK-05 sauber getrennt von BAK-08 (Inhalts-/Prüfsummen-
verifikation, eigene Fehlerklasse, eigenes Ticket). verifikation, eigene Fehlerklasse, eigenes Ticket).
Report-Format bewusst deterministisch: beide Ergebnislisten Report-Format bewusst deterministisch: alle drei Ergebnislisten
(`missing_in_storage`, `orphaned_in_storage`) nach `storage_key` (`missing_in_storage`, `orphaned_in_storage`, `existing_in_storage`)
aufsteigend sortiert, damit BAK-08 später dieselbe Objektliste als nach `storage_key` aufsteigend sortiert.
stabile Stichprobenquelle weiterverarbeiten kann, ohne selbst neu zu
sortieren/filtern (Nutzervorgabe). **Nachtrag (nach Rückfrage vor BAK-08-Start):** Der ursprüngliche Report
enthielt nur die beiden Abweichungslisten keine Liste der bestätigt
existierenden Objekte. Für BAK-08 als Stichprobengrundlage reicht
"keine Abweichung" nicht, es braucht die tatsächliche, deterministisch
sortierte Liste. Ergänzt: `Report.ExistingInStorage` DB-Eintrag UND
Storage-Objekt beide vorhanden, reine Existenzbestätigung (keine
Inhaltsprüfung, Scope-Trennung zu BAK-08 bleibt gewahrt), aufsteigend
nach `storage_key` sortiert. BAK-08 zieht seine Stichprobe daraus, ohne
selbst zu sortieren/filtern. Neuer Test
`TestReconcile_ExistingInStorageIsStableSamplingBasis` beweist Inhalt
und Sortierung. Real neu gebaut, getestet (9/9) und auf 131 erneut
ausgelöst Journal zeigt das Feld `existing_in_storage` im Report.
Meldeweg über OPS-05 (wie später BAK-08) wurde als offene Design-Frage Meldeweg über OPS-05 (wie später BAK-08) wurde als offene Design-Frage
aufgeworfen, aber nicht zur Vorbedingung gemacht hier bewusst noch aufgeworfen, aber nicht zur Vorbedingung gemacht hier bewusst noch
@@ -94,7 +105,7 @@ go test ./... -p 1 -count=1 -> 3/3 Pakete mit Tests ok (internal/backup, intern
``` ```
`internal/reconcile`-Tests separat mit gesetzter `TEST_TENANT_DSN` gegen `internal/reconcile`-Tests separat mit gesetzter `TEST_TENANT_DSN` gegen
`dms_tenant_test` verifiziert: 8/8 Tests bestanden (5 reine `dms_tenant_test` verifiziert: 9/9 Tests bestanden (6 reine
`Reconcile`-Tests + 3 `sources.go`-Integrationstests). `Reconcile`-Tests + 3 `sources.go`-Integrationstests).
## Gesamtergebnis ## Gesamtergebnis
+12 -1
View File
@@ -37,6 +37,13 @@ type Report struct {
// OrphanedInStorage: Objekt im Storage vorhanden, kein Datenbankeintrag // OrphanedInStorage: Objekt im Storage vorhanden, kein Datenbankeintrag
// (Akzeptanzkriterium 2). // (Akzeptanzkriterium 2).
OrphanedInStorage []Finding `json:"orphaned_in_storage"` OrphanedInStorage []Finding `json:"orphaned_in_storage"`
// ExistingInStorage: Datenbankeintrag UND Storage-Objekt beide
// vorhanden — reine Existenzbestätigung, KEINE Inhaltsprüfung. Dient
// Archive BAK-08 als stabile, deterministisch sortierte
// Stichprobengrundlage (nach StorageKey aufsteigend, siehe Report-
// Dokumentation oben) — BAK-08 muss dafür selbst nicht mehr
// sortieren/filtern.
ExistingInStorage []Finding `json:"existing_in_storage"`
} }
// IsClean liefert true, wenn der Lauf keine Abweichungen fand (Pflicht- // IsClean liefert true, wenn der Lauf keine Abweichungen fand (Pflicht-
@@ -71,10 +78,12 @@ func Reconcile(dbEntries []DBEntry, storageKeys []string) Report {
dbSet[e.StorageKey] = e dbSet[e.StorageKey] = e
} }
var missing []Finding var missing, existing []Finding
for _, e := range dbEntries { for _, e := range dbEntries {
if !storageSet[e.StorageKey] { if !storageSet[e.StorageKey] {
missing = append(missing, Finding(e)) missing = append(missing, Finding(e))
} else {
existing = append(existing, Finding(e))
} }
} }
var orphaned []Finding var orphaned []Finding
@@ -86,10 +95,12 @@ func Reconcile(dbEntries []DBEntry, storageKeys []string) Report {
sort.Slice(missing, func(i, j int) bool { return missing[i].StorageKey < missing[j].StorageKey }) sort.Slice(missing, func(i, j int) bool { return missing[i].StorageKey < missing[j].StorageKey })
sort.Slice(orphaned, func(i, j int) bool { return orphaned[i].StorageKey < orphaned[j].StorageKey }) sort.Slice(orphaned, func(i, j int) bool { return orphaned[i].StorageKey < orphaned[j].StorageKey })
sort.Slice(existing, func(i, j int) bool { return existing[i].StorageKey < existing[j].StorageKey })
return Report{ return Report{
GeneratedAt: time.Now().UTC(), GeneratedAt: time.Now().UTC(),
MissingInStorage: missing, MissingInStorage: missing,
OrphanedInStorage: orphaned, OrphanedInStorage: orphaned,
ExistingInStorage: existing,
} }
} }
@@ -89,6 +89,38 @@ func TestReconcile_ExistingButCorruptedObjectProducesNoFinding(t *testing.T) {
} }
} }
// TestReconcile_ExistingInStorageIsStableSamplingBasis ist der Nachweis,
// dass Reconcile eine deterministisch sortierte Liste ALLER bestaetigt
// existierenden Objekte liefert (DB-Eintrag UND Storage-Objekt vorhanden)
// - dies ist die Stichprobengrundlage, die Archive BAK-08 weiterverwendet,
// ohne selbst neu zu sortieren/filtern.
func TestReconcile_ExistingInStorageIsStableSamplingBasis(t *testing.T) {
db := []DBEntry{
{StorageKey: "documents/z/revisions/r1", DocumentID: "z", RevisionID: "r1"},
{StorageKey: "documents/a/revisions/r1", DocumentID: "a", RevisionID: "r1"},
{StorageKey: "documents/fehlt/revisions/r1", DocumentID: "fehlt", RevisionID: "r1"},
}
storage := []string{
"documents/z/revisions/r1",
"documents/a/revisions/r1",
}
report := Reconcile(db, storage)
want := []string{"documents/a/revisions/r1", "documents/z/revisions/r1"}
if len(report.ExistingInStorage) != len(want) {
t.Fatalf("erwartet %d bestaetigt existierende objekte, habe %d: %+v", len(want), len(report.ExistingInStorage), report.ExistingInStorage)
}
for i, w := range want {
if report.ExistingInStorage[i].StorageKey != w {
t.Fatalf("sortierreihenfolge falsch: %v, want beginnend mit %v", report.ExistingInStorage, want)
}
}
if len(report.MissingInStorage) != 1 || report.MissingInStorage[0].StorageKey != "documents/fehlt/revisions/r1" {
t.Fatalf("missing_in_storage unerwartet: %+v", report.MissingInStorage)
}
}
// TestReconcile_DeterministicOrdering ist der Nachweis fuer die // TestReconcile_DeterministicOrdering ist der Nachweis fuer die
// Stabilitaets-Anforderung: gleiche Eingabe liefert bei mehreren Laeufen // Stabilitaets-Anforderung: gleiche Eingabe liefert bei mehreren Laeufen
// IMMER dieselbe Reihenfolge (Voraussetzung dafuer, dass Archive BAK-08 // IMMER dieselbe Reihenfolge (Voraussetzung dafuer, dass Archive BAK-08