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 lifecycle-managed run path — start subsystems, serve, and shut down.
//!
//! [`Application::run_with_lifecycle`] is the normal run path for an
//! application with subsystems (db, cache, storage, mail, jobs). It binds a
//! `TcpListener` on the configured address/port, serves the assembled pipeline
//! until `SIGINT` (Ctrl-C), and tears down the subsystems in reverse startup
//! order. It delegates the lifecycle (startup → state → serve → shutdown) to
//! [`super::serve_with_lifecycle`], which is the testable seam (a caller can
//! pass an ephemeral listener and a `oneshot` channel instead of relying on
//! OS signals).
//!
//! Gated by the `macros` feature (needs `tokio` for `TcpListener` / `ctrl_c`
//! and `tokio-util` for `CancellationToken`). An expert user who wants their
//! own runtime, listener, or shutdown signal calls `Application::serve` /
//! `Application::serve_with_shutdown` directly (engine spec §21/§23) and
//! orchestrates `startup` / `shutdown` themselves via the `pub(crate)` seam
//! (exposed for the engine's own integration tests).

use crate::application::ty::Application;
use crate::application::{EngineError, Result};

impl<S> Application<S>
where
    S: Clone + Send + Sync + 'static,
{
    /// The lifecycle-managed run path: start subsystems, serve, and shut down.
    ///
    /// This is the normal entry point for an application with subsystems
    /// (db, cache, storage, mail, jobs). The `state_fn` closure receives
    /// `&Resources` after startup and returns the application's `AppState`
    /// (which the engine passes to `Routes::with_state` before serving).
    ///
    /// # Lifecycle
    ///
    /// 1. **Startup** — `startup()` connects all configured subsystems in
    ///    order (db → jobs → cache → storage → mail). On failure, the
    ///    already-started subsystems are torn down (reverse order) and the
    ///    error is returned.
    /// 2. **State** — `state_fn(&resources)` builds the `AppState` from the
    ///    live handles (e.g. cloning the `Db` into the state). The engine
    ///    calls `routes.with_state(state)` to resolve the router to `()`.
    /// 3. **Serve** — the engine binds a `TcpListener` on the configured
    ///    address/port and serves the assembled pipeline until `SIGINT`
    ///    (Ctrl-C).
    /// 4. **Shutdown** — `shutdown()` tears down subsystems in reverse
    ///    startup order (worker drain → db close → cache, storage, mail).
    ///
    /// # Errors
    ///
    /// Returns [`EngineError::Startup`](crate::EngineError::Startup) if a subsystem fails to start,
    /// [`EngineError::BindListener`](crate::EngineError::BindListener) if the listener bind fails,
    /// [`EngineError::Serve`](crate::EngineError::Serve) if the server fails, or
    /// [`EngineError::Shutdown`](crate::EngineError::Shutdown) if a subsystem fails to shut down.
    ///
    /// # See also
    ///
    /// [`serve_with_lifecycle`](Self::serve_with_lifecycle) is the testable
    /// analogue: it accepts a pre-bound listener and a caller-provided
    /// shutdown signal, so integration tests can drive the lifecycle
    /// deterministically without sending OS signals.
    pub async fn run_with_lifecycle<F>(self, state_fn: F) -> Result<()>
    where
        F: Fn(&crate::application::Resources) -> S + Send + Sync + 'static,
    {
        let bind_addr = format!("{}:{}", self.bind_address, self.port);
        let listener = tokio::net::TcpListener::bind(&bind_addr)
            .await
            .map_err(|source| EngineError::BindListener {
                address: bind_addr.clone(),
                source,
            })?;

        self.serve_with_lifecycle(listener, state_fn, async {
            let _ = tokio::signal::ctrl_c().await;
        })
        .await
    }
}