Skip to main content

clawft_kernel/
mesh_load.rs

1//! A small load sample a node attaches to its liveness pongs (fleet P3).
2//!
3//! The responder reads its own load average, core count and available
4//! memory when it answers a ping and puts them in the pong as `load`. The
5//! pinger keeps the latest sample per peer. It is what the peer says about
6//! itself over its verified connection: not signed, so the fleet view labels
7//! it `peer_claimed`. A peer that predates this sends no `load`; a value out
8//! of range is dropped, never clamped into a plausible number.
9
10use serde::{Deserialize, Serialize};
11use serde_json::Value;
12
13/// Load averages above this are refused as nonsense.
14const MAX_LOAD: f64 = 100_000.0;
15/// Core counts above this are refused.
16const MAX_CORES: u64 = 65_536;
17
18/// One node's load at the moment it answered.
19#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
20pub struct LoadSample {
21    /// 1-minute load average.
22    pub load1: f64,
23    /// 5-minute load average.
24    pub load5: f64,
25    /// Logical CPUs, when known.
26    #[serde(default, skip_serializing_if = "Option::is_none")]
27    pub cores: Option<u32>,
28    /// Available memory, bytes, when known.
29    #[serde(default, skip_serializing_if = "Option::is_none")]
30    pub mem_avail: Option<u64>,
31    /// Total memory, bytes, when known.
32    #[serde(default, skip_serializing_if = "Option::is_none")]
33    pub mem_total: Option<u64>,
34}
35
36impl LoadSample {
37    /// This host's load now; `None` where the OS gives no load average.
38    pub fn local() -> Option<Self> {
39        let (load1, load5) = load_avg()?;
40        let (mem_avail, mem_total) = memory();
41        Some(Self {
42            load1,
43            load5,
44            cores: std::thread::available_parallelism().ok().and_then(|n| u32::try_from(n.get()).ok()),
45            mem_avail,
46            mem_total,
47        })
48    }
49
50    /// Parse a peer's `load` object, refusing out-of-range values.
51    pub fn from_json(v: &Value) -> Option<Self> {
52        let load = |k: &str| v.get(k)?.as_f64().filter(|x| x.is_finite() && (0.0..=MAX_LOAD).contains(x));
53        let s = Self {
54            load1: load("load1")?,
55            load5: load("load5")?,
56            cores: match v.get("cores") {
57                None | Some(Value::Null) => None,
58                Some(c) => Some(c.as_u64().filter(|c| (1..=MAX_CORES).contains(c))? as u32),
59            },
60            mem_avail: v.get("mem_avail").and_then(Value::as_u64),
61            mem_total: v.get("mem_total").and_then(Value::as_u64),
62        };
63        // Available memory cannot exceed the total.
64        match (s.mem_avail, s.mem_total) {
65            (Some(a), Some(t)) if a > t => None,
66            _ => Some(s),
67        }
68    }
69
70    /// The `load` object sent in a pong.
71    pub fn to_json(&self) -> Value {
72        serde_json::to_value(self).unwrap_or(Value::Null)
73    }
74}
75
76#[cfg(unix)]
77fn load_avg() -> Option<(f64, f64)> {
78    let mut l = [0f64; 3];
79    // SAFETY: `l` holds 3 doubles and we ask for at most 3.
80    let n = unsafe { libc::getloadavg(l.as_mut_ptr(), 3) };
81    (n >= 2).then_some((l[0], l[1]))
82}
83#[cfg(not(unix))]
84fn load_avg() -> Option<(f64, f64)> {
85    None
86}
87
88#[cfg(target_os = "linux")]
89fn memory() -> (Option<u64>, Option<u64>) {
90    let text = std::fs::read_to_string("/proc/meminfo").unwrap_or_default();
91    let kb = |key: &str| {
92        text.lines()
93            .find_map(|l| l.strip_prefix(key)?.strip_prefix(':'))
94            .and_then(|r| r.split_whitespace().next()?.parse::<u64>().ok())
95            .map(|k| k.saturating_mul(1024))
96    };
97    (kb("MemAvailable"), kb("MemTotal"))
98}
99#[cfg(not(target_os = "linux"))]
100fn memory() -> (Option<u64>, Option<u64>) {
101    (None, None)
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107    use serde_json::json;
108
109    #[test]
110    fn a_sample_round_trips_through_its_json() {
111        let s = LoadSample { load1: 0.5, load5: 1.25, cores: Some(4), mem_avail: Some(1), mem_total: Some(2) };
112        assert_eq!(LoadSample::from_json(&s.to_json()), Some(s));
113        let bare = LoadSample { load1: 0.0, load5: 0.0, cores: None, mem_avail: None, mem_total: None };
114        assert_eq!(bare.to_json(), json!({ "load1": 0.0, "load5": 0.0 }));
115        assert_eq!(LoadSample::from_json(&bare.to_json()), Some(bare));
116    }
117
118    #[test]
119    fn out_of_range_values_are_refused_not_clamped() {
120        for bad in [
121            json!({ "load1": -1.0, "load5": 0.1 }),
122            json!({ "load1": 1e9, "load5": 0.1 }),
123            json!({ "load1": "1", "load5": 0.1 }),
124            json!({ "load5": 0.1 }),
125            json!({ "load1": 0.1, "load5": 0.1, "cores": 0 }),
126            json!({ "load1": 0.1, "load5": 0.1, "mem_avail": 9, "mem_total": 1 }),
127            json!(null),
128        ] {
129            assert_eq!(LoadSample::from_json(&bad), None, "{bad}");
130        }
131    }
132
133    #[cfg(unix)]
134    #[test]
135    fn this_host_reports_a_load() {
136        let s = LoadSample::local().expect("unix has getloadavg");
137        assert!(s.load1 >= 0.0 && s.cores.unwrap_or(1) >= 1);
138    }
139}