Skip to main content

hennery_kernel/
health.rs

1//! `/healthz` and `/readyz` (kernel spec §8): whether the process is up, and
2//! whether its database answers. For a service manager or a load balancer:
3//! they need no session, sit outside the browser rules (kernel spec §3.3),
4//! and carry no data, only a fixed word.
5
6use crate::operator::Operator;
7use axum::Router;
8use axum::extract::State;
9use axum::http::StatusCode;
10use axum::routing::get;
11use std::sync::Arc;
12use std::time::Duration;
13
14/// `/readyz` answers 503 when the database has not answered by then.
15pub const READY_TIMEOUT: Duration = Duration::from_secs(2);
16
17pub fn router(operator: Arc<Operator>) -> Router {
18    Router::new()
19        .route("/healthz", get(|| async { "ok" }))
20        .route("/readyz", get(ready))
21        .with_state(operator)
22}
23
24/// 200 `ready` when the database answers a query within `READY_TIMEOUT`,
25/// 503 `not ready` when it does not (the error is logged, not shown). The
26/// query runs on a blocking thread: a database held busy must not hold up
27/// the runtime's workers, nor the probe past its timeout.
28async fn ready(State(operator): State<Arc<Operator>>) -> (StatusCode, &'static str) {
29    let ping = tokio::task::spawn_blocking(move || operator.ping());
30    let why = match tokio::time::timeout(READY_TIMEOUT, ping).await {
31        Ok(Ok(Ok(()))) => return (StatusCode::OK, "ready"),
32        Ok(Ok(Err(err))) => format!("{err:#}"),
33        Ok(Err(err)) => err.to_string(),
34        Err(_) => format!("no answer within {READY_TIMEOUT:?}"),
35    };
36    tracing::warn!(error = %why, "readyz: the database does not answer");
37    (StatusCode::SERVICE_UNAVAILABLE, "not ready")
38}