R-645: the night backup skips an app whose pinned version is not the one it runs (09 decision 142)
Every night leg (DB dump, volume dump, recovery-unit capture, Tier-2 mirror) now leaves alone an app whose app.yaml pin (pinned_images) differs from its running record (installed_images) - the state a failed update leaves behind. A hold lifted by hand (--clear-restore-hold + restart) no longer lets the capture write the just-failed definition over the good unit. Unknown (no pin, no record, a service not observed) never skips. The log says it per leg; the backups page shows one amber line, hu + en (backup.status.version_skip). Seam: backup.Manager.SetVersionCheck <- stacks.Manager.PinNotRunning. Tests: TestR645_HandLiftedHoldKeepsTheGoodUnit (whole night run + Tier 2, unit tree fingerprint), TestR645_VersionSkipSentence, TestR645_PinNotRunning_*, TestR645_BackupRowSaysTheNightBackupSkipsIt, TestR645_VersionCheckIsWiredAtStartup. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -202,6 +202,8 @@ type Manager struct {
|
||||
|
||||
// updatingCheck (slice 4) — nil-safe; see isHeld / SetUpdatingCheck.
|
||||
updatingCheck func(stackName string) bool
|
||||
// versionCheck (R-645) — nil-safe; see versionSkip / SetVersionCheck.
|
||||
versionCheck func(stackName string) (detail string, mismatch bool)
|
||||
// undoCopyRemover (R-671, v0.272.0) deletes an app's leftover undo copies — stacks.Manager.RemoveUndoCopies,
|
||||
// wired in main.go (SetUndoCopyRemover). nil-safe: without it the copies stay, as before.
|
||||
undoCopyRemover func(stackName string) int
|
||||
@@ -620,6 +622,14 @@ func (m *Manager) runDBDumpsInternal(ctx context.Context) error {
|
||||
continue
|
||||
}
|
||||
|
||||
// R-645 (09 §3 decision 142): an app not running its pinned version is left alone — its dump
|
||||
// would land in the unit beside the good definition and become the restore point. A SKIP: the
|
||||
// unit keeps the last good copy, and the backups page says why.
|
||||
if why, skip := m.versionSkip(db.StackName); skip {
|
||||
m.logger.Printf("[WARN] [backup] Skipping DB dump for %s — it is not running its pinned version (%s); the last good backup is kept (R-645)", db.StackName, why)
|
||||
summary = append(summary, fmt.Sprintf("SKIP %s (not running its pinned version)", db.ContainerName))
|
||||
continue
|
||||
}
|
||||
// R-181: the reserve, BEFORE the first byte of this app's backup is written. This is usually
|
||||
// where an app's verdict is taken, because the DB leg runs first; the volume leg and the
|
||||
// capture then read the same memo. SKIP, not FAIL — a deliberate hold is not a broken dump,
|
||||
@@ -761,6 +771,12 @@ func (m *Manager) runVolumeDumps() (summary []string, dumped int, allOK bool) {
|
||||
summary = append(summary, fmt.Sprintf("SKIP %s volumes (held)", stack.Name))
|
||||
continue
|
||||
}
|
||||
// R-645: never stop, dump or restart an app that is not running its pinned version.
|
||||
if why, skip := m.versionSkip(stack.Name); skip {
|
||||
m.logger.Printf("[WARN] [backup] Skipping volume dump for %s — it is not running its pinned version (%s); the last good backup is kept (R-645)", stack.Name, why)
|
||||
summary = append(summary, fmt.Sprintf("SKIP %s volumes (not running its pinned version)", stack.Name))
|
||||
continue
|
||||
}
|
||||
// Volume check FIRST — a volume-less stack must not be stopped at all (see gate-order note).
|
||||
if len(m.stackProvider.GetDockerVolumes(stack.Name)) == 0 {
|
||||
if m.isDebug() {
|
||||
|
||||
Reference in New Issue
Block a user