mecha10-runtime 0.6.5

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;

/// Health status of a node
#[derive(Clone, Debug, PartialEq)]
pub enum HealthStatus {
    /// Node is healthy and operating normally
    Healthy,

    /// Node is degraded but still functional
    Degraded { reason: String },

    /// Node is unhealthy and may need restart
    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),
        }
    }
}

/// Health checker for monitoring node health
pub struct HealthChecker {
    checks: Arc<RwLock<HashMap<String, HealthStatus>>>,
    interval: Duration,
}

impl HealthChecker {
    // duplicate: false positive — matches other unrelated structs' `new()` (e.g. CLI's
    // `MessageBuffer`/`LogBuffer`) purely because both move constructor args into fields
    // around a fresh `HashMap`/`Arc<RwLock<_>>>`; health monitoring shares no behavior with
    // those TUI buffer types worth extracting a helper for.
    /// Create a new health checker with specified interval
    pub fn new(interval: Duration) -> Self {
        Self {
            checks: Arc::new(RwLock::new(HashMap::new())),
            interval,
        }
    }

    // duplicate: false positive — matches `PerformanceMetrics::record_memory`
    // (packages/core/src/profiling.rs) on AST shape only (lock a `RwLock`, insert/mutate);
    // unrelated domains, nothing to share.
    /// 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);
    }

    // duplicate: false positive — matches `RecoveryManager::set_policy`
    // (packages/core/src/recovery.rs) on AST shape only (write-lock a map, insert); unrelated
    // domains (health status vs recovery policy), nothing to share.
    /// 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
    }

    // duplicate: false positive — matches `ConfigWatcher::get_async`, `TopicRegistry::topic_count`,
    // `DeadLetterQueue::get_all`/`len` on AST shape only (read-lock then clone/measure the guarded
    // value); each clones or measures a different type for an unrelated domain, and there's no
    // `RwLockMapExt`-style helper to extract since some of these aren't even keyed maps (e.g.
    // `ConfigWatcher<T>` clones a whole `T`).
    /// 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());
    }
}