internal/restore: Atomarer Restore ueber Temp-Verzeichnis + Rename, nicht-leeres Ziel ohne -force bricht VOR jeder Beruehrung ab, JSONL- Protokoll jedes Laufs. Drei reale Defekte beim Bau gefunden und behoben: pg_combinebackup braucht Plain- statt Tar-Format (Extraktionsschritt ergaenzt), pg_wal.tar.gz wurde nie verifiziert/wiederhergestellt (BAK-01s Verify jetzt erweitert), Go-exec haengt bei pg_ctl start wegen vererbter Pipes (Testfix: echte Logdatei statt CombinedOutput). Beide Restore-Pfade real auf 131 ueber restore-cli nachgewiesen, inkl. echtem Postgres-Start aus wiederhergestelltem Verzeichnis.
7.2 KiB
BAK-03 – Prüfprotokoll: Restore-Verfahren
Voraussetzungen BAK-01, BAK-02 – erledigt, siehe eigene Protokolle.
Grundsatzentscheidung: Atomarität über Temp-Verzeichnis + Rename
internal/restore.AtomicRestore kennt weder Postgres noch restic —
reine Ablaufsteuerung: Wiederherstellung IMMER in ein frisches
temporäres Verzeichnis (nie direkt in das Ziel), Übernahme erst bei
Erfolg per os.Rename (selbes Dateisystem wie das Ziel, daher atomar).
Ist das Ziel nicht leer und keine Bestätigung (-force) gegeben, wird
NICHT einmal die Wiederherstellungsfunktion aufgerufen — Abbruch vor
jeder Berührung des Ziels (Akzeptanzkriterium 2). Jeder Aufruf — Erfolg,
Abbruch oder Fehler — erzeugt genau einen JSONL-Protokolleintrag
(Akzeptanzkriterium 3).
Die eigentliche Wiederherstellung bleibt in internal/backup.Restore
(Datenbank) und internal/objectbackup.Restore (Objekt-Storage) —
internal/restore orchestriert nur.
Drei reale Defekte während der Implementierung gefunden und behoben
Alle drei erst durch den ECHTEN Restore-Test (Postgres-Instanz tatsächlich gestartet, nicht nur Dateien verglichen) aufgedeckt:
pg_combinebackupbraucht Plain-Format, BAK-01 liefert Tar+Gzip.Restoreextrahiert jetzt jede Sicherungsstufe zunächst in ein temporäres Plain-Verzeichnis (inkl.backup_manifest-Kopie) und speist erst DIESE inpg_combinebackup. BAK-01s Speicherformat selbst unverändert (kleinere, leichter prüfbare Dateien).pg_wal.tar.gzwurde nie verifiziert oder wiederhergestellt.pg_basebackups Standard-WAL-Methode (stream) erzeugt bei-Ft -zeine ZWEITE Archivdatei nebenbase.tar.gz— ohne sie ist keine crash-konsistente Wiederherstellung möglich (Postgres findet sonst keinen gültigen Checkpoint).backup.Verifyprüft jetzt BEIDE Archive vollständig;backup.Restoreextrahiert das WAL der ZULETZT gezogenen Stufe (nicht aller Stufen) nachpg_wal/im Wiederherstellungsziel. Ergänzung zu BAK-01s Prüfung, keine Umstellung des Sicherungsverfahrens.- Go-
exec-Hänger beipg_ctl start:pg_ctlstartet Postgres als Hintergrundprozess, der die geerbten stdout/stderr-Pipes NICHT schließt —CombinedOutput()(wartet auf Pipe-EOF) hängt sich dadurch auf, obwohlpg_ctlselbst längst zurückgekehrt ist. Fix nur im Testcode: echte Logdatei (-l) statt Pipe, Standard-pg_ctl-Muster.
Zusätzlich real (nicht Ticket-relevant, aber notiert): auf diesem
Debian-Postgres liegen postgresql.conf/pg_hba.conf NICHT in PGDATA
(sondern /etc/postgresql/17/main/) — ein echtes Restore-Runbook muss
diese Dateien separat mitführen/rekonstruieren, pg_basebackup sichert
sie nicht. Für den Testnachweis minimal nachgereicht, kein Produktcode.
Umsetzung
internal/restore.AtomicRestore— Ablaufsteuerung, JSONL-Protokoll (Timestamp,Kind,Source,Target,Result).internal/backup.Restore—pg_combinebackupgegen extrahierte Plain-Verzeichnisse + WAL-Wiederherstellung der letzten Stufe.internal/backup.extractTarGz(inverify.go, neben der bestehenden Tar/Gzip-Leselogik) — vollständige Extraktion, kein Kopf-only-Read.internal/backup.Verify— prüft jetztbase.tar.gzUNDpg_wal.tar.gzvollständig.internal/objectbackup.Restore—restic restore --target.cmd/restore-cli—database/objects-Unterbefehle,-force-Flag (vor Positionsargumenten, Go-flag-Konvention), kein systemd-Timer (Notfall-/Einzelfall-Werkzeug, kein Zeitplan).
Prüfungen
| # | Prüfung | Ergebnis |
|---|---|---|
| 1 | Restore auf leerem System vollständig und erfolgreich durchgeführt | bestanden — real: TestRestore_CombinesIntoStartablePostgresInstance (Postgres tatsächlich aus wiederhergestelltem Verzeichnis gestartet, SELECT 1 über echte Verbindung beantwortet); zusätzlich real per restore-cli database auf 131 ausgeführt (PG_VERSION/base/pg_wal vorhanden, Exit 0); TestRestore_RecoversRealContentFromSnapshot (Objekt-Storage, echter Dateiinhalt verglichen) UND real per restore-cli objects auf 131 (echter restic-Snapshot wiederhergestellt) |
| 2 | Restore auf nicht-leeres Zielverzeichnis lässt bei Abbruch den ursprünglichen Inhalt unverändert | bestanden — TestAtomicRestore_NonEmptyTargetWithoutForce_LeavesOriginalUntouched (restoreFn wird nachweislich NIE aufgerufen) und TestAtomicRestore_FailedRestoreFn_LeavesTargetUntouched; real auf 131: restore-cli database gegen nicht-leeres Ziel ohne -force → Abbruch, bestehende Testdatei unverändert vorhanden |
| 3 | Protokolleintrag zum Restore ist vollständig und nachvollziehbar | bestanden — TestAtomicRestore_LogsCompleteEntry (Quelle, Ziel, Zeitpunkt, Ergebnis für Erfolgs- UND Abbruchfall in derselben Datei); real auf 131: /tmp/restore-cli-test.log zeigt alle vier realen Läufe (Abbruch, Fehler mangels PATH, Erfolg, Force-Überschreiben) korrekt protokolliert |
Zusätzlich: TestAtomicRestore_EmptyTarget_Succeeds,
TestAtomicRestore_ForceOverwritesNonEmptyTarget (bewusste Bestätigung
ersetzt bestehenden Inhalt real, alter Inhalt nachweislich weg, neuer da).
Echte Verdrahtung auf 192.168.1.131
restore-cligebaut nach/opt/nexarch-archive/bin/- Kein systemd-Timer (bewusst — Notfall-/Einzelfall-Werkzeug)
- Vier reale CLI-Läufe durchgeführt und protokolliert: Abbruch bei
nicht-leerem Ziel ohne
-force, Fehler mangelspg_combinebackupim PATH (zeigt: Fehler wird korrekt erkannt UND protokolliert, kein stiller Fehlschlag), erfolgreicher Restore einer echten Generation (20260829T222054Z) in leeres Ziel, erfolgreiches Überschreiben mit-force. Objekt-Storage-Restore real gegen echten restic-Snapshot (43da36bf) — Inhalt (.placeholder-Datei) tatsächlich vorhanden. - Alle Testartefakte (
/tmp/restore-*, Testlog) nach Prüfung entfernt.
Build/Test-Ergebnis
go build ./... -> clean
go vet ./... -> clean
golangci-lint run ./... -> 0 issues
internal/restore: 5/5 Tests (reine Funktionen, kein Setup nötig).
internal/objectbackup: TestRestore_RecoversRealContentFromSnapshot
bestanden (zusammen mit den bestehenden BAK-02-Tests).
internal/backup: TestRestore_CombinesIntoStartablePostgresInstance
bestanden — Hinweis: dieser eine Test läuft NICHT als root
(pg_ctl: cannot be run as root) und braucht pg_combinebackup/
pg_ctl im PATH (/usr/lib/postgresql/17/bin auf Debian, dort nicht
standardmäßig verlinkt) — daher separat als postgres-Systemnutzer mit
entsprechendem PATH ausgeführt, nicht Teil des root-make check-Laufs;
dort wird er mit klarer Meldung übersprungen (pg_combinebackup nicht installiert), kein stiller Fehlschlag. Alle übrigen BAK-01-Tests
(inkl. der um pg_wal.tar.gz erweiterten Verify) liefen unverändert
grün im normalen make check-Lauf.
Gesamtergebnis
Bestanden. Alle drei Akzeptanzkriterien und alle drei
Pflichtprüfungen real erfüllt — beide Restore-Pfade (Datenbank,
Objekt-Storage) sowohl testgetrieben als auch manuell über restore-cli
auf echter Infrastruktur nachgewiesen. Drei während der Implementierung
gefundene reale Defekte (Tar-vs-Plain-Format, fehlende WAL-Sicherung/
-Verifikation, Go-exec-Pipe-Hänger) behoben und dokumentiert, nicht
stillschweigend umgangen.