@@ -22,6 +22,10 @@ pub(crate) mod proto;
2222pub mod target;
2323pub mod target_metrics;
2424
25+ use std:: fs:: read_to_string;
26+ use std:: io:: Error ;
27+ use std:: path:: PathBuf ;
28+
2529use byte_unit:: Byte ;
2630use 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]
4057pub 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\n 0::/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