mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-23 19:57:09 +00:00
fed7179c92
Pulse under a systemd MemoryMax, docker --memory, or Kubernetes limit ran with a GC that was blind to the cap: GOGC=100 lets total heap float to roughly twice the live set, so a capped process grows until the kernel OOM-kills it instead of collecting harder near the boundary. The demo droplet's kill history (three OOM kills in July under an 800M cap) is this mechanism, and any containerized install with a memory limit is exposed the same way. At startup, when GOMEMLIMIT is not set by the operator, resolve the process cgroup (v2 ancestor walk taking the smallest memory.max on the path, v1 limit_in_bytes fallback) and set the runtime soft limit to 90% of it, leaving headroom for stacks, mmap, and CGO. Best effort: no detectable limit leaves GC defaults untouched. Contract-Neutral: runtime GC limit alignment: no payload or contract delta
128 lines
3.8 KiB
Go
128 lines
3.8 KiB
Go
package server
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"testing"
|
|
)
|
|
|
|
func TestParseCgroupMemoryValue(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
raw string
|
|
want int64
|
|
ok bool
|
|
}{
|
|
{"plain number", "838860800\n", 838860800, true},
|
|
{"max marker", "max\n", 0, false},
|
|
{"empty", "", 0, false},
|
|
{"whitespace", " \n", 0, false},
|
|
{"garbage", "not-a-number", 0, false},
|
|
{"zero", "0", 0, false},
|
|
{"negative", "-5", 0, false},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got, ok := parseCgroupMemoryValue(tc.raw)
|
|
if ok != tc.ok || got != tc.want {
|
|
t.Fatalf("parseCgroupMemoryValue(%q) = (%d, %v), want (%d, %v)", tc.raw, got, ok, tc.want, tc.ok)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCgroupV2PathFrom(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
content string
|
|
want string
|
|
}{
|
|
{"systemd service", "0::/system.slice/pulse.service\n", "system.slice/pulse.service"},
|
|
{"container root", "0::/\n", ""},
|
|
{"hybrid picks unified line", "12:memory:/legacy\n0::/system.slice/pulse.service\n", "system.slice/pulse.service"},
|
|
{"v1 only", "12:memory:/legacy\n", ""},
|
|
{"empty", "", ""},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if got := cgroupV2PathFrom(tc.content); got != tc.want {
|
|
t.Fatalf("cgroupV2PathFrom(%q) = %q, want %q", tc.content, got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func writeCgroupFixture(t *testing.T, path, content string) {
|
|
t.Helper()
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestReadCgroupV2MemoryLimitWalksAncestors(t *testing.T) {
|
|
root := t.TempDir()
|
|
proc := filepath.Join(root, "proc-self-cgroup")
|
|
writeCgroupFixture(t, proc, "0::/system.slice/pulse.service\n")
|
|
|
|
// Limit set on the service, "max" at the root: service limit wins.
|
|
writeCgroupFixture(t, filepath.Join(root, "system.slice/pulse.service/memory.max"), "838860800\n")
|
|
writeCgroupFixture(t, filepath.Join(root, "system.slice/memory.max"), "max\n")
|
|
writeCgroupFixture(t, filepath.Join(root, "memory.max"), "max\n")
|
|
|
|
got, ok := readCgroupV2MemoryLimit(root, proc)
|
|
if !ok || got != 838860800 {
|
|
t.Fatalf("got (%d, %v), want (838860800, true)", got, ok)
|
|
}
|
|
}
|
|
|
|
func TestReadCgroupV2MemoryLimitTakesSmallestOnPath(t *testing.T) {
|
|
root := t.TempDir()
|
|
proc := filepath.Join(root, "proc-self-cgroup")
|
|
writeCgroupFixture(t, proc, "0::/a/b\n")
|
|
|
|
// Ancestor holds the tighter limit.
|
|
writeCgroupFixture(t, filepath.Join(root, "a/b/memory.max"), "2147483648\n")
|
|
writeCgroupFixture(t, filepath.Join(root, "a/memory.max"), "1073741824\n")
|
|
|
|
got, ok := readCgroupV2MemoryLimit(root, proc)
|
|
if !ok || got != 1073741824 {
|
|
t.Fatalf("got (%d, %v), want (1073741824, true)", got, ok)
|
|
}
|
|
}
|
|
|
|
func TestReadCgroupV2MemoryLimitNoLimitAnywhere(t *testing.T) {
|
|
root := t.TempDir()
|
|
proc := filepath.Join(root, "proc-self-cgroup")
|
|
writeCgroupFixture(t, proc, "0::/a\n")
|
|
writeCgroupFixture(t, filepath.Join(root, "a/memory.max"), "max\n")
|
|
writeCgroupFixture(t, filepath.Join(root, "memory.max"), "max\n")
|
|
|
|
if got, ok := readCgroupV2MemoryLimit(root, proc); ok {
|
|
t.Fatalf("expected no limit, got %d", got)
|
|
}
|
|
}
|
|
|
|
func TestReadCgroupV1MemoryLimit(t *testing.T) {
|
|
dir := t.TempDir()
|
|
limitFile := filepath.Join(dir, "memory.limit_in_bytes")
|
|
|
|
writeCgroupFixture(t, limitFile, "536870912\n")
|
|
if got, ok := readCgroupV1MemoryLimit(limitFile); !ok || got != 536870912 {
|
|
t.Fatalf("got (%d, %v), want (536870912, true)", got, ok)
|
|
}
|
|
|
|
// The v1 no-limit sentinel reports no limit.
|
|
writeCgroupFixture(t, limitFile, strconv.FormatInt(cgroupV1NoLimit, 10)+"\n")
|
|
if got, ok := readCgroupV1MemoryLimit(limitFile); ok {
|
|
t.Fatalf("expected sentinel to mean no limit, got %d", got)
|
|
}
|
|
|
|
if _, ok := readCgroupV1MemoryLimit(filepath.Join(dir, "missing")); ok {
|
|
t.Fatal("expected missing file to mean no limit")
|
|
}
|
|
}
|