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
+52 -5
View File
@@ -564,7 +564,7 @@ func (m *Manager) finishUpdate(name, phase, msg string) {
// beside it for the page. key "" = `plain` is a finished sentence and is stored as it is.
func (m *Manager) finishUpdateKey(name, phase, key, plain string, args ...interface{}) {
msg := plain
if key != "" {
if key != "" && key != UpdateErrorKeyHeld { // the held key names no bundle entry: plain IS the sentence
msg = util.Text(i18n.Default, key, args...)
}
m.mu.Lock()
@@ -590,12 +590,57 @@ func UpdatePhaseLabelIn(lang, phase string) string {
// UpdateErrorIn is the stack's update sentence in lang (v0.264.0, R-606).
func (s Stack) UpdateErrorIn(lang string) string {
if s.UpdateErrorKey == "" || lang == i18n.Default {
if s.UpdateErrorKey == "" || s.UpdateErrorKey == UpdateErrorKeyHeld || lang == i18n.Default {
return s.UpdateError
}
return util.Text(lang, s.UpdateErrorKey, s.UpdateErrorArgs...)
}
// UpdateErrorKeyHeld marks an UpdateError that IS the hold sentence (R-647, v0.265.0). It names no
// bundle entry: the sentence is composed by the backup side, so UpdateErrorFor asks it for the
// reader's language, and UpdateErrorIn (no manager to ask) returns the stored Hungarian.
const UpdateErrorKeyHeld = "update.error.held"
// holdLanguage is the OPTIONAL half of UpdateGuards that renders the hold sentence in a given
// language (the production adapter implements it; the test fakes need not).
type holdLanguage interface {
HoldForLang(name, lang string) (bool, string)
}
func holdForLang(g UpdateGuards, name, lang string) (bool, string) {
if g == nil {
return false, ""
}
if hl, ok := g.(holdLanguage); ok {
return hl.HoldForLang(name, lang)
}
return g.HoldFor(name)
}
// HoldReasonFor is a stack's hold sentence in the READER's language (R-647): "" when nothing holds it.
// A Hungarian reader on a Hungarian box gets exactly Stack.HoldReason.
func (m *Manager) HoldReasonFor(st Stack, lang string) string {
if st.HoldReason == "" {
return ""
}
if held, why := holdForLang(m.guards(), st.Name, lang); held && why != "" {
return why
}
return st.HoldReason
}
// UpdateErrorFor is a stack's update error in the READER's language (R-606 + R-647). A held update's
// error is the hold sentence, rendered by the backup side for this reader.
func (m *Manager) UpdateErrorFor(st Stack, lang string) string {
if st.UpdateErrorKey == UpdateErrorKeyHeld {
if held, why := holdForLang(m.guards(), st.Name, lang); held && why != "" {
return why
}
return st.UpdateError
}
return st.UpdateErrorIn(lang)
}
func (m *Manager) updateCompose(dir string, env []string, args ...string) (string, error) {
if m.updateComposeFn != nil {
return m.updateComposeFn(dir, env, args...)
@@ -880,7 +925,7 @@ func (m *Manager) failAndHold(ctx context.Context, name, dir string, env []strin
m.logger.Printf("[ERROR] [stacks] update %s: no UpdateGuards — the hold CANNOT be recorded", name)
} else if err := g.HoldAfterFailedUpdate(name, m.now(), rp, undoState); err != nil {
m.logger.Printf("[ERROR] [stacks] update %s: %v", name, err)
} else if _, w := g.HoldFor(name); w != "" {
} else if _, w := holdForLang(g, name, i18n.Default); w != "" {
holdWhy = w
m.markUpdateHeld(name)
}
@@ -890,8 +935,10 @@ func (m *Manager) failAndHold(ctx context.Context, name, dir string, env []strin
if holdWhy == "" {
m.finishUpdateKey(name, UpdatePhaseFailed, "update.error.hold_unsaved", "")
} else {
// The hold's own sentence (the page renders it per reader through RestoreHoldForLang).
m.finishUpdate(name, UpdatePhaseFailed, holdWhy)
// R-647 (v0.265.0): stored as the HELD key + the Hungarian sentence, so every reader — the
// `?lang=` switch, an operator reading a box in the other language — gets the hold sentence in
// THEIR language (UpdateErrorFor), and the stored text is Hungarian as for every other key.
m.finishUpdateKey(name, UpdatePhaseFailed, UpdateErrorKeyHeld, holdWhy)
}
m.emitUpdateEvent(UpdateEventHeld, name, entry, rp, holdWhy != "")
}