R-1286: a held app says why it is held — each hold reason gets its own sentence in the start gate, the boot sweep and --restore-holds (an unhealthy stop read as a failed restore). Red-proved. Commit only.
gates / gates (push) Successful in 1m28s

This commit is contained in:
admin committed 2026-10-25 22:05:23 +01:00
1 parent b5863a96eb
commit 838c834169
3 files changed
+71 -5

No files matched your search

+24 -5
View File
@@ -192,7 +192,7 @@ func main() {
os.Exit(0)
}
for _, h := range holds {
fmt.Printf("%s\theld since %s\n replay error : %s\n rollback err : %s\n", h.Stack, h.At, h.ReplayError, h.RollbackErr)
fmt.Printf("%s\theld since %s\n why : %s\n replay error : %s\n rollback err : %s\n", h.Stack, h.At, holdWhy(h), h.ReplayError, h.RollbackErr)
}
os.Exit(0)
}
@@ -2558,6 +2558,28 @@ type driveStartGate struct {
sett *settings.Settings
}
// holdWhy says which operation put a hold in place and what lifts it — one sentence per reason (R-1286: an unhealthy
// stop read as „held after a failed restore whose rollback also failed" for 11 days on demo-hp, so the operator could
// not tell a crash loop from a broken restore). Pinned by TestR1286_EveryHoldReasonSaysWhatItIs.
func holdWhy(h settings.RestoreHold) string {
switch h.Reason {
case settings.HoldReasonUpdateFailed:
return "held after a failed update (" + h.At + ") — restore it from its backup to start it"
case settings.HoldReasonUnhealthyStop:
kind := h.UnhealthyKind
if kind == "" {
kind = "unhealthy"
}
return "stopped by the box for a " + strings.ReplaceAll(kind, "_", " ") + " (" + h.At + ") — the household's Start gives it one more try"
case settings.HoldReasonRestoreMixed:
return "held after an off-site restore failed its database replay (" + h.At + ") — clear the hold to start it"
case settings.HoldReasonPutBackPending:
return "held until the pre-restore copy is poured back (" + h.At + ") — the put-back starts it"
default:
return "held after a failed restore whose rollback also failed (" + h.At + ") — clear the hold to start it"
}
}
func (g driveStartGate) MayStart(stackName string) (bool, string) {
// R-379/R-380 HOLDER, checked FIRST and deliberately ABOVE the `HDD_PATH == ""` early return
// below. The apps this hold exists for are DRIVELESS — a failed database restore is the case,
@@ -2569,10 +2591,7 @@ func (g driveStartGate) MayStart(stackName string) (bool, string) {
// app-stop guard's Recover reaches it directly. A hold only one path honours is not a hold.
if g.sett != nil {
if h, ok := g.sett.GetRestoreHold(stackName); ok {
if h.Reason == settings.HoldReasonUpdateFailed {
return false, "held after a failed update (" + h.At + ") — restore it from its backup to start it"
}
return false, "held after a failed restore whose rollback also failed (" + h.At + ") — clear the hold to start it"
return false, holdWhy(h)
}
}
if g.mgr == nil {