use std::time::Duration;
use super::node::Node;
use metrale_bench::hardware::limits::ThermalEnvelope;
pub const RECHECK: Duration = Duration::from_secs(60);
pub const MAX_PARK: Duration = Duration::from_secs(30 * 60);
#[derive(Clone, Debug, PartialEq)]
pub enum Verdict {
Ready,
Park { now_c: f64, throttled: bool },
Blind,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Reading {
pub chassis_c: Option<f64>,
pub throttled: Option<bool>,
}
#[must_use]
pub fn judge(r: Reading, parked: bool, env: &ThermalEnvelope) -> Verdict {
let throttled = r.throttled == Some(true);
let Some(now_c) = r.chassis_c else {
return if throttled {
Verdict::Park {
now_c: f64::NAN,
throttled,
}
} else {
Verdict::Blind
};
};
let hold = throttled
|| if parked {
now_c > env.chassis_resume_c
} else {
now_c >= env.chassis_park_c
};
if hold {
Verdict::Park { now_c, throttled }
} else {
Verdict::Ready
}
}
pub trait Probe: Send + Sync {
fn read(&self, node: &Node) -> Reading;
}
pub struct FleetProbe {
pub metralectl: std::sync::Arc<dyn super::metralectl::Metralectl>,
}
impl Probe for FleetProbe {
fn read(&self, node: &Node) -> Reading {
if node.local {
let s = metrale_bench::hardware::HardwareState::collect();
return Reading {
chassis_c: s.hottest_chassis_c(),
throttled: s.throttle_active.thermal(),
};
}
let blind = Reading {
chassis_c: None,
throttled: None,
};
let Ok(rows) = self.metralectl.nodes(std::slice::from_ref(&node.addr)) else {
return blind;
};
let Some(info) = rows
.iter()
.find(|r| r.node == node.addr)
.and_then(|r| r.info.as_ref())
else {
return blind;
};
let fp = super::node::fingerprint_of(info);
Reading {
chassis_c: fp.hottest_chassis_c,
throttled: fp.thermal_alert,
}
}
}
#[derive(Debug, Default)]
pub struct Gate {
parked_since: Option<std::time::Instant>,
said_blind: bool,
warned_hot: bool,
}
impl Gate {
pub fn may_take(
&mut self,
node: &Node,
probe: &dyn Probe,
envelope: Option<ThermalEnvelope>,
ignore: bool,
say: &dyn Fn(&str),
) -> bool {
let Some(envelope) = envelope else {
return true;
};
let r = probe.read(node);
let park_c = envelope.chassis_park_c;
let resume_c = envelope.chassis_resume_c;
let why = |now_c: f64, throttled: bool| {
if throttled {
format!("the driver reports a thermal slowdown (chassis {now_c:.0} °C)")
} else {
format!("chassis {now_c:.0} °C (park at {park_c:.0}, resume at {resume_c:.0})")
}
};
if ignore {
match judge(r, self.warned_hot, &envelope) {
Verdict::Park { now_c, throttled } => {
if !self.warned_hot {
self.warned_hot = true;
say(&format!(
"WARNING --dangerous-ignore-thermals: {} would be parked — {}; \
continuing on the operator's say-so — its records are still judged \
by the equivalence policy",
node.addr,
why(now_c, throttled)
));
}
}
Verdict::Ready => {
if self.warned_hot {
self.warned_hot = false;
say(&format!(
"{} is back at or below {resume_c:.0} °C",
node.addr
));
}
}
Verdict::Blind => {}
}
return true;
}
match judge(r, self.parked_since.is_some(), &envelope) {
Verdict::Ready => {
if let Some(since) = self.parked_since.take() {
say(&format!(
"cool-down: {} is back at or below {resume_c:.0} °C after {} s; resuming",
node.addr,
since.elapsed().as_secs()
));
}
true
}
Verdict::Blind => {
if !self.said_blind {
self.said_blind = true;
say(&format!(
"cool-down: {} reports no chassis temperature; it is never parked \
(the records' own captures still decide equivalence)",
node.addr
));
}
self.parked_since = None;
true
}
Verdict::Park { now_c, throttled } => {
let since = *self.parked_since.get_or_insert_with(|| {
say(&format!(
"cool-down: {} parked — {}; nothing more until it is at or below \
{resume_c:.0} °C with no throttle — the other boxes keep working",
node.addr,
why(now_c, throttled)
));
std::time::Instant::now()
});
if since.elapsed() >= MAX_PARK {
say(&format!(
"cool-down: {} still {} after {} s parked; resuming anyway — its records \
are judged by the equivalence policy like any other",
node.addr,
why(now_c, throttled),
since.elapsed().as_secs()
));
self.parked_since = None;
return true;
}
false
}
}
}
}
#[cfg(test)]
#[path = "thermal_tests.rs"]
mod thermal_tests;