F1: read cgroup memory limit, not host RAM (restores deploy OOM guard)
/api/system/info reported the Proxmox host's 16GB (the controller container reads host /proc/meminfo with no lxcfs), defeating the deploy memory-headroom hard-block (deploy.go uses GetMemoryMB). readMemInfo now prefers the cgroup memory limit (v2 memory.max / v1 memory.limit_in_bytes; sentinels = unlimited) when finite and below the host total; used = memory.current/usage_in_bytes. Test info_cgroup_test.go (cgroup v2 cap wins, v2 max sentinel, v1 unlimited, v1 finite) — fails on pre-fix code.
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
//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()
|
||||
// 2 GiB limit, 512 MiB current usage.
|
||||
const twoGiB = uint64(2 * 1024 * 1024 * 1024)
|
||||
const halfGiB = uint64(512 * 1024 * 1024)
|
||||
mustWriteFile(t, filepath.Join(dir, "memory.max"), []byte(itoa(twoGiB)))
|
||||
mustWriteFile(t, filepath.Join(dir, "memory.current"), []byte(itoa(halfGiB)))
|
||||
|
||||
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)
|
||||
}
|
||||
if info.UsedMemMB != 512 {
|
||||
t.Fatalf("UsedMemMB = %d, want 512 (memory.current)", info.UsedMemMB)
|
||||
}
|
||||
if info.AvailMemMB != 1536 {
|
||||
t.Fatalf("AvailMemMB = %d, want 1536", info.AvailMemMB)
|
||||
}
|
||||
if info.MemPercent < 24 || info.MemPercent > 26 {
|
||||
t.Fatalf("MemPercent = %.1f, want ~25", info.MemPercent)
|
||||
}
|
||||
}
|
||||
|
||||
// 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:])
|
||||
}
|
||||
Reference in New Issue
Block a user