4989513a96
The cgroup-only approach was a no-op on the demo: the controller container's OWN cgroup is unlimited (the 2GB cap is on the LXC ancestor, hidden), and /proc has no lxcfs, so it kept reporting the host's 16GB. The Docker daemon runs IN the LXC, so 'docker info' MemTotal reports the guest's real cap (2048MB) — now the authoritative source (cgroup limit preferred when present, e.g. non-nested). The deploy memory guard now uses the controller's committed-app memory (sum of running mem requests) for 'used' — accurate and cheap — instead of host /proc RSS (unobservable per guest; would make the guard never/always fire). /api/system/info reports the guest cap as total and committed memory as used. Tests: cgroup-limit path, docker-info fallback (nested case), GuestMemTotalMB fallback (dockerMemTotalFn stub).
144 lines
4.6 KiB
Go
144 lines
4.6 KiB
Go
//go:build linux
|
|
|
|
package system
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
// TestReadMemInfoUsesCgroupV2Limit asserts F1: when a cgroup v2 memory.max caps the container well
|
|
// below the host /proc/meminfo total, readMemInfo reports the cgroup cap (the guest's real ceiling),
|
|
// not the host RAM. On the pre-fix code this test fails because readMemInfo ignored cgroup entirely.
|
|
func TestReadMemInfoUsesCgroupV2Limit(t *testing.T) {
|
|
dir := t.TempDir()
|
|
const twoGiB = uint64(2 * 1024 * 1024 * 1024)
|
|
mustWriteFile(t, filepath.Join(dir, "memory.max"), []byte(itoa(twoGiB)))
|
|
|
|
old := cgroupRoot
|
|
cgroupRoot = dir
|
|
defer func() { cgroupRoot = old }()
|
|
|
|
var info SystemInfo
|
|
readMemInfo(&info)
|
|
|
|
// Host /proc/meminfo total is whatever the test machine has; the cgroup cap must win when smaller.
|
|
if info.TotalMemMB != 2048 {
|
|
t.Fatalf("TotalMemMB = %d, want 2048 (cgroup cap), not host RAM", info.TotalMemMB)
|
|
}
|
|
// Used is a scaled estimate (the container cannot read guest-wide RSS); just assert it is sane.
|
|
if info.UsedMemMB > info.TotalMemMB {
|
|
t.Fatalf("UsedMemMB = %d exceeds TotalMemMB = %d", info.UsedMemMB, info.TotalMemMB)
|
|
}
|
|
if info.AvailMemMB != info.TotalMemMB-info.UsedMemMB {
|
|
t.Fatalf("AvailMemMB inconsistent: %d != %d-%d", info.AvailMemMB, info.TotalMemMB, info.UsedMemMB)
|
|
}
|
|
}
|
|
|
|
// TestReadMemInfoUsesDockerInfoWhenCgroupUnlimited asserts the NESTED-LXC case (the real demo): the
|
|
// container's own cgroup is unlimited ("max"), so the guest cap must come from `docker info` MemTotal.
|
|
// Pre-fix (and the cgroup-only attempt) reports the host RAM here.
|
|
func TestReadMemInfoUsesDockerInfoWhenCgroupUnlimited(t *testing.T) {
|
|
dir := t.TempDir()
|
|
mustWriteFile(t, filepath.Join(dir, "memory.max"), []byte("max")) // container cgroup unlimited
|
|
old := cgroupRoot
|
|
cgroupRoot = dir
|
|
defer func() { cgroupRoot = old }()
|
|
oldFn := dockerMemTotalFn
|
|
dockerMemTotalFn = func() (uint64, bool) { return 2048, true }
|
|
defer func() { dockerMemTotalFn = oldFn }()
|
|
|
|
var info SystemInfo
|
|
readMemInfo(&info)
|
|
if info.TotalMemMB != 2048 {
|
|
t.Fatalf("TotalMemMB = %d, want 2048 (docker-info guest cap)", info.TotalMemMB)
|
|
}
|
|
}
|
|
|
|
// TestGuestMemTotalMB_DockerInfoFallback asserts GuestMemTotalMB (used by the deploy guard) falls back
|
|
// to docker-info when no cgroup limit is present.
|
|
func TestGuestMemTotalMB_DockerInfoFallback(t *testing.T) {
|
|
dir := t.TempDir() // no cgroup files → no limit
|
|
old := cgroupRoot
|
|
cgroupRoot = dir
|
|
defer func() { cgroupRoot = old }()
|
|
oldFn := dockerMemTotalFn
|
|
dockerMemTotalFn = func() (uint64, bool) { return 2048, true }
|
|
defer func() { dockerMemTotalFn = oldFn }()
|
|
|
|
if v, ok := GuestMemTotalMB(); !ok || v != 2048 {
|
|
t.Fatalf("GuestMemTotalMB = (%d, %v), want (2048, true)", v, ok)
|
|
}
|
|
}
|
|
|
|
// TestReadMemInfoCgroupMaxIsUnlimited asserts that a v2 "max" sentinel does NOT override /proc/meminfo
|
|
// (an uncapped container keeps the host view rather than a bogus 0).
|
|
func TestReadMemInfoCgroupMaxIsUnlimited(t *testing.T) {
|
|
dir := t.TempDir()
|
|
mustWriteFile(t, filepath.Join(dir, "memory.max"), []byte("max"))
|
|
|
|
old := cgroupRoot
|
|
cgroupRoot = dir
|
|
defer func() { cgroupRoot = old }()
|
|
|
|
var info SystemInfo
|
|
readMemInfo(&info)
|
|
|
|
if info.TotalMemMB == 0 {
|
|
t.Fatalf("TotalMemMB = 0 with an unlimited cgroup; expected the /proc/meminfo host total")
|
|
}
|
|
}
|
|
|
|
// TestReadCgroupMemLimitV1Unlimited asserts the v1 near-uint64-max sentinel is treated as unlimited.
|
|
func TestReadCgroupMemLimitV1Unlimited(t *testing.T) {
|
|
dir := t.TempDir()
|
|
memDir := filepath.Join(dir, "memory")
|
|
if err := os.MkdirAll(memDir, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Typical v1 "unlimited" value.
|
|
mustWriteFile(t, filepath.Join(memDir, "memory.limit_in_bytes"), []byte("9223372036854771712"))
|
|
|
|
if _, ok := readCgroupMemLimitMB(dir); ok {
|
|
t.Fatalf("readCgroupMemLimitMB treated the v1 unlimited sentinel as a real limit")
|
|
}
|
|
}
|
|
|
|
// TestReadCgroupMemLimitV1Real asserts a finite v1 limit is read.
|
|
func TestReadCgroupMemLimitV1Real(t *testing.T) {
|
|
dir := t.TempDir()
|
|
memDir := filepath.Join(dir, "memory")
|
|
if err := os.MkdirAll(memDir, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
const oneGiB = uint64(1024 * 1024 * 1024)
|
|
mustWriteFile(t, filepath.Join(memDir, "memory.limit_in_bytes"), []byte(itoa(oneGiB)))
|
|
|
|
mb, ok := readCgroupMemLimitMB(dir)
|
|
if !ok || mb != 1024 {
|
|
t.Fatalf("readCgroupMemLimitMB = (%d, %v), want (1024, true)", mb, ok)
|
|
}
|
|
}
|
|
|
|
func mustWriteFile(t *testing.T, path string, data []byte) {
|
|
t.Helper()
|
|
if err := os.WriteFile(path, data, 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func itoa(v uint64) string {
|
|
if v == 0 {
|
|
return "0"
|
|
}
|
|
var buf [20]byte
|
|
i := len(buf)
|
|
for v > 0 {
|
|
i--
|
|
buf[i] = byte('0' + v%10)
|
|
v /= 10
|
|
}
|
|
return string(buf[i:])
|
|
}
|