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}