use std::sync::Arc;
use std::time::Duration;
use tokio::task::JoinHandle;
use crate::report::metrics::MetricsState;
const CGROUP_CURRENT: &str = "/sys/fs/cgroup/memory.current";
const CGROUP_MAX: &str = "/sys/fs/cgroup/memory.max";
const CGROUP_STAT: &str = "/sys/fs/cgroup/memory.stat";
const POLL_INTERVAL: Duration = Duration::from_secs(1);
pub(crate) const HYSTERESIS_PCT: f64 = 5.0;
fn parse_inactive_file(stat: &str) -> Option<f64> {
stat.lines().find_map(|line| {
line.strip_prefix("inactive_file ")
.and_then(|rest| rest.trim().parse().ok())
})
}
fn parse_ratio(current: &str, max: &str, inactive_file: f64) -> Option<f64> {
let max = max.trim();
if max == "max" {
return None; }
let max: f64 = max.parse().ok()?;
if max <= 0.0 {
return None;
}
let current: f64 = current.trim().parse().ok()?;
Some((current - inactive_file).max(0.0) / max)
}
fn read_cgroup_usage_ratio() -> Option<f64> {
let current = std::fs::read_to_string(CGROUP_CURRENT).ok()?;
let max = std::fs::read_to_string(CGROUP_MAX).ok()?;
let inactive_file = std::fs::read_to_string(CGROUP_STAT)
.ok()
.and_then(|stat| parse_inactive_file(&stat))
.unwrap_or(0.0);
parse_ratio(¤t, &max, inactive_file)
}
fn next_high_water(prev: bool, ratio: f64, high: f64, low: f64) -> bool {
if ratio >= high {
true
} else if ratio < low {
false
} else {
prev
}
}
pub(super) struct WatcherGuard(JoinHandle<()>);
impl Drop for WatcherGuard {
fn drop(&mut self) {
self.0.abort();
}
}
pub(super) fn spawn_if_enabled(metrics: &Arc<MetricsState>, high_pct: u8) -> Option<WatcherGuard> {
if high_pct == 0 {
return None;
}
let metrics = Arc::clone(metrics);
let high = f64::from(high_pct) / 100.0;
let low = (f64::from(high_pct) - HYSTERESIS_PCT) / 100.0;
Some(WatcherGuard(tokio::spawn(async move {
let mut ticker = tokio::time::interval(POLL_INTERVAL);
let mut flag = false;
let mut unreadable_warned = false;
loop {
ticker.tick().await;
if let Some(ratio) = read_cgroup_usage_ratio() {
flag = next_high_water(flag, ratio, high, low);
metrics.set_memory_high_water(flag);
} else {
if flag {
flag = false;
metrics.set_memory_high_water(false);
}
if !unreadable_warned {
tracing::warn!(
"memory_high_water_pct is set but the cgroup v2 memory limit is \
unreadable; the ingest memory guard is inert on this host"
);
unreadable_warned = true;
}
}
}
})))
}
#[cfg(test)]
mod tests {
use super::{next_high_water, parse_inactive_file, parse_ratio};
#[test]
fn parse_ratio_handles_unlimited_and_bad_input() {
assert_eq!(parse_ratio("100", "max", 0.0), None);
assert_eq!(parse_ratio("100", "0", 0.0), None);
assert_eq!(parse_ratio("not-a-number", "100", 0.0), None);
assert_eq!(parse_ratio("100", "not-a-number", 0.0), None);
}
#[test]
fn parse_ratio_computes_fraction_and_trims() {
let r = parse_ratio("209715200\n", " 268435456 \n", 0.0).expect("ratio");
assert!((r - 0.78125).abs() < 1e-6, "got {r}");
}
#[test]
fn parse_ratio_subtracts_reclaimable_page_cache() {
let r = parse_ratio("209715200", "268435456", 125_829_120.0).expect("ratio");
assert!((r - 0.3125).abs() < 1e-6, "got {r}");
}
#[test]
fn parse_ratio_clamps_working_set_at_zero() {
let r = parse_ratio("100", "1000", 500.0).expect("ratio");
assert!((r - 0.0).abs() < f64::EPSILON, "got {r}");
}
#[test]
fn parse_inactive_file_finds_the_line() {
let stat = "anon 52428800\nfile 130023424\ninactive_file 125829120\nactive_file 4194304\n";
assert_eq!(parse_inactive_file(stat), Some(125_829_120.0));
assert_eq!(parse_inactive_file("anon 1\nfile 2\n"), None);
assert_eq!(parse_inactive_file("inactive_file not-a-number\n"), None);
}
#[test]
fn next_high_water_has_hysteresis() {
assert!(!next_high_water(false, 0.70, 0.80, 0.75), "below both: off");
assert!(
!next_high_water(false, 0.78, 0.80, 0.75),
"in band from off: hold off"
);
assert!(
next_high_water(false, 0.82, 0.80, 0.75),
"at/above high: on"
);
assert!(
next_high_water(true, 0.78, 0.80, 0.75),
"in band from on: hold on"
);
assert!(!next_high_water(true, 0.74, 0.80, 0.75), "below low: off");
}
}