pub const CEILING_UTILIZATION: f64 = 0.80;
pub const ADMISSION_MARGIN: f64 = 0.05;
#[derive(Clone, Debug, PartialEq)]
pub struct HostResources {
pub valid: bool,
pub mem_total_kb: u64,
pub mem_available_kb: u64,
pub load_one: f64,
pub n_cpus: u32,
pub utilization: f64,
pub headroom: f64,
}
impl HostResources {
pub fn unavailable() -> Self {
HostResources {
valid: false,
mem_total_kb: 0,
mem_available_kb: 0,
load_one: 0.0,
n_cpus: 0,
utilization: 0.0,
headroom: 0.0,
}
}
pub fn has_headroom(&self) -> bool {
self.valid && self.utilization < CEILING_UTILIZATION
}
pub fn has_admission_headroom(&self) -> bool {
self.valid && self.utilization < (CEILING_UTILIZATION - ADMISSION_MARGIN)
}
pub fn advertised_headroom_pct(&self) -> u8 {
if !self.valid {
return 0;
}
(self.headroom * 100.0).round().clamp(0.0, 100.0) as u8
}
pub fn measure() -> Self {
#[cfg(all(not(target_arch = "wasm32"), target_os = "linux"))]
{
Self::measure_linux()
}
#[cfg(not(all(not(target_arch = "wasm32"), target_os = "linux")))]
{
HostResources::unavailable()
}
}
#[cfg(all(not(target_arch = "wasm32"), target_os = "linux"))]
fn measure_linux() -> Self {
let meminfo = match std::fs::read_to_string("/proc/meminfo") {
Ok(s) => s,
Err(_) => return HostResources::unavailable(),
};
let loadavg = match std::fs::read_to_string("/proc/loadavg") {
Ok(s) => s,
Err(_) => return HostResources::unavailable(),
};
let cpuinfo = std::fs::read_to_string("/proc/cpuinfo").unwrap_or_default();
let stat = std::fs::read_to_string("/proc/stat").unwrap_or_default();
from_proc_text(&meminfo, &loadavg, &cpuinfo, &stat)
}
pub fn is_available(&self) -> bool {
self.valid
}
pub(crate) fn from_parts(
mem_total_kb: u64,
mem_available_kb: u64,
load_one: f64,
n_cpus: u32,
) -> Self {
let available = mem_available_kb.min(mem_total_kb);
let mem_fraction = if mem_total_kb > 0 {
1.0 - (available as f64 / mem_total_kb as f64)
} else {
0.0
};
let load_normalized = if n_cpus > 0 {
load_one / n_cpus as f64
} else {
load_one
};
let utilization = mem_fraction.max(load_normalized).clamp(0.0, 1.0);
let headroom = (1.0 - utilization).clamp(0.0, 1.0);
HostResources {
valid: true,
mem_total_kb,
mem_available_kb: available,
load_one,
n_cpus,
utilization,
headroom,
}
}
}
pub fn headroom_pct_sufficient(pct: u8) -> bool {
let min_pct = ((1.0 - CEILING_UTILIZATION) * 100.0).round() as u8;
pct > min_pct
}
pub const ABUNDANT_HEADROOM_PCT: u8 = 50;
pub fn headroom_pct_abundant(pct: u8) -> bool {
pct >= ABUNDANT_HEADROOM_PCT
}
fn from_proc_text(meminfo: &str, loadavg: &str, cpuinfo: &str, stat: &str) -> HostResources {
let n_cpus = parse_cpu_count(cpuinfo).or_else(|| parse_cpu_count_from_stat(stat));
match (parse_meminfo(meminfo), parse_loadavg(loadavg), n_cpus) {
(Some((total, available)), Some(load), Some(cpus)) => {
HostResources::from_parts(total, available, load, cpus)
}
_ => HostResources::unavailable(),
}
}
fn parse_cpu_count(cpuinfo: &str) -> Option<u32> {
let n = cpuinfo
.lines()
.filter(|l| l.trim_start().starts_with("processor"))
.count();
(n > 0).then_some(n as u32)
}
fn parse_cpu_count_from_stat(stat: &str) -> Option<u32> {
let n = stat
.lines()
.filter(|l| {
l.strip_prefix("cpu")
.and_then(|rest| rest.chars().next())
.is_some_and(|c| c.is_ascii_digit())
})
.count();
(n > 0).then_some(n as u32)
}
fn parse_meminfo(text: &str) -> Option<(u64, u64)> {
let mut total = None;
let mut available = None;
for line in text.lines() {
if let Some(rest) = line.strip_prefix("MemTotal:") {
total = parse_leading_kb(rest);
} else if let Some(rest) = line.strip_prefix("MemAvailable:") {
available = parse_leading_kb(rest);
}
}
match (total, available) {
(Some(t), Some(a)) if t > 0 => Some((t, a)),
_ => None,
}
}
fn parse_leading_kb(value: &str) -> Option<u64> {
value.split_whitespace().next()?.parse().ok()
}
fn parse_loadavg(text: &str) -> Option<f64> {
text.split_whitespace().next()?.parse().ok()
}
pub fn sexp_resource_status(node_hex: &str, r: &HostResources) -> String {
format!(
"(resource-status :id \"{}\" :valid {} :mem-total-kb {} :mem-avail-kb {} :load {:.2} :n-cpus {} :util {:.3} :headroom {:.3})",
node_hex,
r.valid,
r.mem_total_kb,
r.mem_available_kb,
r.load_one,
r.n_cpus,
r.utilization,
r.headroom
)
}
#[cfg(test)]
mod tests {
use super::*;
const SAMPLE_MEMINFO: &str = "\
MemTotal: 16384256 kB
MemFree: 2097152 kB
MemAvailable: 8192128 kB
Buffers: 524288 kB
Cached: 4194304 kB
";
const SAMPLE_LOADAVG: &str = "0.75 0.42 0.30 2/512 12345\n";
const SAMPLE_CPUINFO: &str = "\
processor : 0
model name : Test CPU
processor : 1
processor : 2
processor : 3
processor : 4
processor : 5
processor : 6
processor : 7
";
const SAMPLE_STAT: &str = "\
cpu 100 0 50 900 0 0 0 0 0 0
cpu0 25 0 12 225 0 0 0 0 0 0
cpu1 25 0 12 225 0 0 0 0 0 0
cpu2 25 0 13 225 0 0 0 0 0 0
cpu3 25 0 13 225 0 0 0 0 0 0
intr 12345
";
#[test]
fn test_parse_meminfo_sample() {
let (total, available) = parse_meminfo(SAMPLE_MEMINFO).unwrap();
assert_eq!(total, 16_384_256);
assert_eq!(available, 8_192_128);
}
#[test]
fn test_parse_loadavg_sample() {
let load = parse_loadavg(SAMPLE_LOADAVG).unwrap();
assert!((load - 0.75).abs() < 1e-9);
}
#[test]
fn test_parse_cpu_count_sample() {
assert_eq!(parse_cpu_count(SAMPLE_CPUINFO), Some(8));
assert_eq!(parse_cpu_count("model name : x\n"), None);
}
#[test]
fn test_parse_cpu_count_from_stat_sample() {
assert_eq!(parse_cpu_count_from_stat(SAMPLE_STAT), Some(4));
assert_eq!(parse_cpu_count_from_stat("cpu 1 2 3\nintr 0\n"), None);
}
#[test]
fn test_binding_constraint_picks_larger() {
let mem_bound = HostResources::from_parts(1000, 100, 0.1, 8);
assert!((mem_bound.utilization - 0.9).abs() < 1e-6);
let load_bound = HostResources::from_parts(1000, 900, 6.0, 8);
assert!((load_bound.utilization - 0.75).abs() < 1e-6);
assert!(mem_bound.utilization > 0.1); assert!(load_bound.utilization > 0.1); }
#[test]
fn test_from_proc_text_derives_headroom() {
let r = from_proc_text(SAMPLE_MEMINFO, SAMPLE_LOADAVG, SAMPLE_CPUINFO, SAMPLE_STAT);
assert!(r.valid);
assert_eq!(r.mem_total_kb, 16_384_256);
assert_eq!(r.mem_available_kb, 8_192_128);
assert!((r.load_one - 0.75).abs() < 1e-9);
assert_eq!(r.n_cpus, 8);
assert!((r.utilization - 0.5).abs() < 1e-6);
assert!((r.headroom - 0.5).abs() < 1e-6);
}
#[test]
fn test_cpu_count_falls_back_to_stat() {
let r = from_proc_text(SAMPLE_MEMINFO, SAMPLE_LOADAVG, "", SAMPLE_STAT);
assert!(r.valid);
assert_eq!(r.n_cpus, 4);
}
#[test]
fn test_utilization_and_headroom_in_range() {
let r = from_proc_text(SAMPLE_MEMINFO, SAMPLE_LOADAVG, SAMPLE_CPUINFO, SAMPLE_STAT);
assert!((0.0..=1.0).contains(&r.utilization));
assert!((0.0..=1.0).contains(&r.headroom));
assert!((r.utilization + r.headroom - 1.0).abs() < 1e-6);
}
#[test]
fn test_available_clamped_to_total() {
let r = HostResources::from_parts(1000, 5000, 0.0, 4);
assert_eq!(r.mem_available_kb, 1000);
assert!((r.headroom - 1.0).abs() < 1e-9);
assert!((r.utilization - 0.0).abs() < 1e-9);
}
#[test]
fn test_has_headroom_available_under_ceiling_is_true() {
let r = HostResources::from_parts(1000, 500, 0.0, 4);
assert!(r.has_headroom());
}
#[test]
fn test_has_headroom_available_over_ceiling_is_false() {
let over = HostResources::from_parts(1000, 100, 0.0, 4);
assert!(!over.has_headroom());
let at = HostResources::from_parts(1000, 200, 0.0, 4);
assert!((at.utilization - CEILING_UTILIZATION).abs() < 1e-9);
assert!(!at.has_headroom());
}
#[test]
fn test_has_headroom_unavailable_fails_closed() {
assert!(!HostResources::unavailable().has_headroom());
}
#[test]
fn test_has_admission_headroom_is_stricter_than_has_headroom() {
let low = HostResources::from_parts(1000, 500, 0.0, 4); assert!(low.has_headroom());
assert!(low.has_admission_headroom());
let near = HostResources::from_parts(1000, 220, 0.0, 4); assert!(near.has_headroom(), "78% is under the 80% ceiling");
assert!(
!near.has_admission_headroom(),
"78% is within the 5% admission margin → refuse inbound"
);
let over = HostResources::from_parts(1000, 50, 0.0, 4); assert!(!over.has_headroom());
assert!(!over.has_admission_headroom());
assert!(!HostResources::unavailable().has_admission_headroom());
}
#[test]
fn test_advertised_headroom_pct() {
let r = HostResources::from_parts(1000, 500, 0.0, 4);
assert_eq!(r.advertised_headroom_pct(), 50);
assert_eq!(HostResources::unavailable().advertised_headroom_pct(), 0);
}
#[test]
fn test_headroom_pct_sufficient_tracks_ceiling() {
assert!(!headroom_pct_sufficient(20)); assert!(headroom_pct_sufficient(21));
assert!(headroom_pct_sufficient(50));
assert!(!headroom_pct_sufficient(0)); assert!(!headroom_pct_sufficient(10));
let healthy = HostResources::from_parts(1000, 500, 0.0, 4);
assert!(healthy.has_headroom());
assert!(headroom_pct_sufficient(healthy.advertised_headroom_pct()));
}
#[test]
fn test_headroom_pct_abundant_is_above_sufficiency() {
assert!(!headroom_pct_abundant(49));
assert!(headroom_pct_abundant(ABUNDANT_HEADROOM_PCT)); assert!(headroom_pct_abundant(80));
assert!(headroom_pct_sufficient(30));
assert!(!headroom_pct_abundant(30));
for pct in ABUNDANT_HEADROOM_PCT..=100 {
assert!(headroom_pct_sufficient(pct), "abundant {pct} must be sufficient");
}
}
#[test]
fn test_malformed_proc_is_unavailable() {
let bad = "MemTotal: 100 kB\nMemFree: 50 kB\n";
let r = from_proc_text(bad, SAMPLE_LOADAVG, SAMPLE_CPUINFO, SAMPLE_STAT);
assert!(!r.valid);
assert!(!r.is_available());
let no_cpu = from_proc_text(SAMPLE_MEMINFO, SAMPLE_LOADAVG, "", "");
assert!(!no_cpu.valid);
}
#[test]
fn test_unavailable_reading_reports_as_such() {
let r = HostResources::unavailable();
assert!(!r.valid);
assert!(!r.is_available());
assert_eq!(r.mem_total_kb, 0);
assert_eq!(r.mem_available_kb, 0);
assert_eq!(r.n_cpus, 0);
assert_eq!(r.utilization, 0.0);
assert_eq!(r.headroom, 0.0);
}
#[test]
fn test_measure_returns_valid_or_cleanly_unavailable() {
let r = HostResources::measure();
if r.is_available() {
assert!(r.mem_total_kb > 0);
assert!(r.mem_available_kb <= r.mem_total_kb);
assert!(r.n_cpus > 0);
assert!((0.0..=1.0).contains(&r.utilization));
assert!((0.0..=1.0).contains(&r.headroom));
} else {
assert_eq!(r, HostResources::unavailable());
}
}
#[test]
fn test_sexp_resource_status() {
let r = from_proc_text(SAMPLE_MEMINFO, SAMPLE_LOADAVG, SAMPLE_CPUINFO, SAMPLE_STAT);
let s = sexp_resource_status("aabbccdd", &r);
assert!(s.contains("resource-status"));
assert!(s.contains(":id \"aabbccdd\""));
assert!(s.contains(":valid true"));
assert!(s.contains(":mem-total-kb 16384256"));
assert!(s.contains(":n-cpus 8"));
assert!(s.contains(":headroom 0.500"));
}
}