use crate::cron::scheduler::{
CRON_LAST_TICK_AT, CRON_SPAWNED_AT, CronLiveness, cron_liveness, epoch_secs,
};
use crate::db::{CronJobRepository, Pool};
use axum::extract::State;
use axum::{Json, Router, routing::get};
use std::net::SocketAddr;
use std::sync::atomic::Ordering;
#[derive(Clone)]
struct HealthState {
pool: Pool,
}
pub(crate) fn router(pool: Pool) -> Router {
Router::new()
.route("/health", get(health))
.with_state(HealthState { pool })
}
pub async fn serve(port: u16, pool: Pool) -> anyhow::Result<()> {
let app = router(pool);
let addr: SocketAddr = ([0, 0, 0, 0], port).into();
tracing::info!("Daemon health endpoint listening on http://{}/health", addr);
let listener = tokio::net::TcpListener::bind(addr).await?;
axum::serve(listener, app).await?;
Ok(())
}
async fn health(State(state): State<HealthState>) -> Json<serde_json::Value> {
let repo = CronJobRepository::new(state.pool.clone());
let enabled = repo.list_enabled().await.map(|jobs| jobs.len() as u64);
let (cron_state, enabled_jobs, tick_age_secs) = match enabled {
Err(_) => (
"jobs-unreadable",
serde_json::Value::Null,
serde_json::Value::Null,
),
Ok(n) => {
let liveness = cron_liveness(
epoch_secs(),
CRON_SPAWNED_AT.load(Ordering::Relaxed),
CRON_LAST_TICK_AT.load(Ordering::Relaxed),
n,
);
match liveness {
CronLiveness::Idle => ("idle", serde_json::json!(0), serde_json::Value::Null),
CronLiveness::NotRunningWithJobs { enabled } => (
"not-running-with-jobs",
serde_json::json!(enabled),
serde_json::Value::Null,
),
CronLiveness::Stalled { age_secs, enabled } => (
"stalled",
serde_json::json!(enabled),
serde_json::json!(age_secs),
),
CronLiveness::Healthy { age_secs } => {
("healthy", serde_json::json!(n), serde_json::json!(age_secs))
}
}
}
};
Json(serde_json::json!({
"status": "ok",
"version": crate::VERSION,
"mode": "daemon",
"cron": {
"state": cron_state,
"enabled_jobs": enabled_jobs,
"tick_age_secs": tick_age_secs,
},
}))
}