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 {
pub fn is_healthy(&self) -> bool {
matches!(self, HealthStatus::Healthy)
}
pub fn is_degraded(&self) -> bool {
matches!(self, HealthStatus::Degraded { .. })
}
pub fn is_unhealthy(&self) -> bool {
matches!(self, HealthStatus::Unhealthy { .. })
}
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 {
pub fn new(interval: Duration) -> Self {
Self {
checks: Arc::new(RwLock::new(HashMap::new())),
interval,
}
}
pub async fn register(&self, name: String, initial_status: HealthStatus) {
let mut checks = self.checks.write().await;
checks.insert(name, initial_status);
}
pub async fn update(&self, name: &str, status: HealthStatus) {
let mut checks = self.checks.write().await;
checks.insert(name.to_string(), status);
}
pub async fn check_one(&self, name: &str) -> Option<HealthStatus> {
self.checks.cloned_get(&name.to_string()).await
}
pub async fn check_all(&self) -> HashMap<String, HealthStatus> {
let checks = self.checks.read().await;
checks.clone()
}
pub fn interval(&self) -> Duration {
self.interval
}
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());
}
}