gates / gates (push) Successful in 34s
Tests TestR528_* (fake cgroup tree), red-proved by mutation. Commit only; the agent release needs the vouch.
103 lines
3.3 KiB
Go
103 lines
3.3 KiB
Go
package localapi
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// R-528 — the OOM signal from the host side.
|
|
//
|
|
// MEASURED 2026-09-15 (scratch 9202, Docker 29.8.0) and 2026-09-16 (a fresh install in a nested VM): an app killed by
|
|
// its memory limit inside a Felhom LXC guest left Docker's State.OOMKilled false and sent no `oom` event, so the
|
|
// controller's OOM line (R-514) could not fire. The kernel's own counter does count every kill, and the host can read
|
|
// it: /sys/fs/cgroup/lxc/<vmid>/ns/system.slice/docker-<64-hex id>.scope/memory.events, line `oom_kill N` (the
|
|
// systemd cgroup driver; `ns/docker/<id>/` under the cgroupfs driver). Read on Tester 1's host 2026-10-28: the files
|
|
// are mode 0444, readable by the non-root felhom-agent user — no sudo, no new root path.
|
|
//
|
|
// A container's scope is new at each start, so the counter is per container RUN: a count above 0 means this run had a
|
|
// kill. The controller asks every 30 s and raises its existing app_oom line (once per run) for any such container.
|
|
|
|
// readGuestOOMKills returns container id → oom_kill for one guest. available=false (no error) when the guest has no
|
|
// cgroup folder (a VM guest, or a container that is not running).
|
|
func readGuestOOMKills(root string, vmid int) (map[string]int64, bool, error) {
|
|
if root == "" {
|
|
root = "/sys/fs/cgroup"
|
|
}
|
|
base := filepath.Join(root, "lxc", strconv.Itoa(vmid), "ns")
|
|
if fi, err := os.Stat(base); err != nil || !fi.IsDir() {
|
|
return nil, false, nil
|
|
}
|
|
out := map[string]int64{}
|
|
for _, pattern := range []string{
|
|
filepath.Join(base, "system.slice", "docker-*.scope", "memory.events"),
|
|
filepath.Join(base, "docker", "*", "memory.events"),
|
|
} {
|
|
files, err := filepath.Glob(pattern)
|
|
if err != nil {
|
|
return nil, true, err
|
|
}
|
|
for _, f := range files {
|
|
id := strings.TrimSuffix(strings.TrimPrefix(filepath.Base(filepath.Dir(f)), "docker-"), ".scope")
|
|
if !isContainerID(id) {
|
|
continue
|
|
}
|
|
n, err := readOOMKill(f)
|
|
if err != nil {
|
|
continue // a container that stopped between the glob and the read
|
|
}
|
|
out[id] = n
|
|
}
|
|
}
|
|
return out, true, nil
|
|
}
|
|
|
|
func isContainerID(s string) bool {
|
|
if len(s) != 64 {
|
|
return false
|
|
}
|
|
for _, c := range s {
|
|
if !(c >= '0' && c <= '9' || c >= 'a' && c <= 'f') {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func readOOMKill(path string) (int64, error) {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
defer f.Close()
|
|
sc := bufio.NewScanner(f)
|
|
for sc.Scan() {
|
|
k, v, ok := strings.Cut(strings.TrimSpace(sc.Text()), " ")
|
|
if ok && k == "oom_kill" {
|
|
return strconv.ParseInt(strings.TrimSpace(v), 10, 64)
|
|
}
|
|
}
|
|
if err := sc.Err(); err != nil {
|
|
return 0, err
|
|
}
|
|
return 0, fmt.Errorf("no oom_kill line in %s", path)
|
|
}
|
|
|
|
// handleGuestOOMKills is GET /guest/oom-kills: {"vmid", "available", "containers": {id: oom_kill}}.
|
|
func (s *Server) handleGuestOOMKills(w http.ResponseWriter, r *http.Request, vmid int) {
|
|
kills, avail, err := readGuestOOMKills(s.cgroupRoot, vmid)
|
|
if err != nil {
|
|
s.logger.Warn("local-api: guest oom-kills read failed (R-528)", "vmid", vmid, "err", err)
|
|
writeErr(w, http.StatusInternalServerError, "the guest's OOM counters could not be read")
|
|
return
|
|
}
|
|
if kills == nil {
|
|
kills = map[string]int64{}
|
|
}
|
|
writeOK(w, map[string]any{"vmid": vmid, "available": avail, "containers": kills})
|
|
}
|