Files
pulse/pkg/server/memlimit_test.go
pulse-triage[bot] ee6bb64d72 Honor cgroup v1 service memory limits
Resolve the process memory-controller hierarchy before selecting the tightest v1 hard limit, so systemd and nested appliance limits actually inform the existing Go runtime headroom.

Contract-Neutral: runtime memory-limit detection only; no API or persistence contract change
2026-08-30 02:15:36 +01:00

215 lines
7.2 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 TestReadCgroupV2MemoryLimitAtNamespacedRoot(t *testing.T) {
root := t.TempDir()
proc := filepath.Join(root, "proc-self-cgroup")
writeCgroupFixture(t, proc, "0::/\n")
writeCgroupFixture(t, filepath.Join(root, "memory.max"), "536870912\n")
got, ok := readCgroupV2MemoryLimit(root, proc)
if !ok || got != 536870912 {
t.Fatalf("got (%d, %v), want (536870912, true)", got, ok)
}
}
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 TestCgroupV1MemoryPathFrom(t *testing.T) {
cases := []struct {
name string
content string
want string
}{
{"memory controller", "5:memory:/system.slice/pulse.service\n", "system.slice/pulse.service"},
{"combined controllers", "7:cpu,cpuacct,memory:/docker/abc\n", "docker/abc"},
{"namespaced root", "5:memory:/\n", "."},
{"colon in path", "5:memory:/system.slice/pulse:worker.service\n", "system.slice/pulse:worker.service"},
{"other controllers only", "2:cpu,cpuacct:/system.slice/pulse.service\n", ""},
{"v2 only", "0::/system.slice/pulse.service\n", ""},
{"malformed", "memory:/system.slice/pulse.service\n", ""},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := cgroupV1MemoryPathFrom(tc.content); got != tc.want {
t.Fatalf("cgroupV1MemoryPathFrom(%q) = %q, want %q", tc.content, got, tc.want)
}
})
}
}
func TestReadCgroupV1HierarchyMemoryLimitAtNamespacedRoot(t *testing.T) {
root := t.TempDir()
proc := filepath.Join(root, "proc-self-cgroup")
writeCgroupFixture(t, proc, "5:memory:/\n")
writeCgroupFixture(t, filepath.Join(root, "memory.limit_in_bytes"), "536870912\n")
got, ok := readCgroupV1HierarchyMemoryLimit(root, proc)
if !ok || got != 536870912 {
t.Fatalf("got (%d, %v), want (536870912, true)", got, ok)
}
}
func TestReadCgroupV1HierarchyMemoryLimitWalksAncestors(t *testing.T) {
root := t.TempDir()
proc := filepath.Join(root, "proc-self-cgroup")
writeCgroupFixture(t, proc, "5:memory:/system.slice/pulse.service\n")
// The service limit must win over the unlimited controller root. This is
// the layout used by systemd MemoryLimit/MemoryMax on cgroup v1.
writeCgroupFixture(t, filepath.Join(root, "system.slice/pulse.service/memory.limit_in_bytes"), "536870912\n")
writeCgroupFixture(t, filepath.Join(root, "system.slice/memory.limit_in_bytes"), "1073741824\n")
writeCgroupFixture(t, filepath.Join(root, "memory.limit_in_bytes"), strconv.FormatInt(cgroupV1NoLimit, 10)+"\n")
got, ok := readCgroupV1HierarchyMemoryLimit(root, proc)
if !ok || got != 536870912 {
t.Fatalf("got (%d, %v), want (536870912, true)", got, ok)
}
}
func TestReadCgroupV1HierarchyMemoryLimitTakesTightestAncestor(t *testing.T) {
root := t.TempDir()
proc := filepath.Join(root, "proc-self-cgroup")
writeCgroupFixture(t, proc, "8:cpu,memory:/parent/child\n")
writeCgroupFixture(t, filepath.Join(root, "parent/child/memory.limit_in_bytes"), "2147483648\n")
writeCgroupFixture(t, filepath.Join(root, "parent/memory.limit_in_bytes"), "805306368\n")
got, ok := readCgroupV1HierarchyMemoryLimit(root, proc)
if !ok || got != 805306368 {
t.Fatalf("got (%d, %v), want (805306368, true)", got, ok)
}
}
func TestReadCgroupV1HierarchyMemoryLimitNoMemoryController(t *testing.T) {
root := t.TempDir()
proc := filepath.Join(root, "proc-self-cgroup")
writeCgroupFixture(t, proc, "2:cpu,cpuacct:/system.slice/pulse.service\n")
if got, ok := readCgroupV1HierarchyMemoryLimit(root, proc); ok {
t.Fatalf("expected no memory 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")
}
}