controller v0.265.0: R-634 cause fixed, held apps say so, OOM storm alarm, R-647 leftovers
gates / gates (push) Successful in 27s

R-634: a whole-box backup no longer stops/restarts a DEPLOYING app (the
measured cause of containers running under 'not deployed'); StopStack
and StartStack refuse a deploying stack for every caller.
R-625: held badge 'Stopped - restore needed', no Update button.
R-636: kernel oom_kill counter; 20+ in 30 min -> one app_oom_storm.
R-647: held error per reader, copy_holds key, two log wordings.

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:
2026-09-23 17:16:57 +02:00
parent 0a3026180a
commit 0054d4bd69
28 changed files with 832 additions and 48 deletions
+22 -4
View File
@@ -620,7 +620,7 @@ func main() {
d.CopyDate = ev.CopyDate.UTC().Format(time.RFC3339)
}
if backupMgr != nil && ev.CopyTier > 0 {
d.CopyHolds = backupMgr.UpdateCopyHolds(ev.App, ev.CopyTier)
d.CopyHolds = backupMgr.UpdateCopyHoldsKey(ev.App, ev.CopyTier) // R-647: the key, never the Hungarian phrase
}
notifier.NotifyAppUpdateHeld(d, func(lang string) string {
if ev.HoldRecorded && backupMgr != nil {
@@ -851,8 +851,8 @@ func main() {
logger.Printf("[WARN] [deadapp] OOM scan failed: %v", oerr)
} else {
for _, o := range ooms {
logger.Printf("[WARN] [deadapp] %s: container %s was OOM-killed (started %s)", o.Stack, o.Container, o.StartedAt)
notifier.NotifyAppOOM(o.Stack, o.Container, o.StartedAt)
logger.Printf("[WARN] [deadapp] %s: container %s was OOM-killed (started %s; kills %d, limit %s, peak %s)", o.Stack, o.Container, o.StartedAt, o.Kills, o.MemLimit, o.Peak)
notifier.NotifyAppOOM(o.Stack, o.Container, o.StartedAt, o.Kills, o.MemLimit, o.Peak)
}
}
deadAppScans++
@@ -2556,7 +2556,10 @@ func (a *stackAdapter) GetStackClassifiedBinds(name string) ([]backup.Classified
func (a *stackAdapter) ListDeployedStacks() []backup.StackSummary {
var result []backup.StackSummary
for _, s := range a.mgr.GetStacks() {
if !s.Deployed {
// R-634 (v0.265.0): `Deployed` is set true in memory the moment a deploy is ACCEPTED (the
// no-stale-button UX), so without the second test a deploy still pulling was in every backup
// leg's list — and the volume leg stopped and restarted it in the middle of its own `up`.
if !s.Deployed || s.Deploying {
continue
}
result = append(result, backup.StackSummary{
@@ -2574,6 +2577,13 @@ func (a *stackAdapter) StopStack(name string) error {
return a.mgr.StopStack(name)
}
// IsDeploying answers backup's optional deployingReporter (R-634): the volume leg asks it again
// immediately before it stops an app, because its list is taken once at the start of the run.
func (a *stackAdapter) IsDeploying(name string) bool {
s, ok := a.mgr.GetStack(name)
return ok && s.Deploying
}
func (a *stackAdapter) StartStack(name string) error {
return a.mgr.StartStack(name)
}
@@ -3471,6 +3481,14 @@ func (a *updateGuardsAdapter) HoldFor(name string) (bool, string) {
return a.b.RestoreHoldFor(name)
}
// HoldForLang renders the hold sentence for one reader (R-647, v0.265.0).
func (a *updateGuardsAdapter) HoldForLang(name, lang string) (bool, string) {
if a.b == nil {
return false, ""
}
return a.b.RestoreHoldForLang(name, lang)
}
func (a *updateGuardsAdapter) Busy(name string) (bool, string) {
if a.q.SuppressedStacks()[name] {
return true, "a whole-guest backup (quiesce) is holding it"
@@ -1,6 +1,10 @@
package main
import "testing"
import (
"os"
"strings"
"testing"
)
// v0.264.0. The update-event sink and the R-646 backfill are both CALLED from main.go (an AST walk —
// a comment naming them would not count). A sink built and never wired would fail silently: no event,
@@ -21,3 +25,16 @@ func TestUpdateEventSinkIsWiredAtStartup(t *testing.T) {
t.Error("BackfillAppliedMeta (R-646) must be called, after AdoptPins")
}
}
// R-647 (2) — the held event carries the copy_holds KEY, never the Hungarian phrase: the hub prints the
// raw details as the household mail's `Note:` line. COMPANION RED-PROOF (REPORT.md): wire
// UpdateCopyHolds back — this fails.
func TestR647_HeldEventCarriesTheCopyHoldsKey(t *testing.T) {
src, err := os.ReadFile("main.go")
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(src), "d.CopyHolds = backupMgr.UpdateCopyHoldsKey(") {
t.Fatal("main.go must fill AppUpdateDetails.CopyHolds with UpdateCopyHoldsKey (the key), not the Hungarian phrase")
}
}