Skip to content

Commit 2619756

Browse files
committed
Fix get available memory to work with cgroup v1
1 parent cc4fe22 commit 2619756

File tree

2 files changed

+270
-10
lines changed

2 files changed

+270
-10
lines changed

.github/workflows/container.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ jobs:
7777
copy-to-ecr:
7878
name: Copy ${{ matrix.arch }} to ECR
7979
needs: build
80-
runs-on: ubuntu-latest
80+
runs-on: ubuntu-24.04
8181
strategy:
8282
matrix:
8383
arch: [amd64, arm64]
@@ -137,7 +137,7 @@ jobs:
137137
manifest:
138138
name: Create ${{ matrix.registry }} manifest
139139
needs: [build, copy-to-ecr]
140-
runs-on: ubuntu-latest
140+
runs-on: ubuntu-24.04
141141
strategy:
142142
matrix:
143143
registry: [ghcr, ecr]

lading/src/lib.rs

Lines changed: 268 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ pub(crate) mod proto;
2222
pub mod target;
2323
pub mod target_metrics;
2424

25+
use std::fs::read_to_string;
26+
use std::io::Error;
27+
use std::path::PathBuf;
28+
2529
use byte_unit::Byte;
2630
use sysinfo::System;
2731

@@ -34,21 +38,277 @@ pub(crate) fn full<T: Into<bytes::Bytes>>(
3438
.boxed()
3539
}
3640

