use super::spawn::SpawnOutcome;
use std::collections::HashMap;
#[allow(dead_code)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NodeState {
Running(u32),
Failed(String),
Unknown,
}
#[derive(Debug, Default, Clone)]
pub struct NodeRegistry {
pids: HashMap<String, u32>,
last_known_failures: HashMap<String, String>,
}
impl NodeRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn mark_started(&mut self, node: &str, pid: u32) {
self.pids.insert(node.to_string(), pid);
self.last_known_failures.remove(node);
}
pub fn mark_exited(&mut self, node: &str) -> Option<u32> {
self.pids.remove(node)
}
pub fn record_failures(&mut self, failures: &[(String, String)]) {
for (node, err) in failures {
self.last_known_failures.insert(node.clone(), err.clone());
}
self.last_known_failures.retain(|node, _| !self.pids.contains_key(node));
}
pub fn record_spawn_outcome(&mut self, outcome: &SpawnOutcome) {
for (node, pid) in &outcome.pids {
self.mark_started(node, *pid);
}
self.record_failures(&outcome.failures);
}
#[allow(dead_code)]
pub fn is_running(&self, node: &str) -> bool {
self.pids.contains_key(node)
}
#[allow(dead_code)]
pub fn pid(&self, node: &str) -> Option<u32> {
self.pids.get(node).copied()
}
pub fn pids(&self) -> &HashMap<String, u32> {
&self.pids
}
pub fn running_node_names(&self) -> Vec<String> {
self.pids.keys().cloned().collect()
}
pub fn last_known_failures(&self) -> &HashMap<String, String> {
&self.last_known_failures
}
#[allow(dead_code)]
pub fn state(&self, node: &str) -> NodeState {
if let Some(pid) = self.pids.get(node) {
NodeState::Running(*pid)
} else if let Some(err) = self.last_known_failures.get(node) {
NodeState::Failed(err.clone())
} else {
NodeState::Unknown
}
}
#[allow(dead_code)]
pub fn snapshot(&self) -> HashMap<String, NodeState> {
let mut snapshot: HashMap<String, NodeState> = self
.pids
.iter()
.map(|(node, pid)| (node.clone(), NodeState::Running(*pid)))
.collect();
for (node, err) in &self.last_known_failures {
snapshot
.entry(node.clone())
.or_insert_with(|| NodeState::Failed(err.clone()));
}
snapshot
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mark_started_tracks_pid_and_clears_stored_failure() {
let mut registry = NodeRegistry::new();
registry.record_failures(&[("@mecha10/teleop".to_string(), "binary download failed".to_string())]);
registry.mark_started("@mecha10/teleop", 1234);
assert!(registry.is_running("@mecha10/teleop"));
assert_eq!(registry.pid("@mecha10/teleop"), Some(1234));
assert_eq!(registry.last_known_failures().get("@mecha10/teleop"), None);
}
#[test]
fn mark_exited_removes_pid_and_returns_previous_value() {
let mut registry = NodeRegistry::new();
registry.mark_started("@mecha10/teleop", 1234);
let previous = registry.mark_exited("@mecha10/teleop");
assert_eq!(previous, Some(1234));
assert!(!registry.is_running("@mecha10/teleop"));
}
#[test]
fn mark_exited_returns_none_for_a_node_that_was_not_running() {
let mut registry = NodeRegistry::new();
assert_eq!(registry.mark_exited("@mecha10/teleop"), None);
}
#[test]
fn record_failures_stores_new_failures() {
let mut registry = NodeRegistry::new();
registry.record_failures(&[(
"@mecha10/teleop".to_string(),
"binary download failed: HTTP 404".to_string(),
)]);
assert_eq!(
registry.last_known_failures().get("@mecha10/teleop"),
Some(&"binary download failed: HTTP 404".to_string())
);
}
#[test]
fn record_failures_keeps_older_failure_when_node_not_retried() {
let mut registry = NodeRegistry::new();
registry.record_failures(&[(
"@mecha10/teleop".to_string(),
"binary download failed: HTTP 404".to_string(),
)]);
registry.record_failures(&[]);
assert_eq!(
registry.last_known_failures().get("@mecha10/teleop"),
Some(&"binary download failed: HTTP 404".to_string()),
"the original failure reason should survive a call that didn't touch teleop at all"
);
}
#[test]
fn record_failures_overwrites_with_fresher_failure() {
let mut registry = NodeRegistry::new();
registry.record_failures(&[(
"@mecha10/teleop".to_string(),
"binary download failed: HTTP 404".to_string(),
)]);
registry.record_failures(&[("@mecha10/teleop".to_string(), "connection timed out".to_string())]);
assert_eq!(
registry.last_known_failures().get("@mecha10/teleop"),
Some(&"connection timed out".to_string())
);
}
#[test]
fn record_failures_clears_failure_once_node_is_running() {
let mut registry = NodeRegistry::new();
registry.record_failures(&[(
"@mecha10/teleop".to_string(),
"binary download failed: HTTP 404".to_string(),
)]);
registry.mark_started("@mecha10/teleop", 1234);
registry.record_failures(&[]);
assert!(
registry.last_known_failures().get("@mecha10/teleop").is_none(),
"a running node shouldn't keep a stale stored failure"
);
}
#[test]
fn record_spawn_outcome_marks_pids_running_and_records_failures() {
let mut registry = NodeRegistry::new();
let outcome = SpawnOutcome {
pids: HashMap::from([("@mecha10/camera".to_string(), 42)]),
failures: vec![(
"@mecha10/teleop".to_string(),
"binary download failed: HTTP 404".to_string(),
)],
};
registry.record_spawn_outcome(&outcome);
assert_eq!(registry.pid("@mecha10/camera"), Some(42));
assert_eq!(
registry.last_known_failures().get("@mecha10/teleop"),
Some(&"binary download failed: HTTP 404".to_string())
);
}
#[test]
fn state_reports_running_for_a_tracked_pid() {
let mut registry = NodeRegistry::new();
registry.mark_started("@mecha10/camera", 42);
assert_eq!(registry.state("@mecha10/camera"), NodeState::Running(42));
}
#[test]
fn state_reports_failed_for_a_node_with_a_stored_failure() {
let mut registry = NodeRegistry::new();
registry.record_failures(&[("@mecha10/teleop".to_string(), "connection timed out".to_string())]);
assert_eq!(
registry.state("@mecha10/teleop"),
NodeState::Failed("connection timed out".to_string())
);
}
#[test]
fn state_reports_unknown_for_a_node_never_mentioned() {
let registry = NodeRegistry::new();
assert_eq!(registry.state("@mecha10/unknown"), NodeState::Unknown);
}
#[test]
fn snapshot_combines_running_and_failed_nodes() {
let mut registry = NodeRegistry::new();
registry.mark_started("@mecha10/camera", 42);
registry.record_failures(&[("@mecha10/teleop".to_string(), "connection timed out".to_string())]);
let snapshot = registry.snapshot();
assert_eq!(snapshot.get("@mecha10/camera"), Some(&NodeState::Running(42)));
assert_eq!(
snapshot.get("@mecha10/teleop"),
Some(&NodeState::Failed("connection timed out".to_string()))
);
assert_eq!(snapshot.len(), 2);
}
}