R-1251: sync the restore copy's folders before the marker names it (a cut left undo.json as .tmp on Tester 1); the interrupted-restore record says the put-back waits, not 'run it again'. Commit only.
gates / gates (push) Successful in 1m24s

This commit is contained in:
2026-10-24 19:36:32 +02:00
parent bb2809dc68
commit 062f9b41d9
12 changed files with 141 additions and 16 deletions
@@ -73,6 +73,15 @@ func (m *Manager) persistUndoForCrash(stack string, c *liveVolumeCopy) {
m.logger.Printf("[WARN] [backup] %s: the restore copy's journal could not be written: %v — a power cut would restart the app without putting it back", stack, err)
return
}
// MEASURED 2026-10-24 on Tester 1 (audits/restore-holes-2026-10-24/P1-tester1/): a power cut 2.1 s after this
// record left the copy's tar whole but its journal as undo.json.tmp — the rename lived only in the page cache, and
// the marker (another filesystem, already synced) named a copy no start could read. The copy's folders are new
// (copyLiveVolumes made them), so their own entries are not durable either. Every folder from the copy up to
// backups/ is synced BEFORE the marker names the copy; a sync that fails is logged and the copy is still recorded
// (an unreadable copy at start holds the app for support, R-1251 H2 — never a start on unknown data).
if err := syncUndoDirs(c.dir); err != nil {
m.logger.Printf("[WARN] [backup] %s: the restore copy's folders could not be synced to disk: %v — a power cut in the next seconds may leave a copy the start cannot read (the app is then held)", stack, err)
}
if err := m.appStop.SetUndo(stack, c.dir); err != nil {
m.logger.Printf("[WARN] [backup] %s: the restore copy could not be recorded for a power cut: %v", stack, err)
return
@@ -100,7 +109,38 @@ func copyDefinitionFiles(stackDir, dst string) error {
return nil
}
// atomicWriteFile writes data to path through a synced temp file and a rename (0600).
// syncUndoDirs makes a restore copy's folder entries durable: its definition/ folder, the copy folder, and each parent
// up to and including backups/ (restoreUndoDir = <ns>/backups/restore-undo/<app>/<stamp>). A seam for tests.
var syncUndoDirs = func(copyDir string) error {
dirs := []string{filepath.Join(copyDir, undoDefinitionIn), copyDir}
for d, i := copyDir, 0; i < 3; i++ { // <app>, restore-undo, backups
d = filepath.Dir(d)
dirs = append(dirs, d)
}
var first error
for _, d := range dirs {
if err := syncDir(d); err != nil && !os.IsNotExist(err) && first == nil {
first = err
}
}
return first
}
// syncDir fsyncs one directory, so the entries created or renamed in it survive a power cut.
func syncDir(dir string) error {
f, err := os.Open(dir)
if err != nil {
return err
}
if err := f.Sync(); err != nil {
f.Close()
return err
}
return f.Close()
}
// atomicWriteFile writes data to path through a synced temp file and a rename (0600), then syncs the folder so the
// rename itself survives a power cut (R-1251: a journal left as .tmp was measured after a cut).
func atomicWriteFile(path string, data []byte) error {
tmp := path + ".tmp"
f, err := os.OpenFile(tmp, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
@@ -121,7 +161,10 @@ func atomicWriteFile(path string, data []byte) error {
os.Remove(tmp)
return err
}
return os.Rename(tmp, path)
if err := os.Rename(tmp, path); err != nil {
return err
}
return syncDir(filepath.Dir(path))
}
// CrashPutBack is the InterruptedRestorePutBacker the controller wires at start, before the backup manager exists.