Skip to main content

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}