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
gates / gates (push) Successful in 1m24s
This commit is contained in:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user