actionqueue_daemon/bootstrap.rs
1//! Daemon bootstrap module.
2//!
3//! This module provides the bootstrap entry point for the ActionQueue daemon.
4//! Bootstrap assembles runtime dependencies from configuration and prepares
5//! the HTTP introspection surface without starting background loops or
6//! binding network sockets.
7//!
8//! # Overview
9//!
10//! The bootstrap process:
11//! 1. Validates configuration values
12//! 2. Constructs storage and engine dependencies
13//! 3. Initializes metrics registry
14//! 4. Sets up router wiring entry point with control feature flag gating
15//!
16//! # Invariant boundaries
17//!
18//! Bootstrap must not introduce mutation authority bypass or direct state
19//! mutation. All control routes must route through validated mutation
20//! authority defined in [`actionqueue_storage::mutation::authority`].
21//!
22//! # Bootstrap output
23//!
24//! On success, bootstrap returns a [`BootstrapState`] that contains:
25//!
26//! - The validated configuration
27//! - Metrics registry handle
28//! - A concrete HTTP router built with [`crate::http::build_router()`]
29//! - Shared router state (Arc-wrapped [`crate::http::RouterStateInner`])
30//!
31//! The router and router state are constructed deterministically from the
32//! bootstrap output (config control flag, projection state, and readiness).
33//! No sockets are bound and no background tasks are started.
34//!
35//! # Example
36//!
37//! ```no_run
38//! use actionqueue_daemon::bootstrap::{bootstrap, BootstrapState};
39//! use actionqueue_daemon::config::DaemonConfig;
40//!
41//! let config = DaemonConfig::default();
42//! let state = bootstrap(config).expect("bootstrap should succeed");
43//!
44//! // Access the router and state directly from bootstrap output
45//! let router = state.http_router();
46//! let router_state = state.router_state();
47//!
48//! // Use router to start HTTP server at a higher level
49//! // (HTTP server startup is not part of bootstrap)
50//! ```
51
52use std::path::PathBuf;
53
54use actionqueue_storage::mutation::authority::StorageMutationAuthority;
55use actionqueue_storage::recovery::bootstrap::{
56 load_projection_from_storage, RecoveryBootstrapError,
57};
58use actionqueue_storage::recovery::reducer::ReplayReducer;
59
60use crate::config::{ConfigError, DaemonConfig};
61use crate::metrics::registry::MetricsRegistry;
62use crate::time::clock::{SharedDaemonClock, SystemClock};
63
64/// Bootstrap error types.
65///
66/// Typed errors distinguish configuration validation failures from dependency
67/// initialization failures.
68#[derive(Debug, Clone, PartialEq, Eq)]
69pub enum BootstrapError {
70 /// Configuration validation failed.
71 Config(ConfigError),
72 /// WAL file system initialization failed.
73 WalInit(String),
74 /// Dependency assembly failed.
75 Dependency(String),
76}
77
78impl std::fmt::Display for BootstrapError {
79 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80 match self {
81 BootstrapError::Config(e) => write!(f, "config error: {e}"),
82 BootstrapError::WalInit(msg) => write!(f, "WAL initialization error: {msg}"),
83 BootstrapError::Dependency(msg) => write!(f, "dependency error: {msg}"),
84 }
85 }
86}
87
88impl std::error::Error for BootstrapError {}
89
90impl std::convert::From<ConfigError> for BootstrapError {
91 fn from(err: ConfigError) -> Self {
92 BootstrapError::Config(err)
93 }
94}
95
96/// Ready status representation.
97///
98/// This struct holds the daemon readiness state derived from bootstrap completion.
99/// Readiness is a deterministic boolean plus a stable reason string, derived only
100/// from bootstrap state without any IO or polling.
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102#[must_use]
103pub struct ReadyStatus {
104 /// Whether the daemon is ready.
105 is_ready: bool,
106 /// A static string describing the readiness state.
107 reason: &'static str,
108}
109
110impl ReadyStatus {
111 /// The daemon is fully operational.
112 pub const REASON_READY: &'static str = "ready";
113
114 /// Configuration was invalid during bootstrap.
115 pub const REASON_CONFIG_INVALID: &'static str = "config_invalid";
116
117 /// Bootstrap process was incomplete.
118 pub const REASON_BOOTSTRAP_INCOMPLETE: &'static str = "bootstrap_incomplete";
119
120 /// Returns a ready status indicating the daemon is fully operational.
121 pub const fn ready() -> Self {
122 Self { is_ready: true, reason: Self::REASON_READY }
123 }
124
125 /// Returns a not-ready status with the given reason string.
126 ///
127 /// Valid reasons are the documented static constants on [`ReadyStatus`]:
128 /// - [`REASON_READY`](ReadyStatus::REASON_READY)
129 /// - [`REASON_CONFIG_INVALID`](ReadyStatus::REASON_CONFIG_INVALID)
130 /// - [`REASON_BOOTSTRAP_INCOMPLETE`](ReadyStatus::REASON_BOOTSTRAP_INCOMPLETE)
131 pub const fn not_ready(reason: &'static str) -> Self {
132 Self { is_ready: false, reason }
133 }
134
135 /// Returns whether the daemon is ready.
136 pub const fn is_ready(&self) -> bool {
137 self.is_ready
138 }
139
140 /// Returns the static reason string describing the readiness state.
141 pub const fn reason(&self) -> &'static str {
142 self.reason
143 }
144}
145
146/// Router configuration for routing assembly.
147#[derive(Debug, Clone)]
148pub struct RouterConfig {
149 /// Whether control endpoints are enabled.
150 pub control_enabled: bool,
151 /// Whether metrics endpoint exposure is enabled.
152 pub metrics_enabled: bool,
153}
154
155/// Bootstrap state returned after successful bootstrap.
156///
157/// This struct holds all runtime handles assembled during bootstrap.
158/// It does not start background loops or bind network sockets.
159///
160/// The concrete HTTP router and shared router state are assembled from
161/// bootstrap output (config control flag, projection state, and readiness).
162#[must_use]
163pub struct BootstrapState {
164 /// The validated daemon configuration.
165 config: DaemonConfig,
166 /// The metrics registry handle.
167 metrics: std::sync::Arc<MetricsRegistry>,
168 /// The concrete HTTP router built with http::build_router().
169 http_router: axum::Router,
170 /// WAL path for persistence (reconstructed on demand).
171 wal_path: PathBuf,
172 /// Snapshot path for persistence.
173 snapshot_path: PathBuf,
174 /// Replay projection state.
175 projection: ReplayReducer,
176 /// Shared router state (Arc-wrapped inner state).
177 router_state: crate::http::RouterState,
178 /// Shared authoritative daemon clock handle.
179 clock: SharedDaemonClock,
180 /// Ready status indicating daemon readiness derived from bootstrap state.
181 ready_status: ReadyStatus,
182}
183
184impl BootstrapState {
185 /// Returns the validated configuration.
186 pub fn config(&self) -> &DaemonConfig {
187 &self.config
188 }
189
190 /// Returns the metrics registry handle.
191 pub fn metrics(&self) -> &MetricsRegistry {
192 self.metrics.as_ref()
193 }
194
195 /// Returns the concrete HTTP router.
196 pub fn http_router(&self) -> &axum::Router {
197 &self.http_router
198 }
199
200 /// Returns the shared router state (Arc-wrapped inner state).
201 pub fn router_state(&self) -> &crate::http::RouterState {
202 &self.router_state
203 }
204
205 /// Returns the shared authoritative daemon clock handle.
206 pub fn clock(&self) -> &SharedDaemonClock {
207 &self.clock
208 }
209
210 /// Consumes self and returns the concrete router and router state as a tuple.
211 pub fn into_http(self) -> (axum::Router, crate::http::RouterState) {
212 (self.http_router, self.router_state)
213 }
214
215 /// Returns the WAL path.
216 pub fn wal_path(&self) -> &PathBuf {
217 &self.wal_path
218 }
219
220 /// Returns the snapshot path.
221 pub fn snapshot_path(&self) -> &PathBuf {
222 &self.snapshot_path
223 }
224
225 /// Returns the ReplayReducer projection state.
226 pub fn projection(&self) -> &ReplayReducer {
227 &self.projection
228 }
229
230 /// Returns the ready status indicating daemon readiness.
231 pub fn ready_status(&self) -> ReadyStatus {
232 self.ready_status
233 }
234}
235
236/// Bootstrap entry point.
237///
238/// Validates configuration and assembles runtime dependencies.
239/// This function is deterministic and does not start background loops
240/// or bind network sockets.
241///
242/// # Errors
243///
244/// Returns [`BootstrapError::Config`] if configuration validation fails.
245/// Returns [`BootstrapError::WalInit`] if WAL file initialization fails.
246///
247/// # Examples
248///
249/// ```no_run
250/// use actionqueue_daemon::bootstrap::bootstrap;
251/// use actionqueue_daemon::config::DaemonConfig;
252///
253/// let config = DaemonConfig::default();
254/// let state = bootstrap(config).expect("bootstrap should succeed");
255/// ```
256pub fn bootstrap(config: DaemonConfig) -> Result<BootstrapState, BootstrapError> {
257 // Initialize structured logging subscriber.
258 // Uses RUST_LOG env var for filtering (e.g. RUST_LOG=actionqueue=debug).
259 // init() is a no-op if a subscriber is already set (e.g. in tests).
260 let _ = tracing_subscriber::fmt()
261 .with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
262 .try_init();
263
264 // Validate configuration first
265 config.validate()?;
266
267 let recovery = load_projection_from_storage(&config.data_dir).map_err(map_recovery_error)?;
268 let wal_path = recovery.wal_path.clone();
269 let snapshot_path = recovery.snapshot_path.clone();
270 let projection = recovery.projection.clone();
271 let wal_append_telemetry = recovery.wal_append_telemetry.clone();
272 let recovery_observations = recovery.recovery_observations;
273
274 let control_authority = if config.enable_control {
275 Some(std::sync::Arc::new(std::sync::Mutex::new(StorageMutationAuthority::new(
276 recovery.wal_writer,
277 projection.clone(),
278 ))))
279 } else {
280 None
281 };
282
283 // Create metrics registry
284 let metrics =
285 std::sync::Arc::new(MetricsRegistry::new(config.metrics_bind).map_err(|error| {
286 BootstrapError::Dependency(format!("metrics_registry_init_failed: {error}"))
287 })?);
288
289 // Create a single authoritative daemon clock handle for router and metrics wiring.
290 let clock: SharedDaemonClock = std::sync::Arc::new(SystemClock);
291
292 // Build router state from bootstrap output
293 let ready_status = ReadyStatus::ready();
294 let router_config = crate::bootstrap::RouterConfig {
295 control_enabled: config.enable_control,
296 metrics_enabled: config.metrics_bind.is_some(),
297 };
298 let observability = crate::http::RouterObservability {
299 metrics: metrics.clone(),
300 wal_append_telemetry,
301 clock: clock.clone(),
302 recovery_observations,
303 };
304 let shared_projection = std::sync::Arc::new(std::sync::RwLock::new(projection.clone()));
305 let router_state_inner = if let Some(authority) = control_authority {
306 crate::http::RouterStateInner::with_control_authority(
307 router_config,
308 shared_projection,
309 observability,
310 authority,
311 ready_status,
312 )
313 } else {
314 crate::http::RouterStateInner::new(
315 router_config,
316 shared_projection,
317 observability,
318 ready_status,
319 )
320 };
321 let router_state = std::sync::Arc::new(router_state_inner);
322
323 // Build the concrete HTTP router using the assembly entry
324 let http_router = crate::http::build_router(router_state.clone());
325
326 // Return bootstrap state with concrete router and state handle
327 Ok(BootstrapState {
328 config,
329 metrics,
330 http_router,
331 wal_path,
332 snapshot_path,
333 projection,
334 router_state,
335 clock,
336 ready_status,
337 })
338}
339
340fn map_recovery_error(error: RecoveryBootstrapError) -> BootstrapError {
341 match error {
342 RecoveryBootstrapError::WalInit(msg) => BootstrapError::WalInit(msg),
343 RecoveryBootstrapError::WalRead(msg)
344 | RecoveryBootstrapError::SnapshotLoad(msg)
345 | RecoveryBootstrapError::WalReplay(msg)
346 | RecoveryBootstrapError::SnapshotBootstrap(msg) => BootstrapError::Dependency(msg),
347 }
348}
349
350#[cfg(test)]
351mod tests {
352 use super::*;
353
354 #[test]
355 fn test_bootstrap_with_valid_config() {
356 let config = DaemonConfig::default();
357 let control_flag = config.enable_control;
358 let result = bootstrap(config);
359
360 // We expect success if data directory exists or can be created
361 // The exact result may vary based on filesystem permissions
362 if let Ok(state) = result {
363 // Assert router state wiring
364 // RouterState is Arc<RouterStateInner>, and RouterStateInner is public
365 let router_state = state.router_state();
366
367 // Access the inner fields through Arc deref
368 assert!(router_state.ready_status.is_ready());
369 assert_eq!(router_state.router_config.control_enabled, control_flag);
370 assert_eq!(router_state.router_config.metrics_enabled, state.metrics().is_enabled());
371 } else {
372 // Skip assertions if bootstrap fails due to file system issues
373 assert!(matches!(result, Err(BootstrapError::WalInit(_))));
374 }
375 }
376
377 #[test]
378 fn test_bootstrap_with_invalid_config() {
379 // Test with invalid bind address (port 0)
380 let config = DaemonConfig {
381 bind_address: std::net::SocketAddr::from(([127, 0, 0, 1], 0)),
382 ..Default::default()
383 };
384
385 let result = bootstrap(config);
386 assert!(matches!(result, Err(BootstrapError::Config(_))));
387 }
388}