R-1256: four restore edges (review round 2 HUNT-1 H4/H5/H6/H8) — each test seen failing first
gates / gates (push) Successful in 31s

H4: a failed definition write in the off-site restore writes the live definition back before the start;
    a failed write-back holds the app (restore_mixed).
H5: a put-back whose stop fails pours nothing, keeps the copy and holds the app (off-site, local, power-cut);
    a failed definition write-back keeps the copy (D20); a failed 'volume rm -f' fails that volume (no merge).
H6: TryBeginRestoreOp/RestoreBusy: check every operation and record the restore in one step under m.mu;
    every web handler and the kept loads begin through it; install-time kept load asks about the mirror too.
H8: SweepUnusedRestoreCopies every 6 h: removes a copy only with no hold, no marker naming it, no restore
    running, and older than 14 days.

Commit only; rides the next controller release.
This commit is contained in:
admin committed 2026-10-28 22:24:16 +01:00
1 parent ed3927f03a
commit 342a394bff
20 files changed
+860 -50

No files matched your search

+32 -10
View File
@@ -209,28 +209,50 @@ func (s *Server) restoreOpBlocked() (string, bool) {
if s.backupMgr == nil {
return "", false
}
if st := s.backupMgr.RestoreStatus(); restoreOpInFlight(st) {
busy, app := s.backupMgr.RestoreBusy()
return s.restoreBusyMessage(busy, app)
}
// beginRestoreOp records a restore op in the SAME step as the check for every other operation (R-1256 H6): the early
// restoreOpBlocked answer can be stale by the time a handler has read its form, and a second-drive copy that began in
// between ran beside the restore. A refusal returns the same sentence as restoreOpBlocked; the caller redirects with it
// and starts nothing. Every restore handler begins through here — TestR1256_H6_NoHandlerBeginsARestoreUnchecked.
func (s *Server) beginRestoreOp(op, stack string) (string, bool) {
busy, app := s.backupMgr.TryBeginRestoreOp(op, stack)
if busy == "" {
return "", true
}
if s.logger != nil {
s.logger.Printf("[WARN] [restore] %s of %s refused at its start — a %s operation runs (R-1256)", op, stack, busy)
}
msg, _ := s.restoreBusyMessage(busy, app)
return msg, false
}
// restoreBusyMessage is the household's sentence for a RestoreBusy reason ("" = not busy).
func (s *Server) restoreBusyMessage(busy, app string) (string, bool) {
switch busy {
case "":
return "", false
case backup.RestoreBusyRestore:
subject := "Egy visszaállítási művelet"
if st.Stack != "" {
subject = "Egy visszaállítási művelet (" + st.Stack + ")"
if app != "" {
subject = "Egy visszaállítási művelet (" + app + ")"
}
return subject + " már fut, ezért most nem indítható újabb. Az állapotát ezen az oldalon " +
"követheted; amint befejeződik, újra indíthatsz visszaállítást.", true
}
if s.backupMgr.IsRunning() {
case backup.RestoreBusyBackup:
return "Egy mentési művelet már fut, ezért most nem indítható visszaállítás. Az állapotát " +
"ezen az oldalon követheted; amint befejeződik, újra indíthatsz visszaállítást.", true
}
// The stray-copy fix: a second-drive copy (rsync --delete) runs now, or one a previous controller process lost may
// still run — a restore beside it could lose the files it brings back (R-1180's other direction).
if app, blocked := s.backupMgr.Tier2CopyBlocksRestore(); blocked {
default:
// The stray-copy fix: a second-drive copy (rsync --delete) runs now, or one a previous controller process lost
// may still run — a restore beside it could lose the files it brings back (R-1180's other direction).
if s.logger != nil {
s.logger.Printf("[WARN] [restore] restore refused — a second-drive copy is running or may still run (app=%q)", app)
}
return "Éppen fut a második meghajtóra készülő mentési másolat, ezért most nem indítható visszaállítás. " +
"Néhány perc múlva próbáld újra.", true
}
return "", false
}
// backupsRestoreWizardHandler renders GET /backups/restore/app?name=<app> — the single entry the