Skip to main content

actionqueue_daemon/http/
ready.rs

1//! Ready route module.
2//!
3//! This module provides the readiness endpoint (`GET /ready`) for the daemon.
4//! The ready endpoint is side-effect free and provides a deterministic response
5//! indicating daemon readiness. It reflects whether the daemon has completed
6//! bootstrap and is fully operational.
7//!
8//! # Invariant boundaries
9//!
10//! The ready handler performs no IO, reads no storage, and mutates no runtime state.
11//! It reflects the bootstrap state and is constant-time.
12//!
13//! # Response schema
14//!
15//! When ready: `{"status": "ready"}`
16//! When not ready: `{"status": "<reason>"}`
17//!
18//! The status is "ready" when the daemon has completed bootstrap. Otherwise,
19//! it includes a reason string indicating why the daemon is not yet ready.
20//!
21//! # Readiness vocabulary (WP-2)
22//!
23//! The only allowed not-ready reasons in WP-2 are:
24//! - `ReadyStatus::REASON_CONFIG_INVALID`: Configuration was invalid during bootstrap.
25//! - `ReadyStatus::REASON_BOOTSTRAP_INCOMPLETE`: Bootstrap process was incomplete.
26//!
27//! These reasons are defined as static constants on [`ReadyStatus`](crate::bootstrap::ReadyStatus).
28
29use axum::extract::State;
30use axum::http::StatusCode;
31use axum::response::IntoResponse;
32use axum::Json;
33use serde::Serialize;
34
35/// Ready check response payload.
36///
37/// This struct represents the stable schema for the ready endpoint response.
38/// Fields should not be modified without careful consideration of external
39/// dependencies that may rely on this contract.
40///
41/// # Readiness vocabulary (WP-2)
42///
43/// When not ready, the status should be one of the documented reasons from
44/// [`ReadyStatus`](crate::bootstrap::ReadyStatus):
45/// - `ReadyStatus::REASON_CONFIG_INVALID`
46/// - `ReadyStatus::REASON_BOOTSTRAP_INCOMPLETE`
47#[derive(Debug, Clone, Serialize)]
48pub struct ReadyResponse {
49    /// Daemon readiness status.
50    ///
51    /// When the daemon is fully ready, this is "ready".
52    /// When not ready, this contains a reason string.
53    pub status: &'static str,
54}
55
56impl ReadyResponse {
57    /// Creates a new ready response indicating the daemon is ready.
58    pub const fn ready() -> Self {
59        Self { status: "ready" }
60    }
61
62    /// Creates a new ready response indicating the daemon is not ready with a reason.
63    pub const fn not_ready(reason: &'static str) -> Self {
64        Self { status: reason }
65    }
66}
67
68/// Ready check handler.
69///
70/// This handler responds to `GET /ready` requests with a deterministic,
71/// side-effect-free payload indicating daemon readiness.
72///
73/// # Invariant boundaries
74///
75/// This handler performs no IO, reads no storage, and mutates no runtime state.
76/// It reflects the bootstrap state contained in RouterState.
77#[tracing::instrument(skip_all)]
78pub async fn handle(state: State<super::RouterState>) -> impl IntoResponse {
79    if state.ready_status.is_ready() {
80        (StatusCode::OK, Json(ReadyResponse::ready()))
81    } else {
82        (
83            StatusCode::SERVICE_UNAVAILABLE,
84            Json(ReadyResponse::not_ready(state.ready_status.reason())),
85        )
86    }
87}
88
89/// Registers the ready route in the router builder.
90///
91/// This function adds the `/ready` endpoint to the router configuration.
92/// The route is always available when HTTP is enabled and does not depend
93/// on any feature flags.
94///
95/// # Arguments
96///
97/// * `router` - The axum router to register routes with
98pub fn register_routes(
99    router: axum::Router<super::RouterState>,
100) -> axum::Router<super::RouterState> {
101    router.route("/ready", axum::routing::get(handle))
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    #[test]
109    fn test_ready_response_ready() {
110        let response = ReadyResponse::ready();
111        assert_eq!(response.status, "ready");
112    }
113
114    #[test]
115    fn test_ready_response_not_ready() {
116        let response =
117            ReadyResponse::not_ready(crate::bootstrap::ReadyStatus::REASON_BOOTSTRAP_INCOMPLETE);
118        assert_eq!(response.status, crate::bootstrap::ReadyStatus::REASON_BOOTSTRAP_INCOMPLETE);
119    }
120
121    #[test]
122    fn test_ready_response_serialization_ready() {
123        let response = ReadyResponse::ready();
124        let json = serde_json::to_string(&response).expect("serialization should succeed");
125        assert_eq!(json, r#"{"status":"ready"}"#);
126    }
127
128    #[test]
129    fn test_ready_response_serialization_not_ready() {
130        let response =
131            ReadyResponse::not_ready(crate::bootstrap::ReadyStatus::REASON_BOOTSTRAP_INCOMPLETE);
132        let json = serde_json::to_string(&response).expect("serialization should succeed");
133        assert_eq!(json, r#"{"status":"bootstrap_incomplete"}"#);
134    }
135
136    #[test]
137    fn test_ready_response_from_ready_status_ready() {
138        let ready_status = crate::bootstrap::ReadyStatus::ready();
139        let response = if ready_status.is_ready() {
140            ReadyResponse::ready()
141        } else {
142            ReadyResponse::not_ready(ready_status.reason())
143        };
144        assert_eq!(response.status, "ready");
145    }
146
147    #[test]
148    fn test_ready_response_from_ready_status_not_ready() {
149        let ready_status = crate::bootstrap::ReadyStatus::not_ready(
150            crate::bootstrap::ReadyStatus::REASON_BOOTSTRAP_INCOMPLETE,
151        );
152        let response = if ready_status.is_ready() {
153            ReadyResponse::ready()
154        } else {
155            ReadyResponse::not_ready(ready_status.reason())
156        };
157        assert_eq!(response.status, crate::bootstrap::ReadyStatus::REASON_BOOTSTRAP_INCOMPLETE);
158    }
159}