mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-11 14:00:29 +00:00
ee6bb64d72
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
215 lines
7.2 KiB
Go
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")
|
|
}
|
|
}
|