37-
/// Get available memory for the process, checking cgroup v2 limits first,
38-
/// then falling back to system memory.
41+
/// Determine the available memory for this process by inspecting cgroup
42+
/// limits, falling back to total system memory.
43+
///
44+
/// Resolution order:
45+
/// 1. cgroup v2 — walk from process cgroup up to root, return minimum
46+
/// `memory.max` across the ancestor chain
47+
/// 2. cgroup v2 root (`/sys/fs/cgroup/memory.max`)
48+
/// 3. Total system memory via `sysinfo`
49+
///
50+
/// Step 1 uses `/proc/self/cgroup` to resolve the process path.
51+
/// Step 2 is the fallback when `/proc/self/cgroup` is unavailable or shows root (`/`).
52+
///
53+
/// References:
54+
/// - cgroup v2: <https://docs.kernel.org/admin-guide/cgroup-v2.html>
55+
/// - `/proc/$PID/cgroup`: <https://docs.kernel.org/admin-guide/cgroup-v1/cgroups.html>
3956
#[must_use]
4057
pub fn get_available_memory() -> Byte {
41-
if let Ok(content) = std::fs::read_to_string("/sys/fs/cgroup/memory.max") {
42-
let content = content.trim();
43-
if content == "max" {
44-
return Byte::from_u64(u64::MAX);
58+
get_available_memory_with(read_to_string)
59+
}
60+
61+
fn parse_cgroup_v2_limit(content: &str) -> Option<Byte> {
62+
let content = content.trim();
63+
if content == "max" {
64+
return None;
65+
}
66+
let bytes: u64 = content.parse().ok()?;
67+
Some(Byte::from_u64(bytes))
68+
}
69+
70+
/// Extract the cgroup v2 path from `/proc/self/cgroup` content (`0::/path`).
71+
fn resolve_cgroup_v2_path(proc_cgroup_content: &str) -> Option<&str> {
72+
for line in proc_cgroup_content.lines() {
73+
// cgroup v2 unified entries start with "0::"
74+
if let Some(path) = line.strip_prefix("0::") {
75+
let path = path.trim();
76+
if !path.is_empty() && path != "/" {
77+
return Some(path);
78+
}
4579
}
46-
let ignore_case = true;
47-
if let Ok(limit) = Byte::parse_str(content.trim(), ignore_case) {
80+
}
81+
None
82+
}
83+
84+
/// Inner implementation of [`get_available_memory`] with an injectable file
85+
/// reader for testing.
86+
fn get_available_memory_with<F>(read_file: F) -> Byte
87+
where
88+
F: Fn(PathBuf) -> Result<String, Error>,
89+
{
90+
let proc_cgroup = read_file(PathBuf::from("/proc/self/cgroup")).ok();
91+
92+
// Try cgroup v2: walk from the process-specific path up to the root,
93+
// taking the minimum memory.max across the ancestor chain.
94+
if let Some(ref proc_cgroup) = proc_cgroup
95+
&& let Some(cgroup_path) = resolve_cgroup_v2_path(proc_cgroup)
96+
{
97+
let cgroup_root = PathBuf::from("/sys/fs/cgroup");
98+
let mut path = cgroup_root.join(&cgroup_path[1..]); // strip leading '/'
99+
let mut min_limit: Option<Byte> = None;
100+
loop {
101+
let memory_max = path.join("memory.max");
102+
if let Ok(content) = read_file(memory_max)
103+
&& let Some(limit) = parse_cgroup_v2_limit(&content)
104+
{
105+
min_limit = Some(match min_limit {
106+
Some(prev) if prev.as_u64() < limit.as_u64() => prev,
107+
_ => limit,
108+
});
109+
}
110+
if path == cgroup_root {
111+
break;
112+
}
113+
if !path.pop() {
114+
break;
115+
}
116+
}
117+
if let Some(limit) = min_limit {
48118
return limit;
49119
}
50120
}
51121

122+
// Try cgroup v2 root (works when container has its own cgroup namespace)
123+
if let Ok(content) = read_file(PathBuf::from("/sys/fs/cgroup/memory.max"))
124+
&& let Some(limit) = parse_cgroup_v2_limit(&content)
125+
{
126+
return limit;
127+
}
128+
52129
let sys = System::new_all();
53130
Byte::from_u64(sys.total_memory())
54131
}
132+
133+
#[cfg(test)]
134+
mod tests {
135+
use std::io::ErrorKind;
136+
137+
use super::*;
138+
139+
fn mock_reader(
140+
files: &'static [(&'static str, &'static str)],
141+
) -> impl Fn(PathBuf) -> Result<String, Error> {
142+
move |path| {
143+
for (p, content) in files {
144+
if PathBuf::from(p) == path {
145+
return Ok((*content).to_string());
146+
}
147+
}
148+
Err(Error::new(ErrorKind::NotFound, "not found"))
149+
}
150+
}
151+
152+
#[test]
153+
fn cgroup_v2_max_falls_back_to_system_memory() {
154+
let read = mock_reader(&[("/sys/fs/cgroup/memory.max", "max\n")]);
155+
let mem = get_available_memory_with(read);
156+
let sys = System::new_all();
157+
assert_eq!(mem, Byte::from_u64(sys.total_memory()));
158+
}
159+
160+
#[test]
161+
fn cgroup_v2_numeric_limit() {
162+
let read = mock_reader(&[("/sys/fs/cgroup/memory.max", "12582912\n")]);
163+
let mem = get_available_memory_with(read);
164+
assert_eq!(mem, Byte::from_u64(12_582_912));
165+
}
166+
167+
#[test]
168+
fn cgroup_v2_garbage_falls_back_to_system_memory() {
169+
let read = mock_reader(&[("/sys/fs/cgroup/memory.max", "not-a-number\n")]);
170+
let mem = get_available_memory_with(read);
171+
let sys = System::new_all();
172+
assert_eq!(mem, Byte::from_u64(sys.total_memory()));
173+
}
174+
175+
#[test]
176+
fn no_cgroup_file_falls_back_to_system_memory() {
177+
let read = mock_reader(&[]);
178+
let mem = get_available_memory_with(read);
179+
let sys = System::new_all();
180+
assert_eq!(mem, Byte::from_u64(sys.total_memory()));
181+
}
182+
183+
#[test]
184+
fn process_specific_cgroup_v2_path() {
185+
// Simulates host cgroup namespace: /proc/self/cgroup points to a
186+
// nested cgroup, and the memory limit is at that nested path.
187+
let read = mock_reader(&[
188+
(
189+
"/proc/self/cgroup",
190+
"0::/system.slice/docker-abc123.scope\n",
191+
),
192+
(
193+
"/sys/fs/cgroup/system.slice/docker-abc123.scope/memory.max",
194+
"12582912\n",
195+
),
196+
// Root cgroup has no limit
197+
("/sys/fs/cgroup/memory.max", "max\n"),
198+
]);
199+
let mem = get_available_memory_with(read);
200+
assert_eq!(mem, Byte::from_u64(12_582_912));
201+
}
202+
203+
#[test]
204+
fn process_specific_path_takes_precedence_over_root() {
205+
let read = mock_reader(&[
206+
(
207+
"/proc/self/cgroup",
208+
"0::/system.slice/docker-abc123.scope\n",
209+
),
210+
(
211+
"/sys/fs/cgroup/system.slice/docker-abc123.scope/memory.max",
212+
"8388608\n",
213+
),
214+
("/sys/fs/cgroup/memory.max", "16777216\n"),
215+
]);
216+
let mem = get_available_memory_with(read);
217+
assert_eq!(mem, Byte::from_u64(8_388_608));
218+
}
219+
220+
#[test]
221+
fn process_specific_path_max_falls_through_to_root() {
222+
let read = mock_reader(&[
223+
(
224+
"/proc/self/cgroup",
225+
"0::/system.slice/docker-abc123.scope\n",
226+
),
227+
(
228+
"/sys/fs/cgroup/system.slice/docker-abc123.scope/memory.max",
229+
"max\n",
230+
),
231+
("/sys/fs/cgroup/memory.max", "16777216\n"),
232+
]);
233+
let mem = get_available_memory_with(read);
234+
assert_eq!(mem, Byte::from_u64(16_777_216));
235+
}
236+
237+
#[test]
238+
fn proc_cgroup_root_path_skipped() {
239+
// When /proc/self/cgroup shows root path "/", skip it and fall
240+
// through to the root cgroup check.
241+
let read = mock_reader(&[
242+
("/proc/self/cgroup", "0::/\n"),
243+
("/sys/fs/cgroup/memory.max", "12582912\n"),
244+
]);
245+
let mem = get_available_memory_with(read);
246+
assert_eq!(mem, Byte::from_u64(12_582_912));
247+
}
248+
249+
#[test]
250+
fn resolve_cgroup_v2_path_parses_correctly() {
251+
assert_eq!(
252+
resolve_cgroup_v2_path("0::/system.slice/docker-abc.scope\n"),
253+
Some("/system.slice/docker-abc.scope")
254+
);
255+
assert_eq!(resolve_cgroup_v2_path("0::/\n"), None);
256+
assert_eq!(resolve_cgroup_v2_path(""), None);
257+
// Non-v2 lines are ignored
258+
assert_eq!(
259+
resolve_cgroup_v2_path("1:memory:/docker/abc\n0::/system.slice\n"),
260+
Some("/system.slice")
261+
);
262+
}
263+
264+
#[test]
265+
fn cgroup_v2_ancestor_limit_is_tighter() {
266+
// Nested v2 cgroup where the parent has a tighter limit than the
267+
// child. The effective limit is the minimum up the ancestor chain.
268+
let read = mock_reader(&[
269+
("/proc/self/cgroup", "0::/parent/child\n"),
270+
// child: 16 MB
271+
("/sys/fs/cgroup/parent/child/memory.max", "16777216\n"),
272+
// parent: 8 MB (tighter)
273+
("/sys/fs/cgroup/parent/memory.max", "8388608\n"),
274+
// root: no limit
275+
("/sys/fs/cgroup/memory.max", "max\n"),
276+
]);
277+
let mem = get_available_memory_with(read);
278+
assert_eq!(mem, Byte::from_u64(8_388_608));
279+
}
280+
281+
#[test]
282+
fn cgroup_v2_ancestor_limit_three_levels() {
283+
// Three-level hierarchy: grandparent is tightest.
284+
let read = mock_reader(&[
285+
("/proc/self/cgroup", "0::/a/b/c\n"),
286+
("/sys/fs/cgroup/a/b/c/memory.max", "33554432\n"), // 32 MB
287+
("/sys/fs/cgroup/a/b/memory.max", "16777216\n"), // 16 MB
288+
("/sys/fs/cgroup/a/memory.max", "8388608\n"), // 8 MB (tightest)
289+
("/sys/fs/cgroup/memory.max", "max\n"),
290+
]);
291+
let mem = get_available_memory_with(read);
292+
assert_eq!(mem, Byte::from_u64(8_388_608));
293+
}
294+
295+
#[test]
296+
fn cgroup_v2_ancestor_middle_is_tightest() {
297+
// Middle ancestor is tightest.
298+
let read = mock_reader(&[
299+
("/proc/self/cgroup", "0::/a/b/c\n"),
300+
("/sys/fs/cgroup/a/b/c/memory.max", "33554432\n"), // 32 MB
301+
("/sys/fs/cgroup/a/b/memory.max", "4194304\n"), // 4 MB (tightest)
302+
("/sys/fs/cgroup/a/memory.max", "16777216\n"), // 16 MB
303+
("/sys/fs/cgroup/memory.max", "max\n"),
304+
]);
305+
let mem = get_available_memory_with(read);
306+
assert_eq!(mem, Byte::from_u64(4_194_304));
307+
}
308+
309+
#[test]
310+
fn get_available_memory_returns_nonzero() {
311+
let mem = get_available_memory();
312+
assert!(mem.as_u64() > 0);
313+
}
314+
}

0 commit comments

Comments
 (0)