@@ -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,256 @@ pub(crate) fn full<T: Into<bytes::Bytes>>(
3438 . boxed ( )
3539}
3640
37- /// Get available memory for the process, checking cgroup v2 limits first,
41+ /// Get available memory for the process, checking cgroup limits first,
3842/// then falling back to system memory.
43+ ///
44+ /// Resolves the process's own cgroup path from `/proc/self/cgroup`, then
45+ /// checks the memory limit at that path. Falls back to the cgroup root, then
46+ /// cgroup v1, and finally total system memory.
3947#[ must_use]
4048pub 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 ) ;
49+ get_available_memory_with ( read_to_string)
50+ }
51+
52+ /// The cgroup v1 "no limit" sentinel: page-aligned `i64::MAX`.
53+ const CGROUP_V1_NO_LIMIT : u64 = 0x7FFF_FFFF_FFFF_F000 ;
54+
55+ fn parse_cgroup_limit ( content : & str ) -> Option < Byte > {
56+ let content = content. trim ( ) ;
57+ if content == "max" {
58+ return None ;
59+ }
60+ let bytes: u64 = content. parse ( ) . ok ( ) ?;
61+ if bytes >= CGROUP_V1_NO_LIMIT {
62+ return None ;
63+ }
64+ Some ( Byte :: from_u64 ( bytes) )
65+ }
66+
67+ /// Parse `/proc/self/cgroup` to find the cgroup v2 path for this process.
68+ ///
69+ /// In cgroup v2 unified mode, the file contains a single line like `0::/path`.
70+ /// Returns the path portion (e.g., `/path`), or `None` if not found.
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+ fn get_available_memory_with < F > ( read_file : F ) -> Byte
85+ where
86+ F : Fn ( PathBuf ) -> Result < String , Error > ,
87+ {
88+ // Try process-specific cgroup v2 path (handles host cgroup namespace)
89+ if let Ok ( proc_cgroup) = read_file ( PathBuf :: from ( "/proc/self/cgroup" ) )
90+ && let Some ( cgroup_path) = resolve_cgroup_v2_path ( & proc_cgroup)
91+ {
92+ let memory_max = PathBuf :: from ( format ! ( "/sys/fs/cgroup{cgroup_path}/memory.max" ) ) ;
93+ if let Ok ( content) = read_file ( memory_max)
94+ && let Some ( limit) = parse_cgroup_limit ( & content)
95+ {
4896 return limit;
4997 }
5098 }
5199
100+ // Try cgroup v2 root (works when container has its own cgroup namespace)
101+ if let Ok ( content) = read_file ( PathBuf :: from ( "/sys/fs/cgroup/memory.max" ) )
102+ && let Some ( limit) = parse_cgroup_limit ( & content)
103+ {
104+ return limit;
105+ }
106+
107+ // Try cgroup v1
108+ if let Ok ( content) = read_file ( PathBuf :: from ( "/sys/fs/cgroup/memory/memory.limit_in_bytes" ) )
109+ && let Some ( limit) = parse_cgroup_limit ( & content)
110+ {
111+ return limit;
112+ }
113+
52114 let sys = System :: new_all ( ) ;
53115 Byte :: from_u64 ( sys. total_memory ( ) )
54116}
117+
118+ #[ cfg( test) ]
119+ mod tests {
120+ use std:: io:: ErrorKind ;
121+
122+ use super :: * ;
123+
124+ fn mock_reader (
125+ files : & ' static [ ( & ' static str , & ' static str ) ] ,
126+ ) -> impl Fn ( PathBuf ) -> Result < String , Error > {
127+ move |path| {
128+ for ( p, content) in files {
129+ if PathBuf :: from ( p) == path {
130+ return Ok ( ( * content) . to_string ( ) ) ;
131+ }
132+ }
133+ Err ( Error :: new ( ErrorKind :: NotFound , "not found" ) )
134+ }
135+ }
136+
137+ #[ test]
138+ fn cgroup_v2_max_falls_back_to_system_memory ( ) {
139+ let read = mock_reader ( & [ ( "/sys/fs/cgroup/memory.max" , "max\n " ) ] ) ;
140+ let mem = get_available_memory_with ( read) ;
141+ let sys = System :: new_all ( ) ;
142+ assert_eq ! ( mem, Byte :: from_u64( sys. total_memory( ) ) ) ;
143+ }
144+
145+ #[ test]
146+ fn cgroup_v2_numeric_limit ( ) {
147+ let read = mock_reader ( & [ ( "/sys/fs/cgroup/memory.max" , "12582912\n " ) ] ) ;
148+ let mem = get_available_memory_with ( read) ;
149+ assert_eq ! ( mem, Byte :: from_u64( 12_582_912 ) ) ;
150+ }
151+
152+ #[ test]
153+ fn cgroup_v2_garbage_falls_back_to_system_memory ( ) {
154+ let read = mock_reader ( & [ ( "/sys/fs/cgroup/memory.max" , "not-a-number\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 no_cgroup_file_falls_back_to_system_memory ( ) {
162+ let read = mock_reader ( & [ ] ) ;
163+ let mem = get_available_memory_with ( read) ;
164+ let sys = System :: new_all ( ) ;
165+ assert_eq ! ( mem, Byte :: from_u64( sys. total_memory( ) ) ) ;
166+ }
167+
168+ #[ test]
169+ fn cgroup_v1_numeric_limit ( ) {
170+ let read = mock_reader ( & [ ( "/sys/fs/cgroup/memory/memory.limit_in_bytes" , "12582912\n " ) ] ) ;
171+ let mem = get_available_memory_with ( read) ;
172+ assert_eq ! ( mem, Byte :: from_u64( 12_582_912 ) ) ;
173+ }
174+
175+ #[ test]
176+ fn cgroup_v1_no_limit_sentinel_falls_back_to_system_memory ( ) {
177+ // cgroup v1 uses page-aligned i64::MAX as "no limit"
178+ let read = mock_reader ( & [ (
179+ "/sys/fs/cgroup/memory/memory.limit_in_bytes" ,
180+ "9223372036854771712\n " ,
181+ ) ] ) ;
182+ let mem = get_available_memory_with ( read) ;
183+ let sys = System :: new_all ( ) ;
184+ assert_eq ! ( mem, Byte :: from_u64( sys. total_memory( ) ) ) ;
185+ }
186+
187+ #[ test]
188+ fn cgroup_v2_takes_precedence_over_v1 ( ) {
189+ let read = mock_reader ( & [
190+ ( "/sys/fs/cgroup/memory.max" , "8388608\n " ) ,
191+ ( "/sys/fs/cgroup/memory/memory.limit_in_bytes" , "16777216\n " ) ,
192+ ] ) ;
193+ let mem = get_available_memory_with ( read) ;
194+ assert_eq ! ( mem, Byte :: from_u64( 8_388_608 ) ) ;
195+ }
196+
197+ #[ test]
198+ fn cgroup_v2_max_falls_through_to_v1 ( ) {
199+ let read = mock_reader ( & [
200+ ( "/sys/fs/cgroup/memory.max" , "max\n " ) ,
201+ ( "/sys/fs/cgroup/memory/memory.limit_in_bytes" , "16777216\n " ) ,
202+ ] ) ;
203+ let mem = get_available_memory_with ( read) ;
204+ assert_eq ! ( mem, Byte :: from_u64( 16_777_216 ) ) ;
205+ }
206+
207+ #[ test]
208+ fn process_specific_cgroup_v2_path ( ) {
209+ // Simulates host cgroup namespace: /proc/self/cgroup points to a
210+ // nested cgroup, and the memory limit is at that nested path.
211+ let read = mock_reader ( & [
212+ (
213+ "/proc/self/cgroup" ,
214+ "0::/system.slice/docker-abc123.scope\n " ,
215+ ) ,
216+ (
217+ "/sys/fs/cgroup/system.slice/docker-abc123.scope/memory.max" ,
218+ "12582912\n " ,
219+ ) ,
220+ // Root cgroup has no limit
221+ ( "/sys/fs/cgroup/memory.max" , "max\n " ) ,
222+ ] ) ;
223+ let mem = get_available_memory_with ( read) ;
224+ assert_eq ! ( mem, Byte :: from_u64( 12_582_912 ) ) ;
225+ }
226+
227+ #[ test]
228+ fn process_specific_path_takes_precedence_over_root ( ) {
229+ let read = mock_reader ( & [
230+ (
231+ "/proc/self/cgroup" ,
232+ "0::/system.slice/docker-abc123.scope\n " ,
233+ ) ,
234+ (
235+ "/sys/fs/cgroup/system.slice/docker-abc123.scope/memory.max" ,
236+ "8388608\n " ,
237+ ) ,
238+ ( "/sys/fs/cgroup/memory.max" , "16777216\n " ) ,
239+ ] ) ;
240+ let mem = get_available_memory_with ( read) ;
241+ assert_eq ! ( mem, Byte :: from_u64( 8_388_608 ) ) ;
242+ }
243+
244+ #[ test]
245+ fn process_specific_path_max_falls_through_to_root ( ) {
246+ let read = mock_reader ( & [
247+ (
248+ "/proc/self/cgroup" ,
249+ "0::/system.slice/docker-abc123.scope\n " ,
250+ ) ,
251+ (
252+ "/sys/fs/cgroup/system.slice/docker-abc123.scope/memory.max" ,
253+ "max\n " ,
254+ ) ,
255+ ( "/sys/fs/cgroup/memory.max" , "16777216\n " ) ,
256+ ] ) ;
257+ let mem = get_available_memory_with ( read) ;
258+ assert_eq ! ( mem, Byte :: from_u64( 16_777_216 ) ) ;
259+ }
260+
261+ #[ test]
262+ fn proc_cgroup_root_path_skipped ( ) {
263+ // When /proc/self/cgroup shows root path "/", skip it and fall
264+ // through to the root cgroup check.
265+ let read = mock_reader ( & [
266+ ( "/proc/self/cgroup" , "0::/\n " ) ,
267+ ( "/sys/fs/cgroup/memory.max" , "12582912\n " ) ,
268+ ] ) ;
269+ let mem = get_available_memory_with ( read) ;
270+ assert_eq ! ( mem, Byte :: from_u64( 12_582_912 ) ) ;
271+ }
272+
273+ #[ test]
274+ fn resolve_cgroup_v2_path_parses_correctly ( ) {
275+ assert_eq ! (
276+ resolve_cgroup_v2_path( "0::/system.slice/docker-abc.scope\n " ) ,
277+ Some ( "/system.slice/docker-abc.scope" )
278+ ) ;
279+ assert_eq ! ( resolve_cgroup_v2_path( "0::/\n " ) , None ) ;
280+ assert_eq ! ( resolve_cgroup_v2_path( "" ) , None ) ;
281+ // cgroup v1 lines (non-zero hierarchy ID) are ignored
282+ assert_eq ! (
283+ resolve_cgroup_v2_path( "1:memory:/docker/abc\n 0::/system.slice\n " ) ,
284+ Some ( "/system.slice" )
285+ ) ;
286+ }
287+
288+ #[ test]
289+ fn get_available_memory_returns_nonzero ( ) {
290+ let mem = get_available_memory ( ) ;
291+ assert ! ( mem. as_u64( ) > 0 ) ;
292+ }
293+ }
0 commit comments