mirror of
https://github.com/mudler/LocalAI.git
synced 2026-06-14 11:49:33 -04:00
fix(xsysinfo): make reported system RAM total cgroup/lxcfs-aware (#8059) GetSystemRAMInfo derived Total from memory.TotalMemory(), which on Linux uses syscall.Sysinfo().Totalram - the HOST kernel total. lxcfs/LXD does NOT virtualize that value, while MemAvailable (used for Free/Available) IS virtualized. Inside an LXD/container with a 128Gi host but a ~10Gi container view this produced Total=128Gi, Available=10Gi => Used=118Gi, reporting ~92% RAM usage on an idle container. Derive Total instead from the minimum of all non-zero, non-unlimited candidates: cgroup v2 memory.max, cgroup v1 memory.limit_in_bytes (the kernel unlimited sentinel is ignored), /proc/meminfo MemTotal (which lxcfs virtualizes), and the syscall.Sysinfo total as the bare-metal fallback. On bare metal every candidate is unlimited or equals the host total, so behavior is unchanged. The selection/parsing lives in a pure function chooseTotalMemory(...) taking file CONTENTS, unit-tested without a real LXD host; OS file reads stay in a thin wrapper. Assisted-by: claude:claude-opus-4-8 [Claude Code] Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
75 lines
2.1 KiB
Go
75 lines
2.1 KiB
Go
package xsysinfo
|
|
|
|
import (
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
)
|
|
|
|
var _ = Describe("chooseTotalMemory", func() {
|
|
const (
|
|
gi128 = uint64(128) * 1024 * 1024 * 1024
|
|
gi20 = uint64(20) * 1024 * 1024 * 1024
|
|
gi10 = uint64(10) * 1024 * 1024 * 1024
|
|
)
|
|
|
|
// /proc/meminfo MemTotal is in kB; build a snippet for a given byte total.
|
|
memInfo := func(bytes uint64) string {
|
|
kb := bytes / 1024
|
|
return "MemTotal: " + itoa(kb) + " kB\nMemFree: 123 kB\n"
|
|
}
|
|
|
|
Context("bare metal (no cgroup cap, memory.max == max)", func() {
|
|
It("uses the host sysinfo total", func() {
|
|
// MemTotal mirrors sysinfo on bare metal.
|
|
got := chooseTotalMemory("max\n", string(rune(0)), memInfo(gi128), gi128)
|
|
Expect(got).To(Equal(gi128))
|
|
})
|
|
})
|
|
|
|
Context("LXD/lxcfs container (MemTotal virtualized below host, no cap)", func() {
|
|
It("uses the virtualized MemTotal, not the host sysinfo total", func() {
|
|
// This is issue #8059: host sysinfo says 128Gi, but lxcfs
|
|
// virtualizes /proc/meminfo MemTotal to 20Gi and there is no
|
|
// cgroup cap. The corrected total must be 20Gi.
|
|
got := chooseTotalMemory("max\n", "", memInfo(gi20), gi128)
|
|
Expect(got).To(Equal(gi20))
|
|
})
|
|
})
|
|
|
|
Context("cgroup v2 cap set below MemTotal", func() {
|
|
It("uses the cgroup cap", func() {
|
|
got := chooseTotalMemory(itoa(gi10)+"\n", "", memInfo(gi20), gi128)
|
|
Expect(got).To(Equal(gi10))
|
|
})
|
|
})
|
|
|
|
Context("cgroup v1 with the kernel unlimited sentinel", func() {
|
|
It("ignores the sentinel and falls back to MemTotal", func() {
|
|
got := chooseTotalMemory("", "9223372036854771712\n", memInfo(gi20), gi128)
|
|
Expect(got).To(Equal(gi20))
|
|
})
|
|
})
|
|
|
|
Context("all candidates empty/unlimited", func() {
|
|
It("falls back to sysinfo total", func() {
|
|
got := chooseTotalMemory("max\n", "", "", gi128)
|
|
Expect(got).To(Equal(gi128))
|
|
})
|
|
})
|
|
})
|
|
|
|
// itoa is a tiny base-10 formatter to avoid importing strconv into the test.
|
|
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:])
|
|
}
|