D22: every restore puts the data back exactly on failure and after a power cut (R-1177, R-1195); whole restore asks the unit first (R-1178); no mirror during a restore (R-1180); W22 restore refusals in Hungarian; unreleased
gates / gates (push) Successful in 1m40s

This commit is contained in:
2026-10-21 21:19:46 +02:00
parent 7848d4aaac
commit d582feb245
25 changed files with 1065 additions and 71 deletions
+14 -1
View File
@@ -457,6 +457,15 @@ func main() {
gate: driveStartGate{mgr: stackMgr, sett: sett},
logger: logger,
})
// D22 / R-1195: a restore cut after its copy of the live data was taken is put back before the restart — the
// copy's volumes poured back and the live definition written back (backup/restore_crash_putback.go).
appStopGuard.SetPutBacker(backup.NewCrashPutBack(stackMgr.StopStack, func(name, defDir string) error {
cfg := stacks.LoadAppConfigDecrypted(defDir, encKey)
if cfg == nil {
return fmt.Errorf("the kept app.yaml of %s is unreadable", name)
}
return (&stackAdapter{mgr: stackMgr, encKey: encKey}).RecreateStackDefinitionFromUnit(name, defDir, cfg.Env)
}, logger))
appStopRecovery := appStopGuard.Recover()
// --- R-166: desired-state backfill (running-only) ---
@@ -568,7 +577,11 @@ func main() {
if cfg.Backup.Enabled {
backupMgr = backup.NewManager(cfg, sett, logger)
// R-550: load the persisted restore record BEFORE any page can ask for it.
interruptedRestore = loadRestoreRecordAtStartup(backupMgr, cfg.Paths.DataDir, logger)
var putBack []string
if appStopRecovery != nil {
putBack = appStopRecovery.PutBack
}
interruptedRestore = loadRestoreRecordAtStartup(backupMgr, cfg.Paths.DataDir, logger, putBack)
// R-519: an app-data run the stop cut off is said on the backup pages until the next complete run.
loadRunRecordAtStartup(backupMgr, cfg.Paths.DataDir, logger)
// R-166: use the guard that already ran Recover at startup, not a second one over the same
@@ -24,13 +24,14 @@ type restoreEventPusher interface {
}
// loadRestoreRecordAtStartup wires persistence and returns the restore the last stop interrupted.
func loadRestoreRecordAtStartup(mgr *backup.Manager, dataDir string, logger *log.Logger) *backup.RestoreOpResult {
// putBack names the apps the crash recovery put back (AppStopRecovery.PutBack, D22 / R-1195).
func loadRestoreRecordAtStartup(mgr *backup.Manager, dataDir string, logger *log.Logger, putBack []string) *backup.RestoreOpResult {
if mgr == nil {
return nil
}
path := filepath.Join(dataDir, restoreRecordFileName)
mgr.SetRestoreRecordPath(path)
res := mgr.LoadRestoreRecord()
res := mgr.LoadRestoreRecordAfterRecovery(putBack)
if logger != nil {
logger.Printf("[INFO] [backup] restore record wired: %s (interrupted at startup: %t)", path, res != nil)
}
@@ -42,6 +43,12 @@ func reportInterruptedRestore(p restoreEventPusher, res *backup.RestoreOpResult)
if p == nil || res == nil {
return
}
if res.PutBack {
// D22 / R-1195: the data is back as before the restore — the record's own sentence, in the box's language.
p.PushEvent("restore_interrupted", "warning", res.Stack+": "+res.Message,
map[string]any{"stack": res.Stack, "op": res.Op, "put_back": true})
return
}
p.PushEvent("restore_interrupted", "warning",
fmt.Sprintf("A(z) %s visszaállítása megszakadt, mert a doboz újraindult — indítsd el újra.", res.Stack),
map[string]any{"stack": res.Stack, "op": res.Op})
@@ -33,13 +33,13 @@ func TestRestoreRecordAtStartup_RaisesInterruptedOnce(t *testing.T) {
before.BeginRestoreOp("restore", "gokapi") // the box stops here
rec := &pushRecorder{}
reportInterruptedRestore(rec, loadRestoreRecordAtStartup(new(backup.Manager), dir, lg))
reportInterruptedRestore(rec, loadRestoreRecordAtStartup(new(backup.Manager), dir, lg, nil))
if len(rec.got) != 1 || rec.got[0].eventType != "restore_interrupted" || rec.got[0].severity != "warning" {
t.Fatalf("an interrupted restore raised no restore_interrupted (warning): %+v", rec.got)
}
again := &pushRecorder{}
reportInterruptedRestore(again, loadRestoreRecordAtStartup(new(backup.Manager), dir, lg))
reportInterruptedRestore(again, loadRestoreRecordAtStartup(new(backup.Manager), dir, lg, nil))
if len(again.got) != 0 {
t.Fatalf("the same interruption was raised again on the next start: %+v", again.got)
}