use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::Mutex;
use super::component_registry::{ComponentRegistry, ComponentStatus};
pub type HealthCheckFuture = Pin<Box<dyn Future<Output = anyhow::Result<()>> + Send>>;
pub type HealthCheckFn = Box<dyn Fn() -> HealthCheckFuture + Send + Sync>;
const DEFAULT_INTERVAL: Duration = Duration::from_secs(30);
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(5);
struct RegisteredCheck {
name: String,
check: HealthCheckFn,
}
pub struct HealthCheckManager {
registry: Arc<Mutex<ComponentRegistry>>,
checks: Vec<RegisteredCheck>,
interval: Duration,
timeout: Duration,
}
impl HealthCheckManager {
pub fn new(registry: Arc<Mutex<ComponentRegistry>>) -> Self {
Self {
registry,
checks: Vec::new(),
interval: DEFAULT_INTERVAL,
timeout: DEFAULT_TIMEOUT,
}
}
pub async fn register(&mut self, name: &str, critical: bool, check: HealthCheckFn) {
self.registry.lock().await.register(name, critical);
self.checks.push(RegisteredCheck {
name: name.to_string(),
check,
});
}
pub async fn run_once(&self) {
for RegisteredCheck { name, check } in &self.checks {
let outcome = tokio::time::timeout(self.timeout, check()).await;
let mut registry = self.registry.lock().await;
match outcome {
Ok(Ok(())) => registry.report(name, ComponentStatus::Healthy, None),
Ok(Err(err)) => {
tracing::warn!(component = name, error = %err, "health check failed");
registry.report(name, ComponentStatus::Unhealthy, Some(err.to_string()));
}
Err(_) => {
tracing::warn!(component = name, "health check timed out");
registry.report(
name,
ComponentStatus::Unhealthy,
Some("timed out".to_string()),
);
}
}
}
}
pub fn start(self: Arc<Self>) -> tokio::task::JoinHandle<()> {
tokio::spawn(async move {
let mut ticker = tokio::time::interval(self.interval);
loop {
ticker.tick().await;
self.run_once().await;
}
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn a_passing_check_reports_healthy() {
let registry = Arc::new(Mutex::new(ComponentRegistry::new()));
let mut manager = HealthCheckManager::new(registry.clone());
manager
.register("db", true, Box::new(|| Box::pin(async { Ok(()) })))
.await;
manager.run_once().await;
assert_eq!(
registry.lock().await.overall_status(),
ComponentStatus::Healthy
);
}
#[tokio::test]
async fn a_failing_check_reports_unhealthy() {
let registry = Arc::new(Mutex::new(ComponentRegistry::new()));
let mut manager = HealthCheckManager::new(registry.clone());
manager
.register(
"db",
true,
Box::new(|| Box::pin(async { Err(anyhow::anyhow!("connection refused")) })),
)
.await;
manager.run_once().await;
assert_eq!(
registry.lock().await.overall_status(),
ComponentStatus::Unhealthy
);
}
}