use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum PressureLevel {
None,
Warning,
Critical,
}
#[derive(Debug, Clone)]
pub struct MemoryState {
pub usage_bytes: u64,
pub limit_bytes: u64,
#[allow(dead_code)]
pub under_pressure: bool,
pub pressure_level: PressureLevel,
pub usage_ratio: f64,
}
struct MemPaths {
cgroup_v2_max: PathBuf,
cgroup_v2_current: PathBuf,
cgroup_v1_limit: PathBuf,
cgroup_v1_usage: PathBuf,
proc_statm: PathBuf,
}
impl Default for MemPaths {
fn default() -> Self {
Self {
cgroup_v2_max: PathBuf::from("/sys/fs/cgroup/memory.max"),
cgroup_v2_current: PathBuf::from("/sys/fs/cgroup/memory.current"),
cgroup_v1_limit: PathBuf::from("/sys/fs/cgroup/memory/memory.limit_in_bytes"),
cgroup_v1_usage: PathBuf::from("/sys/fs/cgroup/memory/memory.usage_in_bytes"),
proc_statm: PathBuf::from("/proc/self/statm"),
}
}
}
pub struct MemoryMonitor {
limit_bytes: u64,
pressure_threshold: f64,
pressure_exit_threshold: f64,
critical_threshold: f64,
paths: MemPaths,
was_under_pressure: std::cell::Cell<bool>,
}
impl Default for MemoryMonitor {
fn default() -> Self {
Self::new()
}
}
impl MemoryMonitor {
pub fn new() -> Self {
Self::with_paths(MemPaths::default())
}
fn with_paths(paths: MemPaths) -> Self {
let limit_bytes = Self::detect_limit(&paths);
let pressure_threshold: f64 = std::env::var("QRUSTY_MEMORY_PRESSURE_THRESHOLD")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(0.80);
let pressure_exit_threshold: f64 = std::env::var("QRUSTY_MEMORY_PRESSURE_EXIT_THRESHOLD")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(0.70);
let critical_threshold: f64 = std::env::var("QRUSTY_MEMORY_CRITICAL_THRESHOLD")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(0.90);
if limit_bytes > 0 {
tracing::info!(
"Memory monitor: limit={} MB, pressure_threshold={:.0}%, exit={:.0}%, critical={:.0}%",
limit_bytes / (1024 * 1024),
pressure_threshold * 100.0,
pressure_exit_threshold * 100.0,
critical_threshold * 100.0
);
} else {
tracing::info!(
"Memory monitor: no cgroup limit detected, pressure detection disabled. \
Set QRUSTY_MEMORY_LIMIT_MB to enable."
);
}
Self {
limit_bytes,
pressure_threshold,
pressure_exit_threshold,
critical_threshold,
paths,
was_under_pressure: std::cell::Cell::new(false),
}
}
fn detect_limit(paths: &MemPaths) -> u64 {
Self::detect_limit_with_override(paths, std::env::var("QRUSTY_MEMORY_LIMIT_MB").ok())
}
fn detect_limit_with_override(paths: &MemPaths, env_override_mb: Option<String>) -> u64 {
if let Some(mb) = env_override_mb.and_then(|v| v.parse::<u64>().ok()) {
return mb * 1024 * 1024;
}
if let Some(bytes) = read_cgroup_v2_limit(&paths.cgroup_v2_max) {
return bytes;
}
if let Some(bytes) = read_cgroup_v1_limit(&paths.cgroup_v1_limit) {
return bytes;
}
0
}
fn read_usage(&self) -> u64 {
if let Some(bytes) = read_bytes_from_file(&self.paths.cgroup_v2_current) {
return bytes;
}
if let Some(bytes) = read_bytes_from_file(&self.paths.cgroup_v1_usage) {
return bytes;
}
if let Ok(content) = std::fs::read_to_string(&self.paths.proc_statm) {
if let Some(rss_pages) = content.split_whitespace().nth(1) {
if let Ok(pages) = rss_pages.parse::<u64>() {
return pages * PAGE_SIZE;
}
}
}
0
}
pub fn state(&self) -> MemoryState {
let usage_bytes = self.read_usage();
let usage_ratio = if self.limit_bytes > 0 {
usage_bytes as f64 / self.limit_bytes as f64
} else {
0.0
};
let under_pressure = if self.limit_bytes == 0 {
false
} else if self.was_under_pressure.get() {
usage_ratio > self.pressure_exit_threshold
} else {
usage_ratio > self.pressure_threshold
};
self.was_under_pressure.set(under_pressure);
let pressure_level = if self.limit_bytes == 0 || !under_pressure {
PressureLevel::None
} else if usage_ratio > self.critical_threshold {
PressureLevel::Critical
} else {
PressureLevel::Warning
};
MemoryState {
usage_bytes,
limit_bytes: self.limit_bytes,
under_pressure,
pressure_level,
usage_ratio,
}
}
#[allow(dead_code)]
pub fn limit_bytes(&self) -> u64 {
self.limit_bytes
}
pub fn usage_snapshot() -> (u64, u64) {
let paths = MemPaths::default();
let limit = Self::detect_limit(&paths);
let usage = read_bytes_from_file(&paths.cgroup_v2_current)
.or_else(|| read_bytes_from_file(&paths.cgroup_v1_usage))
.unwrap_or_else(|| {
std::fs::read_to_string(&paths.proc_statm)
.ok()
.and_then(|c| c.split_whitespace().nth(1)?.parse::<u64>().ok())
.map(|pages| pages * PAGE_SIZE)
.unwrap_or(0)
});
(usage, limit)
}
}
const PAGE_SIZE: u64 = 4096;
fn read_bytes_from_file(path: &Path) -> Option<u64> {
std::fs::read_to_string(path)
.ok()?
.trim()
.parse::<u64>()
.ok()
}
fn read_cgroup_v2_limit(path: &Path) -> Option<u64> {
let content = std::fs::read_to_string(path).ok()?;
let trimmed = content.trim();
if trimmed == "max" {
return None;
}
trimmed.parse::<u64>().ok()
}
fn read_cgroup_v1_limit(path: &Path) -> Option<u64> {
let bytes = read_bytes_from_file(path)?;
if bytes >= u64::MAX / 2 {
return None; }
Some(bytes)
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
fn mock_paths(dir: &Path) -> MemPaths {
MemPaths {
cgroup_v2_max: dir.join("memory.max"),
cgroup_v2_current: dir.join("memory.current"),
cgroup_v1_limit: dir.join("memory.limit_in_bytes"),
cgroup_v1_usage: dir.join("memory.usage_in_bytes"),
proc_statm: dir.join("statm"),
}
}
#[test]
fn test_memory_monitor_new_does_not_panic() {
let monitor = MemoryMonitor::new();
let state = monitor.state();
assert!(state.usage_bytes > 0 || state.limit_bytes == 0);
}
#[test]
fn test_pressure_with_explicit_limit() {
let dir = TempDir::new().unwrap();
let paths = mock_paths(dir.path());
fs::write(&paths.cgroup_v2_max, "1048576\n").unwrap(); fs::write(&paths.cgroup_v2_current, "2097152\n").unwrap();
let monitor = MemoryMonitor::with_paths(paths);
let state = monitor.state();
assert!(state.under_pressure);
assert_eq!(state.limit_bytes, 1_048_576);
assert_eq!(state.usage_bytes, 2_097_152);
}
#[test]
fn test_cgroup_v2_limit_detection() {
let dir = TempDir::new().unwrap();
let paths = mock_paths(dir.path());
fs::write(&paths.cgroup_v2_max, "1073741824\n").unwrap();
std::env::remove_var("QRUSTY_MEMORY_LIMIT_MB");
let limit = MemoryMonitor::detect_limit(&paths);
assert_eq!(limit, 1073741824); }
#[test]
fn test_cgroup_v2_unlimited_returns_zero() {
let dir = TempDir::new().unwrap();
let paths = mock_paths(dir.path());
fs::write(&paths.cgroup_v2_max, "max\n").unwrap();
std::env::remove_var("QRUSTY_MEMORY_LIMIT_MB");
let limit = MemoryMonitor::detect_limit(&paths);
assert_eq!(limit, 0);
}
#[test]
fn test_cgroup_v1_limit_detection() {
let dir = TempDir::new().unwrap();
let paths = mock_paths(dir.path());
fs::write(&paths.cgroup_v1_limit, "2147483648\n").unwrap();
std::env::remove_var("QRUSTY_MEMORY_LIMIT_MB");
let limit = MemoryMonitor::detect_limit(&paths);
assert_eq!(limit, 2147483648); }
#[test]
fn test_cgroup_v1_unlimited_returns_zero() {
let dir = TempDir::new().unwrap();
let paths = mock_paths(dir.path());
fs::write(&paths.cgroup_v1_limit, "9223372036854775808\n").unwrap();
std::env::remove_var("QRUSTY_MEMORY_LIMIT_MB");
let limit = MemoryMonitor::detect_limit(&paths);
assert_eq!(limit, 0);
}
#[test]
fn test_cgroup_v2_usage_reading() {
let dir = TempDir::new().unwrap();
let paths = mock_paths(dir.path());
fs::write(&paths.cgroup_v2_max, "4294967296\n").unwrap(); fs::write(&paths.cgroup_v2_current, "1073741824\n").unwrap();
std::env::remove_var("QRUSTY_MEMORY_LIMIT_MB");
let monitor = MemoryMonitor::with_paths(paths);
let state = monitor.state();
assert_eq!(state.usage_bytes, 1073741824);
assert_eq!(state.limit_bytes, 4294967296);
assert!(!state.under_pressure); }
#[test]
fn test_pressure_detected_at_threshold() {
let dir = TempDir::new().unwrap();
let paths = mock_paths(dir.path());
fs::write(&paths.cgroup_v2_max, "1000\n").unwrap();
fs::write(&paths.cgroup_v2_current, "900\n").unwrap();
std::env::remove_var("QRUSTY_MEMORY_LIMIT_MB");
let monitor = MemoryMonitor::with_paths(paths);
let state = monitor.state();
assert!(state.under_pressure);
}
#[test]
fn test_no_pressure_below_threshold() {
let dir = TempDir::new().unwrap();
let paths = mock_paths(dir.path());
fs::write(&paths.cgroup_v2_max, "1000\n").unwrap();
fs::write(&paths.cgroup_v2_current, "500\n").unwrap();
std::env::remove_var("QRUSTY_MEMORY_LIMIT_MB");
let monitor = MemoryMonitor::with_paths(paths);
let state = monitor.state();
assert!(!state.under_pressure);
}
#[test]
fn test_cgroup_v1_usage_fallback() {
let dir = TempDir::new().unwrap();
let paths = mock_paths(dir.path());
fs::write(&paths.cgroup_v1_limit, "2000000000\n").unwrap();
fs::write(&paths.cgroup_v1_usage, "500000000\n").unwrap();
std::env::remove_var("QRUSTY_MEMORY_LIMIT_MB");
let monitor = MemoryMonitor::with_paths(paths);
let state = monitor.state();
assert_eq!(state.usage_bytes, 500000000);
assert_eq!(state.limit_bytes, 2000000000);
assert!(!state.under_pressure);
}
#[test]
fn test_proc_statm_fallback() {
let dir = TempDir::new().unwrap();
let paths = mock_paths(dir.path());
fs::write(&paths.proc_statm, "1000 500 100 50 0 300 0\n").unwrap();
std::env::remove_var("QRUSTY_MEMORY_LIMIT_MB");
let monitor = MemoryMonitor::with_paths(paths);
let state = monitor.state();
assert_eq!(state.usage_bytes, 500 * PAGE_SIZE); assert_eq!(state.limit_bytes, 0); assert!(!state.under_pressure); }
#[test]
fn test_no_files_returns_zero_usage() {
let dir = TempDir::new().unwrap();
let paths = mock_paths(dir.path());
std::env::remove_var("QRUSTY_MEMORY_LIMIT_MB");
let monitor = MemoryMonitor::with_paths(paths);
let state = monitor.state();
assert_eq!(state.usage_bytes, 0);
assert_eq!(state.limit_bytes, 0);
assert!(!state.under_pressure);
}
#[test]
fn test_env_override_takes_precedence() {
let dir = TempDir::new().unwrap();
let paths = mock_paths(dir.path());
fs::write(&paths.cgroup_v2_max, "999999\n").unwrap();
let limit = MemoryMonitor::detect_limit_with_override(&paths, Some("42".to_string()));
assert_eq!(limit, 42 * 1024 * 1024);
}
#[test]
fn test_limit_bytes_accessor() {
let dir = TempDir::new().unwrap();
let paths = mock_paths(dir.path());
fs::write(&paths.cgroup_v2_max, "5000\n").unwrap();
std::env::remove_var("QRUSTY_MEMORY_LIMIT_MB");
let monitor = MemoryMonitor::with_paths(paths);
assert_eq!(monitor.limit_bytes(), 5000);
}
#[test]
fn test_default_thresholds() {
let dir = TempDir::new().unwrap();
let paths = mock_paths(dir.path());
fs::write(&paths.cgroup_v2_max, "1000\n").unwrap();
fs::write(&paths.cgroup_v2_current, "1\n").unwrap();
std::env::remove_var("QRUSTY_MEMORY_LIMIT_MB");
std::env::remove_var("QRUSTY_MEMORY_PRESSURE_THRESHOLD");
std::env::remove_var("QRUSTY_MEMORY_PRESSURE_EXIT_THRESHOLD");
std::env::remove_var("QRUSTY_MEMORY_CRITICAL_THRESHOLD");
let monitor = MemoryMonitor::with_paths(paths);
assert_eq!(
monitor.pressure_threshold, 0.80,
"enter threshold should default to 0.80"
);
assert_eq!(
monitor.pressure_exit_threshold, 0.70,
"exit threshold should default to 0.70"
);
assert_eq!(
monitor.critical_threshold, 0.90,
"critical threshold should default to 0.90"
);
}
#[test]
fn test_hysteresis_exits_at_70_percent() {
let dir = TempDir::new().unwrap();
let p = dir.path();
let paths = mock_paths(p);
fs::write(&paths.cgroup_v2_max, "1000\n").unwrap();
std::env::remove_var("QRUSTY_MEMORY_LIMIT_MB");
std::env::remove_var("QRUSTY_MEMORY_PRESSURE_THRESHOLD");
std::env::remove_var("QRUSTY_MEMORY_PRESSURE_EXIT_THRESHOLD");
std::env::remove_var("QRUSTY_MEMORY_CRITICAL_THRESHOLD");
fs::write(p.join("memory.current"), "850\n").unwrap();
let monitor = MemoryMonitor::with_paths(paths);
let state = monitor.state();
assert!(state.under_pressure, "should enter pressure at 85%");
fs::write(p.join("memory.current"), "720\n").unwrap();
let state = monitor.state();
assert!(
state.under_pressure,
"should still be under pressure at 72% (exit is 70%)"
);
fs::write(p.join("memory.current"), "690\n").unwrap();
let state = monitor.state();
assert!(
!state.under_pressure,
"should exit pressure at 69% (below 70% exit)"
);
}
#[test]
fn test_no_pressure_at_79_percent() {
let dir = TempDir::new().unwrap();
let paths = mock_paths(dir.path());
fs::write(&paths.cgroup_v2_max, "1000\n").unwrap();
fs::write(&paths.cgroup_v2_current, "790\n").unwrap();
std::env::remove_var("QRUSTY_MEMORY_LIMIT_MB");
std::env::remove_var("QRUSTY_MEMORY_PRESSURE_THRESHOLD");
let monitor = MemoryMonitor::with_paths(paths);
let state = monitor.state();
assert!(
!state.under_pressure,
"79% should not trigger pressure with 80% threshold"
);
}
#[test]
fn test_pressure_at_81_percent() {
let dir = TempDir::new().unwrap();
let paths = mock_paths(dir.path());
fs::write(&paths.cgroup_v2_max, "1000\n").unwrap();
fs::write(&paths.cgroup_v2_current, "810\n").unwrap();
std::env::remove_var("QRUSTY_MEMORY_LIMIT_MB");
std::env::remove_var("QRUSTY_MEMORY_PRESSURE_THRESHOLD");
let monitor = MemoryMonitor::with_paths(paths);
let state = monitor.state();
assert!(
state.under_pressure,
"81% should trigger pressure with 80% threshold"
);
}
}