F1 (rework): source guest RAM cap from docker info; deploy guard uses committed memory

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).
This commit is contained in:
2026-06-14 10:18:22 +02:00
parent d6c428b5dd
commit 4989513a96
5 changed files with 142 additions and 23 deletions
+40 -9
View File
@@ -13,11 +13,8 @@ import (
// 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
@@ -30,14 +27,48 @@ func TestReadMemInfoUsesCgroupV2Limit(t *testing.T) {
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)
// 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 != 1536 {
t.Fatalf("AvailMemMB = %d, want 1536", info.AvailMemMB)
if info.AvailMemMB != info.TotalMemMB-info.UsedMemMB {
t.Fatalf("AvailMemMB inconsistent: %d != %d-%d", info.AvailMemMB, info.TotalMemMB, info.UsedMemMB)
}
if info.MemPercent < 24 || info.MemPercent > 26 {
t.Fatalf("MemPercent = %.1f, want ~25", info.MemPercent)
}
// 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)
}
}