use crate::connection_manager::ConnectionHealth;
use std::collections::HashMap;
pub struct TransportHealthEvaluator {
pub suspect_after_ms: i64,
pub stale_after_ms: i64,
}
impl TransportHealthEvaluator {
pub fn new(suspect_after_ms: i64, stale_after_ms: i64) -> Self {
Self {
suspect_after_ms,
stale_after_ms,
}
}
pub fn evaluate(
&self,
last_pong_at_ms: Option<i64>,
first_ping_sent_at_ms: Option<i64>,
now_ms: i64,
) -> ConnectionHealth {
let reference_ms = last_pong_at_ms.or(first_ping_sent_at_ms);
let Some(ref_ms) = reference_ms else {
return ConnectionHealth::Unknown;
};
let silence_ms = now_ms.saturating_sub(ref_ms);
if silence_ms >= self.stale_after_ms {
ConnectionHealth::Stale
} else if silence_ms >= self.suspect_after_ms {
ConnectionHealth::Suspect
} else {
ConnectionHealth::Healthy
}
}
}
pub fn aggregate_peer_health(healths: &HashMap<String, ConnectionHealth>) -> ConnectionHealth {
if healths.is_empty() {
return ConnectionHealth::Unknown;
}
let mut any_healthy = false;
let mut all_stale = true;
for h in healths.values() {
match h {
ConnectionHealth::Healthy => {
any_healthy = true;
all_stale = false;
}
ConnectionHealth::Suspect | ConnectionHealth::Unknown => {
all_stale = false;
}
ConnectionHealth::Stale => {}
}
}
if any_healthy {
ConnectionHealth::Healthy
} else if all_stale {
ConnectionHealth::Stale
} else {
ConnectionHealth::Suspect
}
}
#[cfg(test)]
mod tests {
use super::*;
fn eval(last_pong: Option<i64>, first_ping: Option<i64>, now: i64) -> ConnectionHealth {
TransportHealthEvaluator::new(10_000, 30_000).evaluate(last_pong, first_ping, now)
}
#[test]
fn unknown_when_no_ping_sent() {
assert_eq!(eval(None, None, 1000), ConnectionHealth::Unknown);
}
#[test]
fn healthy_shortly_after_pong() {
assert_eq!(eval(Some(0), None, 5_000), ConnectionHealth::Healthy);
}
#[test]
fn suspect_after_missed_pings() {
assert_eq!(eval(Some(0), None, 15_000), ConnectionHealth::Suspect);
}
#[test]
fn stale_after_threshold() {
assert_eq!(eval(Some(0), None, 35_000), ConnectionHealth::Stale);
}
#[test]
fn recovery_resets_to_healthy() {
assert_eq!(eval(Some(98_000), None, 100_000), ConnectionHealth::Healthy);
}
#[test]
fn aggregate_any_healthy_wins() {
let mut map = HashMap::new();
map.insert("iroh".into(), ConnectionHealth::Stale);
map.insert("webrtc".into(), ConnectionHealth::Healthy);
assert_eq!(aggregate_peer_health(&map), ConnectionHealth::Healthy);
}
#[test]
fn aggregate_all_stale() {
let mut map = HashMap::new();
map.insert("iroh".into(), ConnectionHealth::Stale);
map.insert("webrtc".into(), ConnectionHealth::Stale);
assert_eq!(aggregate_peer_health(&map), ConnectionHealth::Stale);
}
#[test]
fn aggregate_mixed_suspect() {
let mut map = HashMap::new();
map.insert("iroh".into(), ConnectionHealth::Suspect);
map.insert("webrtc".into(), ConnectionHealth::Stale);
assert_eq!(aggregate_peer_health(&map), ConnectionHealth::Suspect);
}
#[test]
fn aggregate_empty_unknown() {
assert_eq!(
aggregate_peer_health(&HashMap::new()),
ConnectionHealth::Unknown
);
}
}