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 coordinated engine shutdown — tear down subsystems in reverse startup
//! order, with a bounded drain (engine spec §28).
//!
//! Shutdown order (reverse of [`super::startup`]):
//!
//! ```text
//! mail.shutdown()       (releases SMTP connections)
//! storage.drop()        (last clone dropped; Operator releases)
//! cache.drop()          (last clone dropped; TCP socket closes)
//! worker.cancel() + join (stop claiming; drain in-flight jobs; await)
//! db.close()            (closes the PgPool for all handles — last)
//! ```
//!
//! The worker is cancelled first (stops claiming new jobs), then its in-flight
//! tasks drain (bounded by the per-job timeout), then the pool closes. This
//! guarantees no orphaned job tasks and no "pool closed" errors during drain
//! (AGENTS.md §9: do not claim done unless it runs).
//!
//! # Partial-shutdown safety
//!
//! If `shutdown` is called on a `Started` where some subsystems were not
//! configured (`None`), those steps are skipped. If a subsystem was
//! configured but failed to start, [`super::startup`] calls `shutdown` on the
//! partial state before returning the error — the `None` fields are skipped
//! cleanly.

use crate::application::error::EngineError;
#[cfg(feature = "jobs")]
use crate::application::error::ShutdownError;
use crate::application::startup::Started;

/// Tear down all started subsystems in reverse startup order.
///
/// Returns the first error encountered (if any), but continues shutting down
/// the remaining subsystems even after a failure — a partial shutdown is
/// better than leaving resources open. The error is logged and returned so
/// the operator sees which subsystem failed.
pub(crate) async fn shutdown(started: Started) -> Result<(), EngineError> {
    let Started {
        resources,
        #[cfg(feature = "jobs")]
        worker,
    } = started;

    // `first_error` is only mutated when the `jobs` worker can fail during
    // drain. Without `jobs`, shutdown is infallible (drop semantics only).
    #[cfg(feature = "jobs")]
    let mut first_error: Option<EngineError> = None;

    // ── mail.shutdown() ─────────────────────────────────────────────────
    #[cfg(feature = "mail")]
    {
        if let Some(mailer) = resources.mail {
            // `Mailer::shutdown` takes `&self` and affects all clones (global).
            // We call it on the one we hold; the socket pool releases.
            mailer.shutdown().await;
            // `Mailer::shutdown` does not return an error (it's `async fn
            // shutdown(&self)`). If the SMTP connection fails to close
            // gracefully, lettre logs internally; we treat it as best-effort.
        }
    }

    // ── storage.drop() ──────────────────────────────────────────────────
    #[cfg(feature = "storage")]
    {
        // `Storage` has no explicit shutdown. Dropping the last clone
        // releases the OpenDAL `Operator` (Arc-backed). We explicitly drop
        // here to make the lifecycle ordering visible and deterministic.
        drop(resources.storage);
    }

    // ── cache.drop() ────────────────────────────────────────────────────
    #[cfg(feature = "cache")]
    {
        // `Cache::close` consumes `self` but only drops this handle; the
        // socket closes when the last clone is dropped. Since `Resources`
        // holds the only clone (the application clones from the closure),
        // closing here closes the socket.
        if let Some(cache) = resources.cache {
            cache.close().await;
        }
    }

    // ── worker.cancel() + join (stop claiming; drain in-flight jobs) ────
    #[cfg(feature = "jobs")]
    {
        if let Some(worker_handle) = worker {
            // Signal the worker to stop claiming new jobs. The worker then
            // releases already-claimed rows back to `pending` and drains its
            // `JoinSet` (bounded by the per-job timeout). `run` returns
            // `Ok(())` on graceful shutdown, `Err(WorkerError)` on a fatal DB
            // error during drain.
            worker_handle.shutdown.cancel();
            let result = worker_handle.join.await;
            match result {
                Ok(Ok(())) => { /* graceful shutdown — in-flight jobs drained */ }
                Ok(Err(source)) => {
                    if first_error.is_none() {
                        first_error = Some(EngineError::Shutdown {
                            subsystem: "jobs",
                            source: ShutdownError::Worker(source),
                        });
                    }
                }
                Err(join_err) => {
                    if first_error.is_none() {
                        first_error = Some(EngineError::Shutdown {
                            subsystem: "jobs",
                            source: ShutdownError::Worker(
                                // The JoinHandle panicked; surface the
                                // panic as a typed error.
                                arcature_jobs::WorkerError::Database {
                                    source: arcature_db::sqlx::Error::WorkerCrashed,
                                },
                            ),
                        });
                        let _ = join_err; // suppress unused warning
                    }
                }
            }
        }
    }

    // ── db.close() (closes the PgPool for all handles — last) ─────────────
    #[cfg(feature = "db")]
    {
        if let Some(db) = resources.db {
            // `Db::close` consumes `self` and closes the `Arc`-shared `PgPool`
            // for all handles. Called last so the worker's drain (above) had
            // a live pool to commit its outcomes with.
            db.close().await;
        }
    }

    // Non-jobs, non-db, non-cache, non-storage, non-mail crates: `resources`
    // may still hold unused fields. Drop them explicitly for determinism.
    #[cfg(not(any(
        feature = "db",
        feature = "cache",
        feature = "storage",
        feature = "mail",
        feature = "jobs"
    )))]
    {
        let _ = resources;
    }

    #[cfg(feature = "jobs")]
    {
        if let Some(err) = first_error {
            return Err(err);
        }
    }
    Ok(())
}