ifc_lite_server/
mem_policy.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! Memory-budget policy: the readers and the resolver that decide the
6//! admission byte budget at startup.
7//!
8//! The byte-weighted admission semaphore (see [`crate::admission`]) is the
9//! load-bearing bound against OOM from concurrent large uploads, but it only
10//! engages when the budget is non-zero. Previously the budget was derived from
11//! the cgroup limit alone, so a bare VM / non-containerized deployment (no
12//! readable cgroup ceiling) resolved to `0` and silently degraded to a bare
13//! CPU-concurrency count - the exact failure the admission work set out to
14//! fix. This module adds a physical-RAM fallback so the memory gate stays
15//! active without a cgroup, and keeps the three ceilings + the 70% math in one
16//! place the resolver tests exercise.
17//!
18//! No `sysinfo`/libc dependency: the readers parse `/proc` directly, matching
19//! [`crate::admission::resident_bytes_now`]; the release matrix includes a musl
20//! target where a heavy cross-platform crate is a known build risk.
21
22/// Fraction of the tightest readable memory ceiling to admit, leaving headroom
23/// for the allocator, the OS page cache, and non-parse allocations.
24const BUDGET_PCT_OF_CEILING: u64 = 70;
25
26/// Container memory limit from cgroups (v2 then v1), if readable.
27///
28/// `None` when unlimited or unreadable (non-Linux, or a bare host outside a
29/// cgroup). Moved here from `admission` so all memory-ceiling readers live
30/// beside the resolver.
31pub fn cgroup_memory_limit_bytes() -> Option<u64> {
32    for path in [
33        "/sys/fs/cgroup/memory.max",
34        "/sys/fs/cgroup/memory/memory.limit_in_bytes",
35    ] {
36        if let Ok(raw) = std::fs::read_to_string(path) {
37            match parse_cgroup_limit(&raw) {
38                CgroupLimit::Unlimited => return None,
39                CgroupLimit::Bytes(v) => return Some(v),
40                CgroupLimit::Invalid => {} // unreadable value: try the next path
41            }
42        }
43    }
44    None
45}
46
47/// Parsed outcome of one cgroup memory-limit file. `Unlimited` (`max`, or a
48/// v1 huge sentinel) stops the search - the ceiling is genuinely uncapped, so we
49/// must NOT fall back to another file. `Invalid` lets the caller try the next.
50#[derive(Debug, PartialEq, Eq)]
51enum CgroupLimit {
52    Unlimited,
53    Bytes(u64),
54    Invalid,
55}
56
57fn parse_cgroup_limit(raw: &str) -> CgroupLimit {
58    let raw = raw.trim();
59    if raw == "max" {
60        return CgroupLimit::Unlimited;
61    }
62    match raw.parse::<u64>() {
63        // cgroup v1 reports a huge sentinel (~i64::MAX) when unlimited.
64        Ok(v) if v >= (1 << 60) => CgroupLimit::Unlimited,
65        Ok(v) if v > 0 => CgroupLimit::Bytes(v),
66        _ => CgroupLimit::Invalid,
67    }
68}
69
70/// Total physical RAM in bytes from `/proc/meminfo` `MemTotal`, if readable.
71///
72/// `None` on non-Linux and on any parse failure (never panics). This is the
73/// fallback ceiling when no cgroup limit is set, so a bare Linux VM still gets
74/// a bounded memory gate instead of an unbounded one. NOTE: `MemTotal` is
75/// HOST-wide - in a container with no cgroup limit this over-states the
76/// container-usable RAM (see OPERATIONS.md).
77pub fn total_physical_memory_bytes() -> Option<u64> {
78    #[cfg(target_os = "linux")]
79    {
80        Some(parse_meminfo_memtotal_kb(&std::fs::read_to_string("/proc/meminfo").ok()?)? * 1024)
81    }
82    #[cfg(not(target_os = "linux"))]
83    {
84        None
85    }
86}
87
88/// Extract `MemTotal` (in kB) from `/proc/meminfo` content, or `None`. Pure so
89/// the parse - the only place a wrong line/unit would size every bare-VM budget
90/// wrong - is unit-tested without a live `/proc`.
91// Only called from the Linux branch of `total_physical_memory_bytes`; the unit
92// test exercises it on every target, but the non-Linux *binary* never calls it.
93#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
94fn parse_meminfo_memtotal_kb(meminfo: &str) -> Option<u64> {
95    // `MemTotal:      16305200 kB` - must match MemTotal, not MemFree/MemAvailable.
96    meminfo
97        .lines()
98        .find_map(|line| line.strip_prefix("MemTotal:"))?
99        .split_whitespace()
100        .next()?
101        .parse::<u64>()
102        .ok()
103}
104
105/// Resolve the admission memory budget (MB) from the three inputs, in one place
106/// the tests exercise so it can never silently regress to 0-when-a-ceiling-is-
107/// readable.
108///
109/// Precedence:
110/// 1. An explicit `IFC_MEM_BUDGET_MB` wins outright. `Some(0)` is honored as a
111///    deliberate opt-out (memory gate off) - it is NOT treated as "unset".
112/// 2. Otherwise, 70% of the tightest readable ceiling: `min(cgroup, physical)`.
113///    Using the tighter of the two keeps a small cgroup from being overridden
114///    by a large host, and lets physical RAM bound a bare VM with no cgroup.
115/// 3. Otherwise `0` (gate off) - only when neither ceiling is readable
116///    (non-Linux dev, or `/proc` unavailable), which the caller logs loudly.
117pub fn resolve_mem_budget_mb(
118    explicit_mb: Option<usize>,
119    cgroup_bytes: Option<u64>,
120    physical_bytes: Option<u64>,
121) -> usize {
122    if let Some(mb) = explicit_mb {
123        return mb;
124    }
125    match [cgroup_bytes, physical_bytes].into_iter().flatten().min() {
126        Some(ceiling) => (ceiling / (1024 * 1024) * BUDGET_PCT_OF_CEILING / 100) as usize,
127        None => 0,
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134
135    const GB: u64 = 1024 * 1024 * 1024;
136
137    #[test]
138    fn parse_cgroup_limit_handles_max_sentinel_and_values() {
139        assert_eq!(parse_cgroup_limit("max\n"), CgroupLimit::Unlimited);
140        // cgroup v1 unlimited sentinel (~i64::MAX, >= 1<<60).
141        assert_eq!(parse_cgroup_limit("9223372036854771712\n"), CgroupLimit::Unlimited);
142        assert_eq!(parse_cgroup_limit("  2147483648  "), CgroupLimit::Bytes(2_147_483_648));
143        assert_eq!(parse_cgroup_limit("0"), CgroupLimit::Invalid);
144        assert_eq!(parse_cgroup_limit("garbage"), CgroupLimit::Invalid);
145        assert_eq!(parse_cgroup_limit(""), CgroupLimit::Invalid);
146    }
147
148    /// The v1 "unlimited" sentinel check (`v >= 1 << 60`) must trigger AT the
149    /// boundary itself, not just for values far above it — a value exactly at
150    /// `1 << 60` is exactly what a `>` (instead of `>=`) comparison would miss.
151    #[test]
152    fn parse_cgroup_limit_treats_exactly_one_shl_60_as_unlimited() {
153        let boundary = (1u64 << 60).to_string();
154        assert_eq!(parse_cgroup_limit(&boundary), CgroupLimit::Unlimited);
155        // One byte below the boundary must still be a normal, finite limit.
156        let just_below = ((1u64 << 60) - 1).to_string();
157        assert_eq!(
158            parse_cgroup_limit(&just_below),
159            CgroupLimit::Bytes((1u64 << 60) - 1)
160        );
161    }
162
163    #[test]
164    fn parse_meminfo_picks_memtotal_in_kb_only() {
165        let meminfo = "MemTotal:       16305200 kB\n\
166                       MemFree:         1234567 kB\n\
167                       MemAvailable:    9876543 kB\n";
168        assert_eq!(parse_meminfo_memtotal_kb(meminfo), Some(16_305_200));
169        // Must NOT match MemFree/MemAvailable, and tolerate missing/garbage.
170        assert_eq!(parse_meminfo_memtotal_kb("MemFree: 100 kB\n"), None);
171        assert_eq!(parse_meminfo_memtotal_kb("MemTotal: notanumber kB"), None);
172        assert_eq!(parse_meminfo_memtotal_kb(""), None);
173    }
174
175
176    #[test]
177    fn explicit_budget_wins_over_ceilings() {
178        assert_eq!(
179            resolve_mem_budget_mb(Some(4096), Some(10 * GB), Some(32 * GB)),
180            4096
181        );
182    }
183
184    #[test]
185    fn explicit_zero_is_honored_as_opt_out() {
186        // Some(0) is a deliberate "turn the gate off", not "unset" - it must
187        // NOT fall through to a ceiling-derived budget.
188        assert_eq!(resolve_mem_budget_mb(Some(0), Some(10 * GB), Some(32 * GB)), 0);
189    }
190
191    #[test]
192    fn uses_tightest_readable_ceiling() {
193        // 70% of the tighter (cgroup) ceiling, not the host.
194        assert_eq!(
195            resolve_mem_budget_mb(None, Some(10 * GB), Some(32 * GB)),
196            10 * 1024 * 70 / 100,
197        );
198    }
199
200    #[test]
201    fn physical_ram_bounds_a_bare_vm_with_no_cgroup() {
202        // The load-bearing fix: no cgroup, but readable physical RAM -> a
203        // NON-ZERO budget, so the memory gate stays active on a bare VM.
204        let mb = resolve_mem_budget_mb(None, None, Some(16 * GB));
205        assert_eq!(mb, 16 * 1024 * 70 / 100);
206        assert!(mb > 0);
207    }
208
209    #[test]
210    fn no_readable_ceiling_disables_the_gate() {
211        assert_eq!(resolve_mem_budget_mb(None, None, None), 0);
212    }
213
214    #[cfg(target_os = "linux")]
215    #[test]
216    fn physical_memory_is_readable_on_linux() {
217        // On any Linux host (incl. CI) /proc/meminfo yields a positive total.
218        let total = total_physical_memory_bytes();
219        assert!(total.is_some_and(|b| b > 0));
220    }
221}