Skip to main content

hyperi_rustlib/memory/
cgroup.rs

1// Project:   hyperi-rustlib
2// File:      src/memory/cgroup.rs
3// Purpose:   Cgroup-aware memory limit detection
4// Language:  Rust
5//
6// License:   BUSL-1.1
7// Copyright: (c) 2026 HYPERI PTY LIMITED
8
9//! Cgroup-aware memory limit detection.
10
11use std::fs;
12
13/// Detect the memory limit for this process.
14///
15/// Priority:
16/// 1. Cgroup v2: `/sys/fs/cgroup/memory.max`
17/// 2. Cgroup v1: `/sys/fs/cgroup/memory/memory.limit_in_bytes`
18/// 3. System available memory (via sysinfo)
19///
20/// Returns the limit in bytes.
21pub fn detect_memory_limit() -> u64 {
22    // Try cgroup v2
23    if let Some(limit) = read_cgroup_v2_limit() {
24        tracing::info!(
25            limit_bytes = limit,
26            source = "cgroup-v2",
27            "detected memory limit"
28        );
29        return limit;
30    }
31
32    // Try cgroup v1
33    if let Some(limit) = read_cgroup_v1_limit() {
34        tracing::info!(
35            limit_bytes = limit,
36            source = "cgroup-v1",
37            "detected memory limit"
38        );
39        return limit;
40    }
41
42    // Fallback to system memory
43    let mut sys = sysinfo::System::new();
44    sys.refresh_memory();
45    let total = sys.total_memory();
46    tracing::info!(
47        limit_bytes = total,
48        source = "system-memory",
49        "detected memory limit (no cgroup)"
50    );
51    total
52}
53
54/// Detect THIS container's own memory pressure as `current / limit`.
55///
56/// Reads the cgroup's `memory.current` (v2) / `memory.usage_in_bytes` (v1)
57/// against its `memory.max` / `memory.limit_in_bytes`. Returns `None` when no
58/// cgroup memory limit is in force (bare metal, or `memory.max == max`), in
59/// which case callers should fall back to a process/host signal.
60///
61/// This is the signal a container scheduler (K8s/cgroup OOM killer) actually
62/// acts on -- unlike host-wide `used/total` memory, which on a large shared
63/// host is unrelated to this container's limit.
64#[must_use]
65pub fn detect_memory_pressure() -> Option<f64> {
66    let limit = read_cgroup_v2_limit().or_else(read_cgroup_v1_limit)?;
67    if limit == 0 {
68        return None;
69    }
70    let current = read_cgroup_v2_current().or_else(read_cgroup_v1_current)?;
71    Some(current as f64 / limit as f64)
72}
73
74fn read_cgroup_v2_limit() -> Option<u64> {
75    let content = fs::read_to_string("/sys/fs/cgroup/memory.max").ok()?;
76    let trimmed = content.trim();
77    if trimmed == "max" {
78        return None; // No limit set
79    }
80    trimmed.parse::<u64>().ok()
81}
82
83fn read_cgroup_v2_current() -> Option<u64> {
84    fs::read_to_string("/sys/fs/cgroup/memory.current")
85        .ok()?
86        .trim()
87        .parse::<u64>()
88        .ok()
89}
90
91fn read_cgroup_v1_current() -> Option<u64> {
92    fs::read_to_string("/sys/fs/cgroup/memory/memory.usage_in_bytes")
93        .ok()?
94        .trim()
95        .parse::<u64>()
96        .ok()
97}
98
99fn read_cgroup_v1_limit() -> Option<u64> {
100    let content = fs::read_to_string("/sys/fs/cgroup/memory/memory.limit_in_bytes").ok()?;
101    let value = content.trim().parse::<u64>().ok()?;
102    // cgroup v1 uses a very large number for "no limit"
103    if value > 1 << 62 {
104        return None;
105    }
106    Some(value)
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[test]
114    fn test_detect_memory_limit_returns_nonzero() {
115        let limit = detect_memory_limit();
116        assert!(limit > 0, "memory limit should be positive");
117    }
118
119    #[test]
120    fn test_detect_memory_pressure_is_none_or_valid_fraction() {
121        // Either no cgroup limit is in force (None) or a finite, non-negative
122        // ratio. We can't assert an exact value -- it depends on the host.
123        match detect_memory_pressure() {
124            None => {}
125            Some(r) => assert!(
126                r.is_finite() && r >= 0.0,
127                "cgroup pressure must be finite and non-negative, got {r}"
128            ),
129        }
130    }
131}