Skip to main content

ferrox_app/
lib.rs

1//! # Ferrox App (`ferrox-app`)
2//!
3//! `ferrox-app` provides the primary application bootstrapper and multi-transport lifecycle orchestrator
4//! for the Ferrox framework. It manages concurrent server instances (HTTP, gRPC, WebSockets, etc.) and enforces
5//! graceful shutdown handling across UNIX signals (`SIGTERM`) and cross-platform interrupts (`Ctrl+C`).
6//!
7//! ## Architectural Role
8//! In enterprise applications, backends often need to serve multiple network protocols simultaneously (e.g. Axum for HTTP/REST,
9//! Tonic for gRPC inter-service communication). `FerroxApp` encapsulates these network transports into unified `Arc<dyn Transport>`
10//! workers and manages their startup, execution lifecycle, and teardown concurrently.
11//!
12//! ## Key Features
13//! - 🌐 **Multi-Transport Execution**: Boot HTTP, gRPC, and background listeners concurrently.
14//! - 🛡️ **Graceful Shutdown Orchestration**: Catches OS termination signals and shuts down active transport threads cleanly.
15//! - ⚡ **Integration with Tower & Sentry**: Built-in support for middleware, cors, timeouts, and error capturing.
16//!
17//! ## Example Usage
18//! ```rust,no_run
19//! use ferrox_app::FerroxApp;
20//! use ferrox_transports::http::HttpTransport;
21//! use axum::{Router, routing::get};
22//!
23//! #[tokio::main]
24//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
25//!     let router = Router::new().route("/health", get(|| async { "OK" }));
26//!     let transport = HttpTransport::new(router, 8080);
27//!
28//!     FerroxApp::new()
29//!         .add_transport(transport)
30//!         .start()
31//!         .await?;
32//!
33//!     Ok(())
34//! }
35//! ```
36
37use std::sync::Arc;
38use tokio::signal;
39use tokio::task::JoinHandle;
40use tracing::{info, error};
41use ferrox_errors::AppError;
42use ferrox_transports::Transport;
43
44pub struct FerroxApp {
45    transports: Vec<Arc<dyn Transport>>,
46}
47
48impl Default for FerroxApp {
49    fn default() -> Self {
50        Self::new()
51    }
52}
53
54impl FerroxApp {
55    pub fn new() -> Self {
56        Self {
57            transports: Vec::new(),
58        }
59    }
60
61    /// Add a transport layer to the app (e.g. HttpTransport, GrpcTransport, FtpTransport)
62    pub fn add_transport<T: Transport + 'static>(mut self, transport: T) -> Self {
63        self.transports.push(Arc::new(transport));
64        self
65    }
66
67    /// Starts all configured transports concurrently and waits for shutdown signal
68    pub async fn start(self) -> Result<(), AppError> {
69        info!("Starting FerroxApp multi-transport system...");
70
71        if self.transports.is_empty() {
72            return Err(AppError::InternalServerError(
73                "Cannot start FerroxApp: no transports configured!".into(),
74            ));
75        }
76
77        let mut join_handles: Vec<JoinHandle<Result<(), AppError>>> = Vec::new();
78
79        for transport in self.transports {
80            let t = Arc::clone(&transport);
81            
82            let handle = tokio::spawn(async move {
83                info!("Booting transport: {}", t.name());
84                if let Err(e) = t.start().await {
85                    error!("Transport {} crashed: {:?}", t.name(), e);
86                    return Err(e);
87                }
88                Ok(())
89            });
90            
91            join_handles.push(handle);
92        }
93
94        // Wait for shutdown signal
95        shutdown_signal().await;
96
97        info!("Graceful shutdown initiated...");
98        
99        // In a real implementation we would send a cancellation token to all join handles
100        for handle in join_handles {
101            handle.abort();
102        }
103
104        info!("FerroxApp stopped gracefully.");
105        Ok(())
106    }
107}
108
109async fn shutdown_signal() {
110    let ctrl_c = async {
111        signal::ctrl_c()
112            .await
113            .expect("failed to install Ctrl+C handler");
114    };
115
116    #[cfg(unix)]
117    let terminate = async {
118        signal::unix::signal(signal::unix::SignalKind::terminate())
119            .expect("failed to install signal handler")
120            .recv()
121            .await;
122    };
123
124    #[cfg(not(unix))]
125    let terminate = std::future::pending::<()>();
126
127    tokio::select! {
128        _ = ctrl_c => {},
129        _ = terminate => {},
130    }
131
132    info!("Shutdown signal received, starting graceful shutdown...");
133}
134
135pub fn setup() {
136    println!("ferrox-app initialized: Multi-Transport FerroxApp bootstrap ready.");
137}