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 health-managed run path — the production default (AP2.1-10).
//!
//! [`Application::run_with_health`] is the normal run path for a production
//! application with subsystems and health endpoints. It binds a
//! `TcpListener` on the configured address/port, serves the assembled
//! pipeline (with `/up/live` and `/up/ready` merged in and the
//! [`Lifecycle`] layered on) until a production termination signal (Unix
//! `SIGTERM`/`SIGINT`, Windows Ctrl-C/Ctrl-Break), and drains + shuts down
//! the subsystems in reverse startup order. It delegates the lifecycle to
//! [`super::serve_with_health`], the testable seam.
//!
//! Gated by the `macros` feature (tokio `TcpListener` / `signal`) plus at
//! least one lifecycle subsystem, matching `run_with_lifecycle`. The
//! `signal` sub-feature is on the certified tokio runtime line; no new
//! dependency.

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

impl<S> Application<S>
where
    S: Clone + Send + Sync + 'static,
{
    /// The production run path: start subsystems, serve with health
    /// endpoints, and drain + shut down on a termination signal.
    ///
    /// This is the production default for an application with subsystems
    /// (db, cache, storage, mail, jobs). The `state_fn` closure receives
    /// `&Resources` **and** `&Lifecycle` after startup: build the
    /// `AppState` from the live handles, register readiness checks
    /// (`lifecycle.register_readiness(...)`), and register drain hooks
    /// (`lifecycle.register_drain_hook(...)`). The engine calls
    /// `routes.with_state(state)` before serving, then serves until a
    /// production termination signal (Unix `SIGTERM`/`SIGINT`, Windows
    /// Ctrl-C/Ctrl-Break) — see `termination_signal`.
    ///
    /// # Lifecycle
    ///
    /// 1. **Startup** — `startup()` connects subsystems; the lifecycle is
    ///    `Starting`.
    /// 2. **State** — `state_fn(&resources, &lifecycle)` builds `AppState`
    ///    and registers readiness/drain hooks.
    /// 3. **Ready** — the lifecycle becomes `Ready`; `/up/ready` returns 200
    ///    iff the app's readiness checks pass.
    /// 4. **Serve** — the engine binds a `TcpListener` on the configured
    ///    address/port and serves the assembled pipeline (with `/up/live`
    ///    and `/up/ready`) until a termination signal.
    /// 5. **Drain** — on the signal, readiness goes 503 first, then axum
    ///    drains in-flight HTTP, then the drain hooks run.
    /// 6. **Shutdown** — subsystem resources tear down in reverse startup
    ///    order; the lifecycle becomes `Stopped`.
    ///
    /// # Errors
    ///
    /// Returns [`EngineError::Startup`] if a subsystem fails,
    /// [`EngineError::BindListener`] if the listener bind fails,
    /// [`EngineError::Serve`] if the server fails, or
    /// [`EngineError::Shutdown`] if a subsystem fails to shut down.
    ///
    /// # See also
    ///
    /// [`serve_with_health`](Self::serve_with_health) is the testable
    /// analogue: it accepts a pre-bound listener and a caller-provided
    /// shutdown signal, so integration tests drive the lifecycle
    /// deterministically without sending OS signals.
    pub async fn run_with_health<F>(self, state_fn: F) -> Result<()>
    where
        F: Fn(&crate::application::Resources, &crate::application::Lifecycle) -> 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_health(listener, state_fn, termination_signal())
            .await
    }
}