From c1ea8e50ac3c03c4f4d0dea77d23bfdee0704e2c Mon Sep 17 00:00:00 2001 From: Akshay2191 Date: Tue, 30 Jun 2026 19:07:06 +0100 Subject: [PATCH] memoryscraper cleanup --- .../memoryscraper/internal/cgroup/memory.go | 15 +++- .../internal/cgroup/memory_test.go | 87 +++++++++++++++++++ 2 files changed, 99 insertions(+), 3 deletions(-) diff --git a/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/internal/cgroup/memory.go b/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/internal/cgroup/memory.go index d64a152a1..2aa637c1c 100644 --- a/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/internal/cgroup/memory.go +++ b/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/internal/cgroup/memory.go @@ -104,19 +104,19 @@ func (ms *MemorySource) VirtualMemoryStatWithContext(ctx context.Context) (*mem. var usedMemoryPercent float64 - usedMemory := memoryUsageInBytes - memoryStat.cached + usedMemory := saturatingSub(memoryUsageInBytes, memoryStat.cached) if memoryLimitInBytes > 0 { usedMemoryPercent = float64(100 * usedMemory / memoryLimitInBytes) } cgroupStat.Total = memoryLimitInBytes - cgroupStat.Available = memoryLimitInBytes - usedMemory + cgroupStat.Available = saturatingSub(memoryLimitInBytes, usedMemory) cgroupStat.Used = usedMemory cgroupStat.Cached = memoryStat.cached cgroupStat.Shared = memoryStat.shared cgroupStat.UsedPercent = usedMemoryPercent - cgroupStat.Free = memoryLimitInBytes - usedMemory + cgroupStat.Free = saturatingSub(memoryLimitInBytes, usedMemory) return &cgroupStat, nil } @@ -181,3 +181,12 @@ func V1DefaultMaxValue() string { maxInt := int64(math.MaxInt64) return strconv.FormatInt((maxInt/pageSize)*pageSize, 10) } + +// saturatingSub subtracts b from a, clamping to 0 on underflow. +func saturatingSub(a, b uint64) uint64 { + if a < b { + return 0 + } + + return a - b +} diff --git a/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/internal/cgroup/memory_test.go b/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/internal/cgroup/memory_test.go index 42cc9497b..02181329c 100644 --- a/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/internal/cgroup/memory_test.go +++ b/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/internal/cgroup/memory_test.go @@ -131,3 +131,90 @@ func TestVirtualMemoryStat(t *testing.T) { }) } } + +func TestSaturatingSub(t *testing.T) { + tests := []struct { + name string + a, b uint64 + expected uint64 + }{ + { + name: "normal subtraction", + a: 100, + b: 40, + expected: 60, + }, + { + name: "equal values return zero", + a: 100, + b: 100, + expected: 0, + }, + { + name: "b greater than a clamps to zero (cached exceeds usage)", + a: 100, + b: 200, + expected: 0, + }, + { + name: "b greater than a clamps to zero (used exceeds limit)", + a: 400, + b: 500, + expected: 0, + }, + { + name: "zero minus zero returns zero", + a: 0, + b: 0, + expected: 0, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.expected, saturatingSub(test.a, test.b)) + }) + } +} + +func TestVirtualMemoryStat_CachedExceedsUsage(t *testing.T) { + dir := t.TempDir() + + // cgroup.controllers marks basePath as v2 + require.NoError(t, os.WriteFile(path.Join(dir, "cgroup.controllers"), []byte(""), 0o600)) + require.NoError(t, os.WriteFile(path.Join(dir, "memory.max"), []byte("1000\n"), 0o600)) + require.NoError(t, os.WriteFile(path.Join(dir, "memory.current"), []byte("100\n"), 0o600)) + require.NoError(t, os.WriteFile(path.Join(dir, "memory.stat"), []byte("file 200\nshmem 0\n"), 0o600)) + + src := NewMemorySource(dir) + stat, err := src.VirtualMemoryStatWithContext(t.Context()) + require.NoError(t, err) + + // usedMemory = saturatingSub(100, 200) = 0 — must not wrap to 2^64 + assert.Equal(t, uint64(0), stat.Used, "Used must clamp to 0 when cached > usage") + // Free = saturatingSub(1000, 0) = 1000 — no secondary underflow + assert.Equal(t, uint64(1000), stat.Free, "Free must equal limit when usedMemory is 0") + assert.InDelta(t, float64(0), stat.UsedPercent, 0.001, "UsedPercent must be 0") +} + +func TestVirtualMemoryStat_UsedExceedsLimit(t *testing.T) { + dir := t.TempDir() + + // cgroup.controllers marks basePath as v2 + require.NoError(t, os.WriteFile(path.Join(dir, "cgroup.controllers"), []byte(""), 0o600)) + // usage (500) > limit (400) — OOM/transient cgroup state + require.NoError(t, os.WriteFile(path.Join(dir, "memory.max"), []byte("400\n"), 0o600)) + require.NoError(t, os.WriteFile(path.Join(dir, "memory.current"), []byte("500\n"), 0o600)) + require.NoError(t, os.WriteFile(path.Join(dir, "memory.stat"), []byte("file 0\nshmem 0\n"), 0o600)) + + src := NewMemorySource(dir) + stat, err := src.VirtualMemoryStatWithContext(t.Context()) + require.NoError(t, err) + + // usedMemory = saturatingSub(500, 0) = 500 — no underflow + assert.Equal(t, uint64(500), stat.Used, "Used = usage - cached = 500") + // Free = saturatingSub(400, 500) = 0 — must not wrap to 2^64 + assert.Equal(t, uint64(0), stat.Free, "Free must clamp to 0 when used > limit") + // Available = saturatingSub(400, 500) = 0 — same expression + assert.Equal(t, uint64(0), stat.Available, "Available must clamp to 0 when used > limit") +}