Replace cgroups-rs with direct cgroup memory file reads (#10295)

* Replace cgroups-rs with direct cgroup memory file reads

We used cgroups-rs in exactly one place, to read the memory limit and
usage of our own cgroup, so read those files directly instead. Drops 34
crates from the lockfile, including the zbus stack that carries
RUSTSEC-2026-0221.

Also fixes two latent cgroup v1 bugs (the LONG_MAX unlimited sentinel
reported ~9 EB of total memory, an unreadable limit file reported 0
bytes) and the hierarchy mix-up on hybrid hosts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Decline cgroup memory reporting when the usage read fails

Reporting a usage of 0 made available_memory_bytes claim the whole cgroup
limit as free. Fall back to sysinfo when the usage file cannot be read at
init, and keep the last known value on a failed refresh.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Keep the last known memory limit when its read fails

A transient read failure cleared the cached limit and silently fell back
to host memory while the process was still capped, the same direction of
over-reporting as the usage read. Both now keep their last known value,
and a limit lifted at runtime still clears.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Treat a malformed memory limit as an error, not as unlimited

Parse failures returned Ok(None), so garbage in the limit file cleared a
valid cached limit on refresh and read as unlimited at init. Reserve
Ok(None) for "max" and the v1 sentinel, and report anything else as
InvalidData so the last known limit survives.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Arnaud Gourlay
2026-08-24 15:08:00 +02:00
committed by GitHub
co-authored by Claude Opus 5
parent 2067eb3398
commit 5b338115f2
3 changed files with 250 additions and 460 deletions
Generated
-424
View File
@@ -612,126 +612,6 @@ dependencies = [
"serde_json",
]
[[package]]
name = "async-broadcast"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532"
dependencies = [
"event-listener",
"event-listener-strategy",
"futures-core",
"pin-project-lite",
]
[[package]]
name = "async-channel"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"
dependencies = [
"concurrent-queue",
"event-listener-strategy",
"futures-core",
"pin-project-lite",
]
[[package]]
name = "async-executor"
version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a"
dependencies = [
"async-task",
"concurrent-queue",
"fastrand",
"futures-lite",
"pin-project-lite",
"slab",
]
[[package]]
name = "async-io"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc"
dependencies = [
"autocfg",
"cfg-if",
"concurrent-queue",
"futures-io",
"futures-lite",
"parking",
"polling",
"rustix 1.1.4",
"slab",
"windows-sys 0.61.2",
]
[[package]]
name = "async-lock"
version = "3.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"
dependencies = [
"event-listener",
"event-listener-strategy",
"pin-project-lite",
]
[[package]]
name = "async-process"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75"
dependencies = [
"async-channel",
"async-io",
"async-lock",
"async-signal",
"async-task",
"blocking",
"cfg-if",
"event-listener",
"futures-lite",
"rustix 1.1.4",
]
[[package]]
name = "async-recursion"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "async-signal"
version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c"
dependencies = [
"async-io",
"async-lock",
"atomic-waker",
"cfg-if",
"futures-core",
"futures-io",
"rustix 1.1.4",
"signal-hook-registry",
"slab",
"windows-sys 0.61.2",
]
[[package]]
name = "async-task"
version = "4.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
[[package]]
name = "async-trait"
version = "0.1.92"
@@ -923,12 +803,6 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "222fb4925a15bea6a68075021910e03d6aa2d04951d71ff1d956190a551d738f"
[[package]]
name = "bit-vec"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
[[package]]
name = "bitflags"
version = "1.3.2"
@@ -1039,19 +913,6 @@ dependencies = [
"objc2",
]
[[package]]
name = "blocking"
version = "1.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21"
dependencies = [
"async-channel",
"async-task",
"futures-io",
"futures-lite",
"piper",
]
[[package]]
name = "bm25"
version = "0.1.0"
@@ -1253,20 +1114,6 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "cgroups-rs"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25ae79ba89081d30804e3312bb1163ab82cc8dca0a1b16275e55fb19fce4e89b"
dependencies = [
"bit-vec",
"libc",
"log",
"nix 0.25.1",
"thiserror 1.0.69",
"zbus",
]
[[package]]
name = "chacha20"
version = "0.10.0"
@@ -1557,15 +1404,6 @@ dependencies = [
"zerocopy",
]
[[package]]
name = "concurrent-queue"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "config"
version = "0.15.25"
@@ -2347,33 +2185,6 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "endi"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099"
[[package]]
name = "enumflags2"
version = "0.7.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef"
dependencies = [
"enumflags2_derive",
"serde",
]
[[package]]
name = "enumflags2_derive"
version = "0.7.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "env_filter"
version = "2.0.0"
@@ -2463,27 +2274,6 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "event-listener"
version = "5.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
dependencies = [
"concurrent-queue",
"parking",
"pin-project-lite",
]
[[package]]
name = "event-listener-strategy"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
dependencies = [
"event-listener",
"pin-project-lite",
]
[[package]]
name = "faccess"
version = "0.2.4"
@@ -2872,19 +2662,6 @@ version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed"
[[package]]
name = "futures-lite"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad"
dependencies = [
"fastrand",
"futures-core",
"futures-io",
"parking",
"pin-project-lite",
]
[[package]]
name = "futures-macro"
version = "0.3.34"
@@ -4550,18 +4327,6 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab250442c86f1850815b5d268639dff018c0627022bc1940eb2d642ca1ce12f0"
[[package]]
name = "nix"
version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4"
dependencies = [
"autocfg",
"bitflags 1.3.2",
"cfg-if",
"libc",
]
[[package]]
name = "nix"
version = "0.26.4"
@@ -4916,16 +4681,6 @@ dependencies = [
"serde",
]
[[package]]
name = "ordered-stream"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50"
dependencies = [
"futures-core",
"pin-project-lite",
]
[[package]]
name = "p256"
version = "0.13.2"
@@ -4960,12 +4715,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "parking"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
[[package]]
name = "parking_lot"
version = "0.12.5"
@@ -5199,17 +4948,6 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "piper"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1"
dependencies = [
"atomic-waker",
"fastrand",
"futures-io",
]
[[package]]
name = "pkcs1"
version = "0.7.5"
@@ -5271,20 +5009,6 @@ dependencies = [
"plotters-backend",
]
[[package]]
name = "polling"
version = "3.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218"
dependencies = [
"cfg-if",
"concurrent-queue",
"hermit-abi",
"pin-project-lite",
"rustix 1.1.4",
"windows-sys 0.61.2",
]
[[package]]
name = "portable-atomic"
version = "1.13.1"
@@ -5419,15 +5143,6 @@ dependencies = [
"elliptic-curve",
]
[[package]]
name = "proc-macro-crate"
version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983"
dependencies = [
"toml_edit",
]
[[package]]
name = "proc-macro-error-attr3"
version = "3.0.2"
@@ -7019,7 +6734,6 @@ dependencies = [
"byteorder",
"bytes",
"cc",
"cgroups-rs",
"charabia",
"chrono",
"clap",
@@ -7223,17 +6937,6 @@ dependencies = [
"thiserror 1.0.69",
]
[[package]]
name = "serde_repr"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "serde_spanned"
version = "1.1.1"
@@ -8214,18 +7917,6 @@ dependencies = [
"serde_core",
]
[[package]]
name = "toml_edit"
version = "0.23.10+spec-1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269"
dependencies = [
"indexmap 2.14.0",
"toml_datetime",
"toml_parser",
"winnow 0.7.15",
]
[[package]]
name = "toml_parser"
version = "1.0.9+spec-1.1.0"
@@ -8569,17 +8260,6 @@ version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
[[package]]
name = "uds_windows"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9"
dependencies = [
"memoffset",
"tempfile",
"winapi",
]
[[package]]
name = "uio-grpc-client"
version = "0.1.0"
@@ -9465,9 +9145,6 @@ name = "winnow"
version = "0.7.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
dependencies = [
"memchr",
]
[[package]]
name = "winnow"
@@ -9640,67 +9317,6 @@ dependencies = [
"synstructure",
]
[[package]]
name = "zbus"
version = "5.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca82f95dbd3943a40a53cfded6c2d0a2ca26192011846a1810c4256ef92c60bc"
dependencies = [
"async-broadcast",
"async-executor",
"async-io",
"async-lock",
"async-process",
"async-recursion",
"async-task",
"async-trait",
"blocking",
"enumflags2",
"event-listener",
"futures-core",
"futures-lite",
"hex",
"libc",
"ordered-stream",
"rustix 1.1.4",
"serde",
"serde_repr",
"tracing",
"uds_windows",
"uuid",
"windows-sys 0.61.2",
"winnow 0.7.15",
"zbus_macros",
"zbus_names",
"zvariant",
]
[[package]]
name = "zbus_macros"
version = "5.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "897e79616e84aac4b2c46e9132a4f63b93105d54fe8c0e8f6bffc21fa8d49222"
dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
"syn 2.0.119",
"zbus_names",
"zvariant",
"zvariant_utils",
]
[[package]]
name = "zbus_names"
version = "4.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f"
dependencies = [
"serde",
"winnow 0.7.15",
"zvariant",
]
[[package]]
name = "zerocopy"
version = "0.8.56"
@@ -9828,43 +9444,3 @@ dependencies = [
"cc",
"pkg-config",
]
[[package]]
name = "zvariant"
version = "5.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5708299b21903bbe348e94729f22c49c55d04720a004aa350f1f9c122fd2540b"
dependencies = [
"endi",
"enumflags2",
"serde",
"winnow 0.7.15",
"zvariant_derive",
"zvariant_utils",
]
[[package]]
name = "zvariant_derive"
version = "5.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b59b012ebe9c46656f9cc08d8da8b4c726510aef12559da3e5f1bf72780752c"
dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
"syn 2.0.119",
"zvariant_utils",
]
[[package]]
name = "zvariant_utils"
version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9"
dependencies = [
"proc-macro2",
"quote",
"serde",
"syn 2.0.119",
"winnow 0.7.15",
]
-1
View File
@@ -124,7 +124,6 @@ roaring = { workspace = true }
duplicate = "2.0.1"
[target.'cfg(target_os = "linux")'.dependencies]
cgroups-rs = "0.5"
procfs = { version = "0.18", default-features = false }
[[bench]]
+250 -35
View File
@@ -75,20 +75,31 @@ impl Mem {
#[cfg(target_os = "linux")]
mod cgroups_mem {
use cgroups_rs::fs::{Cgroup, hierarchies, memory};
use procfs::process::Process;
use std::io;
use std::path::{Path, PathBuf};
use fs_err as fs;
use procfs::process::{MountInfos, Process};
/// Mount point of the unified (v2) hierarchy.
const UNIFIED_MOUNTPOINT: &str = "/sys/fs/cgroup";
/// With no limit set, `memory.limit_in_bytes` (v1) reads back as `LONG_MAX` rounded down to the
/// page size. Treat anything that large as "no limit", rather than a ~9 EB total memory.
const V1_UNLIMITED_THRESHOLD: u64 = i64::MAX as u64 - (1 << 20);
#[derive(Clone, Debug)]
pub struct CgroupsMem {
mem_controller: memory::MemController,
limit_path: PathBuf,
usage_path: PathBuf,
memory_limit_bytes: Option<u64>,
used_memory_bytes: u64,
}
impl CgroupsMem {
pub fn new() -> Option<Self> {
let memory_cgroup_path = match get_current_process_memory_cgroup_path() {
Ok(memory_cgroup_path) => memory_cgroup_path?,
let (dir, is_v2) = match memory_cgroup_dir() {
Ok(dir) => dir?,
Err(err) => {
log::error!(
"Failed to query current process info \
@@ -99,26 +110,55 @@ mod cgroups_mem {
}
};
let cgroup = Cgroup::load(
hierarchies::auto(),
memory_cgroup_path.trim_start_matches('/'),
);
let mut mem = Self {
mem_controller: cgroup.controller_of::<memory::MemController>()?.clone(),
memory_limit_bytes: None,
used_memory_bytes: 0,
let (limit_file, usage_file) = if is_v2 {
("memory.max", "memory.current")
} else {
("memory.limit_in_bytes", "memory.usage_in_bytes")
};
mem.refresh();
let limit_path = dir.join(limit_file);
Some(mem)
let memory_limit_bytes = match read_memory_limit(&limit_path) {
Ok(memory_limit_bytes) => memory_limit_bytes,
// The memory controller is not available in this cgroup
Err(err) if err.kind() == io::ErrorKind::NotFound => return None,
Err(err) => {
log::error!("Failed to read memory limit while initializing CgroupsMem: {err}");
return None;
}
};
let usage_path = dir.join(usage_file);
let used_memory_bytes = match read_memory_usage(&usage_path) {
Ok(used_memory_bytes) => used_memory_bytes,
Err(err) if err.kind() == io::ErrorKind::NotFound => return None,
Err(err) => {
log::error!("Failed to read memory usage while initializing CgroupsMem: {err}");
return None;
}
};
Some(Self {
limit_path,
usage_path,
memory_limit_bytes,
used_memory_bytes,
})
}
/// Failed reads keep the last known value: dropping the limit would silently fall back to
/// host memory, and zeroing the usage would report the whole limit as free.
pub fn refresh(&mut self) {
let stat = self.mem_controller.memory_stat();
self.memory_limit_bytes = stat.limit_in_bytes.try_into().ok();
self.used_memory_bytes = stat.usage_in_bytes;
if let Ok(memory_limit_bytes) = read_memory_limit(&self.limit_path) {
self.memory_limit_bytes = memory_limit_bytes;
}
if let Ok(used_memory_bytes) = read_memory_usage(&self.usage_path) {
self.used_memory_bytes = used_memory_bytes;
}
}
pub fn memory_limit_bytes(&self) -> Option<u64> {
@@ -130,30 +170,205 @@ mod cgroups_mem {
}
}
fn get_current_process_memory_cgroup_path() -> procfs::ProcResult<Option<String>> {
/// Directory holding the memory controller files of the current process, and whether it belongs
/// to the unified (v2) hierarchy.
fn memory_cgroup_dir() -> procfs::ProcResult<Option<(PathBuf, bool)>> {
let process = Process::myself()?;
let cgroups = process.cgroups()?;
for cgroup in cgroups {
if is_cgroup2_unified_mode() {
// TODO: Can a process belong to multiple v2 cgroups!?
let is_v2_cgroup = cgroup.controllers.is_empty()
|| cgroup
.controllers
.iter()
.all(|controller| controller.is_empty());
let dir = process
.cgroups()?
.into_iter()
// The v2 entry is the one with hierarchy ID 0
.find(|cgroup| cgroup.hierarchy == 0)
.map(|cgroup| join_cgroup_path(Path::new(UNIFIED_MOUNTPOINT), &cgroup.pathname));
// TODO: Can a process belong to multiple v1 cgroups, with some of these cgroups having the same controllers (e.g., memory)!?
let is_v1_memory_cgroup = cgroup
.controllers
.iter()
.any(|controller| controller == "memory");
return Ok(dir.map(|dir| (dir, true)));
}
if is_v2_cgroup || is_v1_memory_cgroup {
return Ok(Some(cgroup.pathname));
let Some(mount_point) = v1_memory_mount_point(process.mountinfo()?) else {
return Ok(None);
};
// TODO: Can a process belong to multiple v1 cgroups, with some of these cgroups having the same controllers (e.g., memory)!?
let dir = process
.cgroups()?
.into_iter()
.find(|cgroup| cgroup.controllers.iter().any(|c| c == "memory"))
.map(|cgroup| join_cgroup_path(&mount_point, &cgroup.pathname));
Ok(dir.map(|dir| (dir, false)))
}
fn is_cgroup2_unified_mode() -> bool {
Path::new(UNIFIED_MOUNTPOINT)
.join("cgroup.controllers")
.exists()
}
/// Where the v1 memory controller is mounted.
fn v1_memory_mount_point(mountinfo: MountInfos) -> Option<PathBuf> {
mountinfo
.into_iter()
.find(|mount| mount.fs_type == "cgroup" && mount.super_options.contains_key("memory"))
.map(|mount| mount.mount_point)
}
/// Cgroup pathnames are relative to the mount point of their hierarchy, but start with a `/`.
fn join_cgroup_path(mount_point: &Path, pathname: &str) -> PathBuf {
mount_point.join(pathname.trim_start_matches('/'))
}
/// `None` if no limit is set, either as v2 `max` or as the v1 `LONG_MAX` sentinel.
fn read_memory_limit(path: &Path) -> io::Result<Option<u64>> {
let raw = fs::read_to_string(path)?;
let raw = raw.trim();
if raw == "max" {
return Ok(None);
}
let memory_limit_bytes = raw
.parse::<u64>()
.map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?;
if memory_limit_bytes >= V1_UNLIMITED_THRESHOLD {
return Ok(None);
}
Ok(Some(memory_limit_bytes))
}
fn read_memory_usage(path: &Path) -> io::Result<u64> {
let raw = fs::read_to_string(path)?;
raw.trim()
.parse()
.map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))
}
#[cfg(test)]
mod tests {
use super::*;
fn read_limit(contents: &str) -> Option<u64> {
let dir = tempfile::Builder::new().tempdir().unwrap();
let path = dir.path().join("memory.max");
fs::write(&path, contents).unwrap();
read_memory_limit(&path).unwrap()
}
#[test]
fn memory_limit_parsing() {
assert_eq!(read_limit("1073741824\n"), Some(1073741824));
// v2 reports "max" when unlimited
assert_eq!(read_limit("max\n"), None);
// v1 reports LONG_MAX rounded down to the page size when unlimited
assert_eq!(read_limit("9223372036854771712\n"), None);
assert_eq!(read_limit("9223372036854710272\n"), None);
}
#[test]
fn malformed_memory_limit_is_invalid_data() {
let dir = tempfile::Builder::new().tempdir().unwrap();
let path = dir.path().join("memory.max");
fs::write(&path, "garbage").unwrap();
let err = read_memory_limit(&path).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
}
#[test]
fn v1_memory_mount_point_is_picked_by_super_option() {
let mountinfo = MountInfos(
[
"30 23 0:26 / /sys/fs/cgroup ro,nosuid,nodev,noexec - tmpfs tmpfs ro,mode=755",
"32 30 0:28 / /sys/fs/cgroup/cpu,cpuacct rw,relatime - cgroup cgroup rw,cpu,cpuacct",
"33 30 0:29 / /sys/fs/cgroup/memory rw,relatime - cgroup cgroup rw,memory",
]
.into_iter()
.map(|line| procfs::process::MountInfo::from_line(line).unwrap())
.collect(),
);
assert_eq!(
v1_memory_mount_point(mountinfo),
Some(PathBuf::from("/sys/fs/cgroup/memory")),
);
}
#[test]
fn cgroup_paths_are_joined_onto_the_mount_point() {
let mount_point = Path::new("/sys/fs/cgroup");
// A container with its own cgroup namespace sees the root
assert_eq!(
join_cgroup_path(mount_point, "/"),
Path::new("/sys/fs/cgroup"),
);
assert_eq!(
join_cgroup_path(mount_point, "/user.slice/session-2.scope"),
Path::new("/sys/fs/cgroup/user.slice/session-2.scope"),
);
}
fn cgroup_mem(dir: &Path) -> CgroupsMem {
let limit_path = dir.join("memory.max");
let usage_path = dir.join("memory.current");
fs::write(&limit_path, "1073741824").unwrap();
fs::write(&usage_path, "1024").unwrap();
CgroupsMem {
limit_path,
usage_path,
memory_limit_bytes: Some(1073741824),
used_memory_bytes: 1024,
}
}
Ok(None)
#[test]
fn unreadable_memory_usage_keeps_the_last_known_value() {
let dir = tempfile::Builder::new().tempdir().unwrap();
let mut mem = cgroup_mem(dir.path());
fs::write(&mem.usage_path, "garbage").unwrap();
mem.refresh();
assert_eq!(mem.used_memory_bytes(), 1024);
}
#[test]
fn unreadable_memory_limit_keeps_the_last_known_value() {
let dir = tempfile::Builder::new().tempdir().unwrap();
let mut mem = cgroup_mem(dir.path());
fs::remove_file(&mem.limit_path).unwrap();
mem.refresh();
assert_eq!(mem.memory_limit_bytes(), Some(1073741824));
fs::write(&mem.limit_path, "garbage").unwrap();
mem.refresh();
assert_eq!(mem.memory_limit_bytes(), Some(1073741824));
// A limit lifted at runtime still clears
fs::write(&mem.limit_path, "max").unwrap();
mem.refresh();
assert_eq!(mem.memory_limit_bytes(), None);
}
#[test]
fn missing_memory_limit_file_is_not_found() {
let dir = tempfile::Builder::new().tempdir().unwrap();
let err = read_memory_limit(&dir.path().join("memory.max")).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::NotFound);
let err = read_memory_usage(&dir.path().join("memory.current")).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::NotFound);
}
}
}