From 3c95c4a7fbe0cb5a5e940c8d428f2ccbb595db0a Mon Sep 17 00:00:00 2001 From: leiysky Date: Wed, 20 May 2026 20:21:52 +0800 Subject: [PATCH 1/2] fix: detect hybrid cgroup memory limits --- src/unix/linux/cgroup.rs | 291 +++++++++++++++++++++++++++++++++++---- 1 file changed, 263 insertions(+), 28 deletions(-) diff --git a/src/unix/linux/cgroup.rs b/src/unix/linux/cgroup.rs index aaba3cf5e..57f356e22 100644 --- a/src/unix/linux/cgroup.rs +++ b/src/unix/linux/cgroup.rs @@ -13,31 +13,55 @@ struct CGroupLimitsContext { swap_free: u64, } +#[derive(Debug, Default, PartialEq, Eq)] +struct CGroupPath { + v2: Option, + v1_memory: Option, +} + pub(crate) fn limits_for_system() -> Option { let v2_base = Path::new("/sys/fs/cgroup"); let v1_base = Path::new("/sys/fs/cgroup/memory"); - limits_for_base(v2_base, v2_base, v1_base, v1_base) + limits_for_base(Some(v2_base), v2_base, Some(v1_base), v1_base) } pub(crate) fn limits_for_process(proc_path: &Path) -> Option { let cgroup_path = get_cgroup_path(&proc_path.join("cgroup"))?; let v2_root = Path::new("/sys/fs/cgroup"); let v1_root = Path::new("/sys/fs/cgroup/memory"); - let v2_base = v2_root.join(&cgroup_path); - let v1_base = v1_root.join(&cgroup_path); + let v2_base = cgroup_path.v2.as_ref().map(|path| v2_root.join(path)); + let v1_base = cgroup_path + .v1_memory + .as_ref() + .map(|path| v1_root.join(path)); - limits_for_base(&v2_base, v2_root, &v1_base, v1_root) + limits_for_base(v2_base.as_deref(), v2_root, v1_base.as_deref(), v1_root) } +// A process may have only a cgroup v2 entry, only a v1 memory controller entry, +// or both. Keep the bases optional so hybrid cgroups don't reuse the v2 path for +// the v1 memory controller. fn limits_for_base( - v2_base: &Path, + v2_base: Option<&Path>, v2_root: &Path, - v1_base: &Path, + v1_base: Option<&Path>, v1_root: &Path, ) -> Option { let context = read_cgroup_limits_context()?; - v2_limits(v2_base, v2_root, context).or_else(|| v1_limits(v1_base, v1_root, context)) + limits_for_base_with_context(v2_base, v2_root, v1_base, v1_root, context) +} + +fn limits_for_base_with_context( + v2_base: Option<&Path>, + v2_root: &Path, + v1_base: Option<&Path>, + v1_root: &Path, + context: CGroupLimitsContext, +) -> Option { + v1_base + .and_then(|v1_base| v1_limits(v1_base, v1_root, context)) + .or_else(|| v2_base.and_then(|v2_base| v2_limits(v2_base, v2_root, context))) } fn read_cgroup_limits_context() -> Option { @@ -131,8 +155,8 @@ where F: Fn(&Path) -> u64, { let mem_cur = read_u64(&base.join(usage_file))?; - let mut total_memory = mem_total; - let mut free_memory = mem_total.saturating_sub(mem_cur); + let mut total_memory = None; + let mut free_memory = None; for (pos, path) in base.ancestors().enumerate() { let is_base = pos == 0; @@ -147,11 +171,17 @@ where } else { read_u64(&path.join(usage_file))? }; - total_memory = min(total_memory, mem_max); - free_memory = min(free_memory, mem_max.saturating_sub(mem_cur)); + total_memory = Some(match total_memory { + Some(total_memory) => min(total_memory, mem_max), + None => mem_max, + }); + free_memory = Some(match free_memory { + Some(free_memory) => min(free_memory, mem_max.saturating_sub(mem_cur)), + None => mem_max.saturating_sub(mem_cur), + }); } if path == root { - return Some((total_memory, free_memory)); + return Some((total_memory?, free_memory?)); } } @@ -229,32 +259,59 @@ fn read_table_key(filename: &Path, target_key: &str, colsep: char) -> Option Option { +fn get_cgroup_path(path: &Path) -> Option { let content = get_all_utf8_data(path, 4096).ok()?; - parse_cgroup_path(&content) + let cgroup_path = parse_cgroup_path(&content); + if cgroup_path.v2.is_none() && cgroup_path.v1_memory.is_none() { + return None; + } + Some(cgroup_path) } -fn parse_cgroup_path(content: &str) -> Option { +fn parse_cgroup_path(content: &str) -> CGroupPath { + let mut cgroup_path = CGroupPath::default(); + for line in content.lines() { let mut fields = line.splitn(3, ':'); - let hierarchy_id = fields.next()?; - let controllers = fields.next()?; - let path = fields.next()?; - - if (hierarchy_id == "0" && controllers.is_empty()) - || controllers - .split(',') - .any(|controller| controller == "memory") + let Some(hierarchy_id) = fields.next() else { + continue; + }; + let Some(controllers) = fields.next() else { + continue; + }; + let Some(path) = fields.next() else { + continue; + }; + + if hierarchy_id == "0" && controllers.is_empty() { + cgroup_path.v2 = Some(normalize_cgroup_path(path)); + continue; + } + + if controllers + .split(',') + .any(|controller| controller == "memory") { - return Some(Path::new(path).strip_prefix("/").ok()?.to_path_buf()); + cgroup_path.v1_memory = Some(normalize_cgroup_path(path)); } } - None + + cgroup_path +} + +fn normalize_cgroup_path(path: &str) -> PathBuf { + if let Ok(path) = Path::new(path).strip_prefix("/") { + return path.to_path_buf(); + } + + PathBuf::from(path) } #[cfg(test)] mod test { use super::CGroupLimitsContext; + use super::CGroupPath; + use super::limits_for_base_with_context; use super::parse_cgroup_path; use super::read_table; use super::read_table_key; @@ -408,6 +465,31 @@ mod test { assert_eq!(limits.free_memory, 50); } + #[test] + fn test_v2_unlimited_memory() { + let root = tempdir().unwrap(); + let child = root.path().join("child"); + + create_dir_all(&child).unwrap(); + write(root.path().join("memory.max"), "max").unwrap(); + write(root.path().join("memory.current"), "350").unwrap(); + write(child.join("memory.max"), "max").unwrap(); + write(child.join("memory.current"), "100").unwrap(); + write(child.join("memory.stat"), "anon 30\n").unwrap(); + + let limits = v2_limits( + &child, + root.path(), + CGroupLimitsContext { + mem_total: 2000, + swap_total: 1000, + swap_free: 700, + }, + ); + + assert!(limits.is_none()); + } + #[test] fn test_v1_parent_limit() { let root = tempdir().unwrap(); @@ -442,11 +524,145 @@ mod test { assert_eq!(limits.rss, 30); } + #[test] + fn test_v1_unlimited_memory() { + let root = tempdir().unwrap(); + let child = root.path().join("child"); + + create_dir_all(&child).unwrap(); + write( + root.path().join("memory.limit_in_bytes"), + u64::MAX.to_string(), + ) + .unwrap(); + write(root.path().join("memory.usage_in_bytes"), "350").unwrap(); + write(child.join("memory.limit_in_bytes"), u64::MAX.to_string()).unwrap(); + write(child.join("memory.usage_in_bytes"), "100").unwrap(); + write(child.join("memory.stat"), "total_rss 30\n").unwrap(); + + let limits = v1_limits( + &child, + root.path(), + CGroupLimitsContext { + mem_total: 2000, + swap_total: 1000, + swap_free: 700, + }, + ); + + assert!(limits.is_none()); + } + + #[test] + fn test_hybrid_cgroup_uses_v1_memory_path_when_v2_is_unlimited() { + let root = tempdir().unwrap(); + let v2_root = root.path().join("unified"); + let v2_child = v2_root.join("system.slice/service.scope"); + let v1_root = root.path().join("memory"); + let v1_child = v1_root.join("memory.slice/service.scope"); + let host_memory = 32 * 1024 * 1024 * 1024; + let cgroup_limit = 8 * 1024 * 1024 * 1024; + let cgroup_usage = 1024 * 1024 * 1024; + + create_dir_all(&v2_child).unwrap(); + create_dir_all(&v1_child).unwrap(); + write(v2_root.join("memory.max"), "max").unwrap(); + write(v2_root.join("memory.current"), cgroup_usage.to_string()).unwrap(); + write(v2_child.join("memory.max"), "max").unwrap(); + write(v2_child.join("memory.current"), cgroup_usage.to_string()).unwrap(); + write(v2_child.join("memory.stat"), "anon 1073741824\n").unwrap(); + write( + v1_child.join("memory.limit_in_bytes"), + cgroup_limit.to_string(), + ) + .unwrap(); + write( + v1_child.join("memory.usage_in_bytes"), + cgroup_usage.to_string(), + ) + .unwrap(); + write(v1_child.join("memory.stat"), "total_rss 1073741824\n").unwrap(); + + let cgroup_path = parse_cgroup_path( + "0::/system.slice/service.scope\n\ + 11:memory:/memory.slice/service.scope\n", + ); + let v2_base = cgroup_path.v2.as_ref().map(|path| v2_root.join(path)); + let v1_base = cgroup_path + .v1_memory + .as_ref() + .map(|path| v1_root.join(path)); + + let limits = limits_for_base_with_context( + v2_base.as_deref(), + &v2_root, + v1_base.as_deref(), + &v1_root, + CGroupLimitsContext { + mem_total: host_memory, + swap_total: 0, + swap_free: 0, + }, + ) + .unwrap(); + + assert_eq!(limits.total_memory, cgroup_limit); + assert_eq!(limits.free_memory, cgroup_limit - cgroup_usage); + assert_eq!(limits.rss, cgroup_usage); + } + + #[test] + fn test_hybrid_cgroup_prefers_v1_memory_path_over_v2_limit() { + let root = tempdir().unwrap(); + let v2_root = root.path().join("unified"); + let v2_child = v2_root.join("system.slice/service.scope"); + let v1_root = root.path().join("memory"); + let v1_child = v1_root.join("memory.slice/service.scope"); + let v2_limit = 32 * 1024 * 1024 * 1024; + let v1_limit = 8 * 1024 * 1024 * 1024; + let cgroup_usage = 1024 * 1024 * 1024; + + create_dir_all(&v2_child).unwrap(); + create_dir_all(&v1_child).unwrap(); + write(v2_root.join("memory.max"), v2_limit.to_string()).unwrap(); + write(v2_root.join("memory.current"), cgroup_usage.to_string()).unwrap(); + write(v2_child.join("memory.max"), v2_limit.to_string()).unwrap(); + write(v2_child.join("memory.current"), cgroup_usage.to_string()).unwrap(); + write(v2_child.join("memory.stat"), "anon 1073741824\n").unwrap(); + write(v1_child.join("memory.limit_in_bytes"), v1_limit.to_string()).unwrap(); + write( + v1_child.join("memory.usage_in_bytes"), + cgroup_usage.to_string(), + ) + .unwrap(); + write(v1_child.join("memory.stat"), "total_rss 1073741824\n").unwrap(); + + let limits = limits_for_base_with_context( + Some(&v2_child), + &v2_root, + Some(&v1_child), + &v1_root, + CGroupLimitsContext { + mem_total: v2_limit, + swap_total: 0, + swap_free: 0, + }, + ) + .unwrap(); + + assert_eq!(limits.total_memory, v1_limit); + assert_eq!(limits.free_memory, v1_limit - cgroup_usage); + assert_eq!(limits.rss, cgroup_usage); + } + #[test] fn test_parse_cgroup_path_v2() { assert_eq!( parse_cgroup_path("0::/user.slice/session.scope"), - Some(PathBuf::from("user.slice/session.scope")) + CGroupPath { + v2: Some(PathBuf::from("user.slice/session.scope")), + v1_memory: None, + } ); } @@ -454,12 +670,31 @@ mod test { fn test_parse_cgroup_path_v1_memory() { assert_eq!( parse_cgroup_path("12:cpuset:/\n11:memory:/system.slice/service.scope"), - Some(PathBuf::from("system.slice/service.scope")), + CGroupPath { + v2: None, + v1_memory: Some(PathBuf::from("system.slice/service.scope")), + } + ); + } + + #[test] + fn test_parse_cgroup_path_hybrid() { + assert_eq!( + parse_cgroup_path( + "0::/system.slice/service.scope\n11:memory:/kubepods/pod/container\n" + ), + CGroupPath { + v2: Some(PathBuf::from("system.slice/service.scope")), + v1_memory: Some(PathBuf::from("kubepods/pod/container")), + } ); } #[test] fn test_parse_cgroup_path_missing_memory_controller() { - assert_eq!(parse_cgroup_path("12:cpuset:/\n10:cpu:/"), None); + assert_eq!( + parse_cgroup_path("12:cpuset:/\n10:cpu:/"), + CGroupPath::default() + ); } } From 99840ee51a80cb5b3a4e3a7a6254e7e18943bf6b Mon Sep 17 00:00:00 2001 From: leiysky Date: Thu, 21 May 2026 15:27:15 +0800 Subject: [PATCH 2/2] fix: resolve cgroup memory paths from mountinfo --- src/unix/linux/cgroup.rs | 431 +++++++++++++++++++++++++++++++++++---- 1 file changed, 395 insertions(+), 36 deletions(-) diff --git a/src/unix/linux/cgroup.rs b/src/unix/linux/cgroup.rs index 57f356e22..58cc7d0d0 100644 --- a/src/unix/linux/cgroup.rs +++ b/src/unix/linux/cgroup.rs @@ -19,49 +19,83 @@ struct CGroupPath { v1_memory: Option, } +impl CGroupPath { + fn is_empty(&self) -> bool { + self.v2.is_none() && self.v1_memory.is_none() + } +} + +#[derive(Debug, PartialEq, Eq)] +struct CGroupBase { + base: PathBuf, + root: PathBuf, +} + +impl CGroupBase { + fn new(base: PathBuf, root: PathBuf) -> Self { + Self { base, root } + } + + fn root(root: &Path) -> Self { + Self::new(root.to_path_buf(), root.to_path_buf()) + } +} + +#[derive(Debug, PartialEq, Eq)] +struct CGroupMount { + root: PathBuf, + mount_point: PathBuf, +} + +#[derive(Debug, Default, PartialEq, Eq)] +struct CGroupMounts { + v2: Vec, + v1_memory: Vec, +} + pub(crate) fn limits_for_system() -> Option { let v2_base = Path::new("/sys/fs/cgroup"); let v1_base = Path::new("/sys/fs/cgroup/memory"); - limits_for_base(Some(v2_base), v2_base, Some(v1_base), v1_base) + limits_for_base(&[CGroupBase::root(v2_base)], &[CGroupBase::root(v1_base)]) } pub(crate) fn limits_for_process(proc_path: &Path) -> Option { let cgroup_path = get_cgroup_path(&proc_path.join("cgroup"))?; + let cgroup_mounts = get_cgroup_mounts(&proc_path.join("mountinfo")); let v2_root = Path::new("/sys/fs/cgroup"); let v1_root = Path::new("/sys/fs/cgroup/memory"); - let v2_base = cgroup_path.v2.as_ref().map(|path| v2_root.join(path)); - let v1_base = cgroup_path - .v1_memory - .as_ref() - .map(|path| v1_root.join(path)); + let (v2_bases, v1_bases) = + cgroup_base_paths(&cgroup_path, cgroup_mounts.as_ref(), v2_root, v1_root); - limits_for_base(v2_base.as_deref(), v2_root, v1_base.as_deref(), v1_root) + limits_for_base(&v2_bases, &v1_bases) } -// A process may have only a cgroup v2 entry, only a v1 memory controller entry, -// or both. Keep the bases optional so hybrid cgroups don't reuse the v2 path for -// the v1 memory controller. +/// Evaluate candidate cgroup base paths for a process or the whole system. +/// Prefer paths resolved from mountinfo, then fall back to the conventional +/// cgroup locations. v1 memory is tried before v2 because hybrid cgroups expose +/// the effective memory limit through the v1 memory controller. fn limits_for_base( - v2_base: Option<&Path>, - v2_root: &Path, - v1_base: Option<&Path>, - v1_root: &Path, + v2_bases: &[CGroupBase], + v1_bases: &[CGroupBase], ) -> Option { let context = read_cgroup_limits_context()?; - limits_for_base_with_context(v2_base, v2_root, v1_base, v1_root, context) + limits_for_base_with_context(v2_bases, v1_bases, context) } fn limits_for_base_with_context( - v2_base: Option<&Path>, - v2_root: &Path, - v1_base: Option<&Path>, - v1_root: &Path, + v2_bases: &[CGroupBase], + v1_bases: &[CGroupBase], context: CGroupLimitsContext, ) -> Option { - v1_base - .and_then(|v1_base| v1_limits(v1_base, v1_root, context)) - .or_else(|| v2_base.and_then(|v2_base| v2_limits(v2_base, v2_root, context))) + v1_bases + .iter() + .find_map(|v1_base| v1_limits(&v1_base.base, &v1_base.root, context)) + .or_else(|| { + v2_bases + .iter() + .find_map(|v2_base| v2_limits(&v2_base.base, &v2_base.root, context)) + }) } fn read_cgroup_limits_context() -> Option { @@ -262,12 +296,156 @@ fn read_table_key(filename: &Path, target_key: &str, colsep: char) -> Option Option { let content = get_all_utf8_data(path, 4096).ok()?; let cgroup_path = parse_cgroup_path(&content); - if cgroup_path.v2.is_none() && cgroup_path.v1_memory.is_none() { + if cgroup_path.is_empty() { return None; } Some(cgroup_path) } +fn get_cgroup_mounts(path: &Path) -> Option { + let content = get_all_utf8_data(path, 1_048_576).ok()?; + Some(parse_cgroup_mounts(&content)) +} + +fn cgroup_base_paths( + cgroup_path: &CGroupPath, + cgroup_mounts: Option<&CGroupMounts>, + v2_root: &Path, + v1_root: &Path, +) -> (Vec, Vec) { + let v2_mounts = cgroup_mounts + .map(|mounts| mounts.v2.as_slice()) + .unwrap_or(&[]); + let v2_bases = match &cgroup_path.v2 { + Some(path) => cgroup_bases_for_path(path, v2_mounts, v2_root), + None => Vec::new(), + }; + + let v1_memory_mounts = cgroup_mounts + .map(|mounts| mounts.v1_memory.as_slice()) + .unwrap_or(&[]); + let v1_bases = match &cgroup_path.v1_memory { + Some(path) => cgroup_bases_for_path(path, v1_memory_mounts, v1_root), + None => Vec::new(), + }; + + (v2_bases, v1_bases) +} + +fn cgroup_bases_for_path( + cgroup_path: &Path, + mounts: &[CGroupMount], + fallback_root: &Path, +) -> Vec { + let mut bases = Vec::new(); + + for mount in mounts { + if let Some(base) = resolve_cgroup_base(cgroup_path, mount) { + push_unique_base(&mut bases, base); + } + } + + push_unique_base( + &mut bases, + CGroupBase::new( + join_cgroup_path(fallback_root, cgroup_path), + fallback_root.to_path_buf(), + ), + ); + push_unique_base(&mut bases, CGroupBase::root(fallback_root)); + + bases +} + +fn push_unique_base(bases: &mut Vec, candidate: CGroupBase) { + if !bases.iter().any(|existing| existing == &candidate) { + bases.push(candidate); + } +} + +fn resolve_cgroup_base(cgroup_path: &Path, mount: &CGroupMount) -> Option { + let relative_path = if mount.root.as_os_str().is_empty() { + cgroup_path + } else { + cgroup_path.strip_prefix(&mount.root).ok()? + }; + + Some(CGroupBase::new( + join_cgroup_path(&mount.mount_point, relative_path), + mount.mount_point.clone(), + )) +} + +fn join_cgroup_path(root: &Path, path: &Path) -> PathBuf { + if path.as_os_str().is_empty() { + return root.to_path_buf(); + } + + root.join(path) +} + +fn parse_cgroup_mounts(content: &str) -> CGroupMounts { + let mut mounts = CGroupMounts::default(); + + for line in content.lines() { + let mut fields = line.split(' '); + let Some(_mount_id) = fields.next() else { + continue; + }; + let Some(_parent_id) = fields.next() else { + continue; + }; + let Some(_major_minor) = fields.next() else { + continue; + }; + let Some(root) = fields.next() else { + continue; + }; + let Some(mount_point) = fields.next() else { + continue; + }; + let Some(_mount_options) = fields.next() else { + continue; + }; + + let mut found_separator = false; + for field in fields.by_ref() { + if field == "-" { + found_separator = true; + break; + } + } + if !found_separator { + continue; + } + + let Some(filesystem_type) = fields.next() else { + continue; + }; + let Some(_mount_source) = fields.next() else { + continue; + }; + let Some(super_options) = fields.next() else { + continue; + }; + + let mount = CGroupMount { + root: normalize_mountinfo_path(root), + mount_point: PathBuf::from(decode_cgroup_path(mount_point)), + }; + + match filesystem_type { + "cgroup2" => mounts.v2.push(mount), + "cgroup" if super_options.split(',').any(|option| option == "memory") => { + mounts.v1_memory.push(mount) + } + _ => (), + } + } + + mounts +} + fn parse_cgroup_path(content: &str) -> CGroupPath { let mut cgroup_path = CGroupPath::default(); @@ -307,11 +485,61 @@ fn normalize_cgroup_path(path: &str) -> PathBuf { PathBuf::from(path) } +fn normalize_mountinfo_path(path: &str) -> PathBuf { + let path = decode_cgroup_path(path); + + if let Ok(path) = Path::new(&path).strip_prefix("/") { + return path.to_path_buf(); + } + + PathBuf::from(path) +} + +fn decode_cgroup_path(path: &str) -> String { + let bytes = path.as_bytes(); + let mut decoded = Vec::with_capacity(bytes.len()); + let mut pos = 0; + + while pos < bytes.len() { + if bytes[pos] == b'\\' + && pos + 3 < bytes.len() + && let Some(value) = decode_octal_escape(&bytes[pos + 1..pos + 4]) + { + decoded.push(value); + pos += 4; + continue; + } + + decoded.push(bytes[pos]); + pos += 1; + } + + String::from_utf8(decoded).unwrap_or_else(|_| path.to_owned()) +} + +fn decode_octal_escape(digits: &[u8]) -> Option { + let mut value = 0; + + for digit in digits { + if !(b'0'..=b'7').contains(digit) { + return None; + } + value = value * 8 + (digit - b'0'); + } + + Some(value) +} + #[cfg(test)] mod test { + use super::CGroupBase; use super::CGroupLimitsContext; + use super::CGroupMount; + use super::CGroupMounts; use super::CGroupPath; + use super::cgroup_base_paths; use super::limits_for_base_with_context; + use super::parse_cgroup_mounts; use super::parse_cgroup_path; use super::read_table; use super::read_table_key; @@ -587,17 +815,116 @@ mod test { "0::/system.slice/service.scope\n\ 11:memory:/memory.slice/service.scope\n", ); - let v2_base = cgroup_path.v2.as_ref().map(|path| v2_root.join(path)); - let v1_base = cgroup_path - .v1_memory - .as_ref() - .map(|path| v1_root.join(path)); + let (v2_bases, v1_bases) = cgroup_base_paths(&cgroup_path, None, &v2_root, &v1_root); + + let limits = limits_for_base_with_context( + &v2_bases, + &v1_bases, + CGroupLimitsContext { + mem_total: host_memory, + swap_total: 0, + swap_free: 0, + }, + ) + .unwrap(); + + assert_eq!(limits.total_memory, cgroup_limit); + assert_eq!(limits.free_memory, cgroup_limit - cgroup_usage); + assert_eq!(limits.rss, cgroup_usage); + } + + #[test] + fn test_mountinfo_resolves_v1_memory_path() { + let root = tempdir().unwrap(); + let v2_root = root.path().join("unified"); + let v1_root = root.path().join("memory"); + let v1_mount = root.path().join("mounted-memory"); + let v1_child = v1_mount.join("pod/container"); + let host_memory = 32 * 1024 * 1024 * 1024; + let cgroup_limit = 8 * 1024 * 1024 * 1024; + let cgroup_usage = 1024 * 1024 * 1024; + + create_dir_all(&v1_child).unwrap(); + write( + v1_child.join("memory.limit_in_bytes"), + cgroup_limit.to_string(), + ) + .unwrap(); + write( + v1_child.join("memory.usage_in_bytes"), + cgroup_usage.to_string(), + ) + .unwrap(); + write(v1_child.join("memory.stat"), "total_rss 1073741824\n").unwrap(); + + let cgroup_path = parse_cgroup_path("11:memory:/kubepods/pod/container\n"); + let mountinfo = format!( + "30 23 0:25 /kubepods {} rw,nosuid,nodev,noexec - cgroup cgroup rw,memory\n", + v1_mount.display(), + ); + let cgroup_mounts = parse_cgroup_mounts(&mountinfo); + let (v2_bases, v1_bases) = + cgroup_base_paths(&cgroup_path, Some(&cgroup_mounts), &v2_root, &v1_root); + + assert_eq!( + v1_bases.first(), + Some(&CGroupBase::new(v1_child.clone(), v1_mount.clone())) + ); + + let limits = limits_for_base_with_context( + &v2_bases, + &v1_bases, + CGroupLimitsContext { + mem_total: host_memory, + swap_total: 0, + swap_free: 0, + }, + ) + .unwrap(); + + assert_eq!(limits.total_memory, cgroup_limit); + assert_eq!(limits.free_memory, cgroup_limit - cgroup_usage); + assert_eq!(limits.rss, cgroup_usage); + } + + #[test] + fn test_cgroup_root_fallback_uses_v1_memory_limit() { + let root = tempdir().unwrap(); + let v2_root = root.path().join("unified"); + let v2_child = v2_root.join("system.slice/service.scope"); + let v1_root = root.path().join("memory"); + let host_memory = 32 * 1024 * 1024 * 1024; + let cgroup_limit = 8 * 1024 * 1024 * 1024; + let cgroup_usage = 1024 * 1024 * 1024; + + create_dir_all(&v2_child).unwrap(); + create_dir_all(&v1_root).unwrap(); + write(v2_root.join("memory.max"), "max").unwrap(); + write(v2_root.join("memory.current"), cgroup_usage.to_string()).unwrap(); + write(v2_child.join("memory.max"), "max").unwrap(); + write(v2_child.join("memory.current"), cgroup_usage.to_string()).unwrap(); + write(v2_child.join("memory.stat"), "anon 1073741824\n").unwrap(); + write( + v1_root.join("memory.limit_in_bytes"), + cgroup_limit.to_string(), + ) + .unwrap(); + write( + v1_root.join("memory.usage_in_bytes"), + cgroup_usage.to_string(), + ) + .unwrap(); + write(v1_root.join("memory.stat"), "total_rss 1073741824\n").unwrap(); + + let cgroup_path = parse_cgroup_path( + "0::/system.slice/service.scope\n\ + 11:memory:/kubepods/pod/container\n", + ); + let (v2_bases, v1_bases) = cgroup_base_paths(&cgroup_path, None, &v2_root, &v1_root); let limits = limits_for_base_with_context( - v2_base.as_deref(), - &v2_root, - v1_base.as_deref(), - &v1_root, + &v2_bases, + &v1_bases, CGroupLimitsContext { mem_total: host_memory, swap_total: 0, @@ -637,11 +964,11 @@ mod test { .unwrap(); write(v1_child.join("memory.stat"), "total_rss 1073741824\n").unwrap(); + let v2_bases = vec![CGroupBase::new(v2_child, v2_root)]; + let v1_bases = vec![CGroupBase::new(v1_child, v1_root)]; let limits = limits_for_base_with_context( - Some(&v2_child), - &v2_root, - Some(&v1_child), - &v1_root, + &v2_bases, + &v1_bases, CGroupLimitsContext { mem_total: v2_limit, swap_total: 0, @@ -655,6 +982,38 @@ mod test { assert_eq!(limits.rss, cgroup_usage); } + #[test] + fn test_parse_cgroup_mounts() { + assert_eq!( + parse_cgroup_mounts( + "29 23 0:28 /kubepods\\040burstable /sys/fs/cgroup/memory\\040controller rw,nosuid,nodev,noexec - cgroup cgroup rw,memory\n\ + 30 23 0:29 / /sys/fs/cgroup rw,nosuid,nodev,noexec shared:1 master:2 propagate_from:3 unbindable x-extra:y - cgroup2 cgroup rw\n\ + 31 23 0:30 / /sys/fs/cgroup/cpu rw,nosuid,nodev,noexec - cgroup cgroup rw,cpu\n", + ), + CGroupMounts { + v2: vec![CGroupMount { + root: PathBuf::new(), + mount_point: PathBuf::from("/sys/fs/cgroup"), + }], + v1_memory: vec![CGroupMount { + root: PathBuf::from("kubepods burstable"), + mount_point: PathBuf::from("/sys/fs/cgroup/memory controller"), + }], + } + ); + } + + #[test] + fn test_parse_cgroup_path_keeps_literal_path_bytes() { + assert_eq!( + parse_cgroup_path("11:memory:/kubepods\\040literal/a:b c\n0::/unified\\011path\n"), + CGroupPath { + v2: Some(PathBuf::from("unified\\011path")), + v1_memory: Some(PathBuf::from("kubepods\\040literal/a:b c")), + } + ); + } + #[test] fn test_parse_cgroup_path_v2() { assert_eq!(