arcature 2026.2.1

Arcature application framework: a high-level Application facade over the certified Arcature subsystems, with the low-level Axum/Tower escape hatch preserved.
Documentation
//! The `/up/live` liveness endpoint (AP2.1-10).
//!
//! Liveness is independent of any external dependency (PROGRAM.md AP2.1-10:
//! "liveness independent of external DB"): it is true once the process is
//! up and stays true through `Starting`, `Ready`, and `Draining`. It goes
//! false only when the process is `Stopped` (drain and shutdown complete,
//! the process is about to exit). A failing database or a wedged cache must
//! NOT flip liveness false — that is a readiness concern, not a liveness
//! one. Restarting a process that is genuinely live (but unready) would
//! create a crash loop, not recover the service.
//!
//! Returns `200 OK` while the process is live, `503 Service Unavailable`
//! once it is `Stopped`. The body is a tiny, stable, content-type-agnostic
//! string (`"live"` / `"stopped"`) so a load-balancer health check that
//! ignores the body still works, and a human reading the body sees the
//! state without parsing JSON (no information leakage — the body is a
//! public-safe status word).

use crate::application::lifecycle::Lifecycle;
use crate::axum::Extension;
use crate::axum::http::StatusCode;
use crate::axum::response::IntoResponse;

/// The liveness response: `200 OK` with body `"live"` while the process is
/// up, `503 Service Unavailable` with body `"stopped"` once it has stopped.
///
/// This is the body the `/up/live` route returns. Public so an application
/// that wires its own liveness route (e.g. at a different path) can reuse
/// the same response.
#[must_use]
pub fn live_response(lifecycle: &Lifecycle) -> (StatusCode, &'static str) {
    if lifecycle.live() {
        (StatusCode::OK, "live")
    } else {
        (StatusCode::SERVICE_UNAVAILABLE, "stopped")
    }
}

/// The `/up/live` handler. Reads the [`Lifecycle`] from an Axum
/// [`Extension`], so it composes with any application state type `S` (the
/// health router is state-agnostic; the lifecycle is layered on as an
/// extension, not carried as router state).
pub(crate) async fn live(Extension(lifecycle): Extension<Lifecycle>) -> impl IntoResponse {
    live_response(&lifecycle)
}

#[cfg(test)]
mod tests {
    use super::live_response;
    use crate::application::lifecycle::Lifecycle;
    use crate::axum::http::StatusCode;

    #[test]
    fn live_returns_200_while_starting() {
        let lifecycle = Lifecycle::new();
        let (status, body) = live_response(&lifecycle);
        assert_eq!(status, StatusCode::OK);
        assert_eq!(body, "live");
    }

    #[test]
    fn live_returns_200_while_ready() {
        let lifecycle = Lifecycle::new();
        lifecycle.mark_ready();
        let (status, body) = live_response(&lifecycle);
        assert_eq!(status, StatusCode::OK);
        assert_eq!(body, "live");
    }

    #[test]
    fn live_returns_200_while_draining() {
        let lifecycle = Lifecycle::new();
        lifecycle.mark_ready();
        lifecycle.begin_drain();
        let (status, body) = live_response(&lifecycle);
        assert_eq!(status, StatusCode::OK);
        assert_eq!(body, "live");
    }

    #[test]
    fn live_returns_503_when_stopped() {
        let lifecycle = Lifecycle::new();
        lifecycle.mark_stopped();
        let (status, body) = live_response(&lifecycle);
        assert_eq!(status, StatusCode::SERVICE_UNAVAILABLE);
        assert_eq!(body, "stopped");
    }
}