v0.243.0: FileBrowser generated admin password (R-513); per-tier whole-guest backup truth (R-517); skip absent-storage tiers (R-518); OOM-killed worker visible (R-514)
gates / gates (push) Successful in 14s
gates / gates (push) Successful in 14s
MinAgent: 0.131.0 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
package stacks
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// R-514 (v0.243.0) — an OOM-killed worker inside a RUNNING container is invisible to the state model.
|
||||
//
|
||||
// BIGNIGHT: Paperless's celery worker was killed by the memory cgroup inside the webserver container;
|
||||
// the container kept running (`docker inspect` → OOMKilled=true, RestartCount=0), the app read „Fut",
|
||||
// 11 documents failed and 8 waited forever. Docker's State.OOMKilled is set when ANY process in the
|
||||
// container's cgroup was OOM-killed and stays set until the container restarts — so it is the
|
||||
// observable, read for the running containers of deployed stacks.
|
||||
|
||||
// OOMContainer is one container whose cgroup had a process OOM-killed since it started.
|
||||
type OOMContainer struct {
|
||||
Stack string
|
||||
Container string
|
||||
StartedAt string // identifies the container run — one event per run
|
||||
}
|
||||
|
||||
// ScanOOMKilled inspects the containers of every deployed, running-ish stack in ONE docker call and
|
||||
// returns those with State.OOMKilled=true. It also caches the per-stack result for the dashboard.
|
||||
func (m *Manager) ScanOOMKilled() ([]OOMContainer, error) {
|
||||
m.mu.RLock()
|
||||
owner := map[string]string{}
|
||||
var names []string
|
||||
for name, st := range m.stacks {
|
||||
if !st.Deployed {
|
||||
continue
|
||||
}
|
||||
for _, c := range st.Containers {
|
||||
if c.State == StateRunning || c.State == StateUnhealthy || c.State == StateRestarting {
|
||||
owner[c.Name] = name
|
||||
names = append(names, c.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
m.mu.RUnlock()
|
||||
if len(names) == 0 {
|
||||
m.setOOMCache(nil)
|
||||
return nil, nil
|
||||
}
|
||||
sort.Strings(names)
|
||||
args := append([]string{"inspect", "-f", "{{.Name}}|{{.State.OOMKilled}}|{{.State.StartedAt}}"}, names...)
|
||||
out, err := m.execCommand("docker", args...)
|
||||
if err != nil && strings.TrimSpace(out) == "" {
|
||||
return nil, err
|
||||
}
|
||||
var found []OOMContainer
|
||||
for _, line := range strings.Split(strings.TrimSpace(out), "\n") {
|
||||
f := strings.SplitN(strings.TrimSpace(line), "|", 3)
|
||||
if len(f) != 3 || f[1] != "true" {
|
||||
continue
|
||||
}
|
||||
cname := strings.TrimPrefix(f[0], "/")
|
||||
if st, ok := owner[cname]; ok {
|
||||
found = append(found, OOMContainer{Stack: st, Container: cname, StartedAt: f[2]})
|
||||
}
|
||||
}
|
||||
m.setOOMCache(found)
|
||||
return found, nil
|
||||
}
|
||||
|
||||
func (m *Manager) setOOMCache(found []OOMContainer) {
|
||||
cache := map[string][]string{}
|
||||
for _, o := range found {
|
||||
cache[o.Stack] = append(cache[o.Stack], o.Container)
|
||||
}
|
||||
m.oomMu.Lock()
|
||||
m.oomCache = cache
|
||||
m.oomMu.Unlock()
|
||||
}
|
||||
|
||||
// OOMKilledStacks returns stack name → OOM-killed container names from the last scan.
|
||||
func (m *Manager) OOMKilledStacks() map[string][]string {
|
||||
m.oomMu.Lock()
|
||||
defer m.oomMu.Unlock()
|
||||
out := make(map[string][]string, len(m.oomCache))
|
||||
for k, v := range m.oomCache {
|
||||
out[k] = append([]string(nil), v...)
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user