mecha10-runtime 0.8.0

Runtime supervisor for Mecha10 nodes - launching, monitoring, and lifecycle management
Documentation
//! Health checking infrastructure

use mecha10_core::rwlock_collections::RwLockMapExt;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::RwLock;

#[derive(Clone, Debug, PartialEq)]
pub enum HealthStatus {
    Healthy,

    Degraded { reason: String },

    Unhealthy { reason: String },
}

impl HealthStatus {
    /// Check if status is healthy
    pub fn is_healthy(&self) -> bool {
        matches!(self, HealthStatus::Healthy)
    }

    /// Check if status is degraded
    pub fn is_degraded(&self) -> bool {
        matches!(self, HealthStatus::Degraded { .. })
    }

    /// Check if status is unhealthy
    pub fn is_unhealthy(&self) -> bool {
        matches!(self, HealthStatus::Unhealthy { .. })
    }

    /// Get reason if not healthy
    pub fn reason(&self) -> Option<&str> {
        match self {
            HealthStatus::Healthy => None,
            HealthStatus::Degraded { reason } => Some(reason),
            HealthStatus::Unhealthy { reason } => Some(reason),
        }
    }
}

pub struct HealthChecker {
    checks: Arc<RwLock<HashMap<String, HealthStatus>>>,
    interval: Duration,
}

impl HealthChecker {
    /// Create a new health checker with specified interval
    pub fn new(interval: Duration) -> Self {
        Self {
            checks: Arc::new(RwLock::new(HashMap::new())),
            interval,
        }
    }

    /// Register a node for health checking
    pub async fn register(&self, name: String, initial_status: HealthStatus) {
        let mut checks = self.checks.write().await;
        checks.insert(name, initial_status);
    }

    /// Update health status for a node
    pub async fn update(&self, name: &str, status: HealthStatus) {
        let mut checks = self.checks.write().await;
        checks.insert(name.to_string(), status);
    }

    /// Get health status for a specific node
    pub async fn check_one(&self, name: &str) -> Option<HealthStatus> {
        self.checks.cloned_get(&name.to_string()).await
    }

    /// Get health status for all nodes
    pub async fn check_all(&self) -> HashMap<String, HealthStatus> {
        let checks = self.checks.read().await;
        checks.clone()
    }

    /// Get the check interval
    pub fn interval(&self) -> Duration {
        self.interval
    }

    /// Remove a node from health checking
    pub async fn unregister(&self, name: &str) {
        let mut checks = self.checks.write().await;
        checks.remove(name);
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn check_one_returns_none_before_registration() {
        let checker = HealthChecker::new(Duration::from_secs(1));

        assert_eq!(checker.check_one("camera").await, None);
    }

    #[tokio::test]
    async fn check_one_returns_the_registered_status() {
        let checker = HealthChecker::new(Duration::from_secs(1));

        checker.register("camera".to_string(), HealthStatus::Healthy).await;

        assert_eq!(checker.check_one("camera").await, Some(HealthStatus::Healthy));
    }

    #[tokio::test]
    async fn update_overwrites_the_status_seen_by_check_one_and_check_all() {
        let checker = HealthChecker::new(Duration::from_secs(1));
        checker.register("camera".to_string(), HealthStatus::Healthy).await;

        checker
            .update(
                "camera",
                HealthStatus::Degraded {
                    reason: "low battery".to_string(),
                },
            )
            .await;

        assert_eq!(
            checker.check_one("camera").await,
            Some(HealthStatus::Degraded {
                reason: "low battery".to_string()
            })
        );
        assert_eq!(checker.check_all().await.len(), 1);
    }

    #[tokio::test]
    async fn unregister_removes_the_node_from_check_one_and_check_all() {
        let checker = HealthChecker::new(Duration::from_secs(1));
        checker.register("camera".to_string(), HealthStatus::Healthy).await;

        checker.unregister("camera").await;

        assert_eq!(checker.check_one("camera").await, None);
        assert!(checker.check_all().await.is_empty());
    }
}