controller v0.268.0: the undo finds volumes by definition; a held app names only a whole copy; one press = one tested step (R-658, R-659, R-660, R-651; 09 §6.4 part 5)
gates / gates (push) Successful in 27s
gates / gates (push) Successful in 27s
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:
@@ -622,6 +622,17 @@ func main() {
|
||||
if backupMgr != nil && ev.CopyTier > 0 {
|
||||
d.CopyHolds = backupMgr.UpdateCopyHoldsKey(ev.App, ev.CopyTier) // R-647: the key, never the Hungarian phrase
|
||||
}
|
||||
var hold settings.RestoreHold
|
||||
haveHold := false
|
||||
if ev.HoldRecorded && backupMgr != nil {
|
||||
hold, haveHold = backupMgr.UpdateHold(ev.App)
|
||||
}
|
||||
if haveHold { // R-659: the details name the copy the SENTENCE names, not the precondition's
|
||||
d.CopyTier, d.CopyDate, d.CopyHolds = hold.CopyTier, hold.CopyDate, ""
|
||||
if hold.CopyTier > 0 {
|
||||
d.CopyHolds = backupMgr.UpdateCopyHoldsKey(ev.App, hold.CopyTier)
|
||||
}
|
||||
}
|
||||
notifier.NotifyAppUpdateHeld(d, func(lang string) string {
|
||||
if ev.HoldRecorded && backupMgr != nil {
|
||||
if held, why := backupMgr.RestoreHoldForLang(ev.App, lang); held {
|
||||
@@ -630,6 +641,12 @@ func main() {
|
||||
}
|
||||
return util.Text(lang, "update.error.hold_unsaved")
|
||||
})
|
||||
// R-659 (v0.268.0; operator ruling 2026-09-24, option A): no copy on this box brings the app
|
||||
// back whole — the household was just told support is informed; this is that information.
|
||||
if haveHold && hold.NoWholeCopy {
|
||||
notifier.NotifyAppHoldNoWholeCopy(notify.AppHoldNoWholeCopyDetails{App: ev.App, StackName: ev.App,
|
||||
From: ev.From, To: ev.To, At: ev.At.UTC().Format(time.RFC3339), CopiesSeen: hold.CopiesSeen, UndoState: hold.UndoState})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -843,7 +860,7 @@ func main() {
|
||||
if time.Since(startTime) < deadAppBootGrace {
|
||||
return nil // still inside the startup settle window
|
||||
}
|
||||
dead, states := scanDeployedAppRunStates(stackMgr, quiesceLoop, appStopGuard)
|
||||
dead, states := scanDeployedAppRunStates(stackMgr, quiesceLoop, appStopGuard, backupMgr)
|
||||
alertMgr.SetDeadAppAlerts(dead)
|
||||
notifier.NotifyAppStartFailures(states)
|
||||
// R-514: a worker OOM-killed inside a running container leaves the app „Fut". Surface it.
|
||||
@@ -2353,7 +2370,7 @@ func recordLateRecovery(logger *log.Logger, started time.Time, res bootrecon.Res
|
||||
// state-based dashboard banner) and EVERY deployed app's run state (for the notifier's one-event-per-
|
||||
// transition tracking). Deploying apps are skipped (mid-deploy is not a fault). Pure over GetStacks()
|
||||
// — the derivation itself lives in classifyRunStates so it is testable without a live Manager.
|
||||
func scanDeployedAppRunStates(mgr *stacks.Manager, q *quiesce.Loop, g *backup.AppStopGuard) ([]web.DeadApp, []notify.AppRunState) {
|
||||
func scanDeployedAppRunStates(mgr *stacks.Manager, q *quiesce.Loop, g *backup.AppStopGuard, held updateHeldLister) ([]web.DeadApp, []notify.AppRunState) {
|
||||
// R-97b: a stack THIS controller stopped for a backup is not a fault. q may be nil (unprovisioned
|
||||
// guest) — SuppressedStacks is nil-safe and returns nothing, i.e. suppress nothing.
|
||||
//
|
||||
@@ -2366,9 +2383,26 @@ func scanDeployedAppRunStates(mgr *stacks.Manager, q *quiesce.Loop, g *backup.Ap
|
||||
// for health, and it ends in healthy or HELD. Counting it as dead mid-update would be R-330's false
|
||||
// alarm one mechanism over.
|
||||
suppressed := unionSuppressed(unionSuppressed(q.SuppressedStacks(), g.SuppressedStacks()), mgr.UpdatingStacks())
|
||||
// R-660 (v0.268.0): a FOURTH — an app HELD after a failed update is stopped by the product and has
|
||||
// its own event (`app_update_held`). Without this each hold was followed by an `app_start_failed`
|
||||
// for the same app. Pinned by TestR660_UpdateHeldAppIsNotReportedDown + the wiring test beside it.
|
||||
suppressed = unionSuppressed(suppressed, updateHeldSet(held))
|
||||
return classifyRunStates(mgr.GetStacks(), suppressed, q.FailedRestarts(), time.Now())
|
||||
}
|
||||
|
||||
// updateHeldLister is the backup manager's UpdateHeldStacks, as a seam for the test.
|
||||
type updateHeldLister interface {
|
||||
UpdateHeldStacks() map[string]bool
|
||||
}
|
||||
|
||||
// updateHeldSet is nil-safe over a nil interface (a box with backup disabled has no holds).
|
||||
func updateHeldSet(l updateHeldLister) map[string]bool {
|
||||
if l == nil {
|
||||
return nil
|
||||
}
|
||||
return l.UpdateHeldStacks()
|
||||
}
|
||||
|
||||
// unionSuppressed merges the suppression sets of the two mechanisms that stop apps on purpose.
|
||||
// Returns nil when both are empty so the common case allocates nothing.
|
||||
func unionSuppressed(a, b map[string]bool) map[string]bool {
|
||||
@@ -3545,6 +3579,17 @@ func (a *updateGuardsAdapter) HoldAfterFailedUpdate(name string, at time.Time, r
|
||||
if a.b == nil {
|
||||
return fmt.Errorf("backup is not enabled on this box — the hold cannot be recorded")
|
||||
}
|
||||
// R-479: the sentence names what the chosen copy holds, decided by the app's data layout NOW.
|
||||
return a.b.HoldAfterFailedUpdateHolding(name, at, rp.ProvenAt, rp.Tier, a.b.UpdateCopyHolds(name, rp.Tier), undoState)
|
||||
// R-659 (v0.268.0): the hold names the newest copy that brings the app back WHOLE — not the
|
||||
// precondition copy `rp`, which may be one the restore refuses — and none when there is none.
|
||||
// R-479 stands inside it: the sentence still says what that copy holds.
|
||||
_, err := a.b.HoldAfterFailedUpdateWhole(context.Background(), name, at, undoState)
|
||||
return err
|
||||
}
|
||||
|
||||
// HoldNoWholeCopy is the page's half of R-659: a hold naming no copy gets no restore button.
|
||||
func (a *updateGuardsAdapter) HoldNoWholeCopy(name string) bool {
|
||||
if a.b == nil {
|
||||
return false
|
||||
}
|
||||
return a.b.HoldNoWholeCopy(name)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user