adaptive_pipeline_bootstrap/
signals.rs

1// /////////////////////////////////////////////////////////////////////////////
2// Adaptive Pipeline
3// Copyright (c) 2025 Michael Gardner, A Bit of Help, Inc.
4// SPDX-License-Identifier: BSD-3-Clause
5// See LICENSE file in the project root.
6// /////////////////////////////////////////////////////////////////////////////
7
8//! # System Signal Handling
9//!
10//! Cross-platform signal handling for graceful shutdown.
11//!
12//! ## Supported Signals
13//!
14//! - **SIGTERM** (15) - Graceful shutdown request
15//! - **SIGINT** (2) - User interrupt (Ctrl+C)
16//! - **SIGHUP** (1) - Hangup (terminal closed)
17//!
18//! ## Design Pattern
19//!
20//! The signal handler provides:
21//! - **Async signal handling** via tokio
22//! - **Trait abstraction** for testing
23//! - **Callback-based** shutdown initiation
24//! - **Platform-specific** implementations (Unix vs Windows)
25//!
26//! ## Usage
27//!
28//! ```rust,no_run
29//! use adaptive_pipeline_bootstrap::signals::{SystemSignals, UnixSignalHandler};
30//! use std::sync::atomic::{AtomicBool, Ordering};
31//! use std::sync::Arc;
32//!
33//! #[tokio::main]
34//! async fn main() {
35//!     let shutdown_flag = Arc::new(AtomicBool::new(false));
36//!     let flag_clone = shutdown_flag.clone();
37//!
38//!     let signal_handler = UnixSignalHandler::new();
39//!
40//!     // Install signal handlers
41//!     tokio::spawn(async move {
42//!         let callback = Box::new(move || {
43//!             flag_clone.store(true, Ordering::SeqCst);
44//!         });
45//!         signal_handler.wait_for_signal(callback).await;
46//!     });
47//!
48//!     // Main application loop
49//!     while !shutdown_flag.load(Ordering::SeqCst) {
50//!         // Application work...
51//!         tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
52//!     }
53//! }
54//! ```
55
56use std::future::Future;
57use std::pin::Pin;
58
59/// Callback type for shutdown notification
60pub type ShutdownCallback = Box<dyn FnOnce() + Send + 'static>;
61
62/// System signal handling trait
63///
64/// Abstracts platform-specific signal handling for graceful shutdown.
65pub trait SystemSignals: Send + Sync {
66    /// Wait for a shutdown signal and invoke the callback
67    ///
68    /// This method blocks until one of the shutdown signals is received:
69    /// - SIGTERM
70    /// - SIGINT
71    /// - SIGHUP (Unix only)
72    ///
73    /// When a signal is received, the provided callback is invoked to
74    /// initiate graceful shutdown.
75    fn wait_for_signal(&self, on_shutdown: ShutdownCallback) -> Pin<Box<dyn Future<Output = ()> + Send + '_>>;
76}
77
78/// Unix signal handler implementation
79///
80/// Handles SIGTERM, SIGINT, and SIGHUP using tokio::signal.
81#[cfg(unix)]
82pub struct UnixSignalHandler;
83
84#[cfg(unix)]
85impl UnixSignalHandler {
86    /// Create a new Unix signal handler
87    pub fn new() -> Self {
88        Self
89    }
90}
91
92#[cfg(unix)]
93impl Default for UnixSignalHandler {
94    fn default() -> Self {
95        Self::new()
96    }
97}
98
99#[cfg(unix)]
100impl SystemSignals for UnixSignalHandler {
101    fn wait_for_signal(&self, on_shutdown: ShutdownCallback) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
102        Box::pin(async move {
103            use tokio::signal::unix::{signal, SignalKind};
104
105            // Attempt to register signal handlers
106            // If registration fails, log error and exit gracefully (no shutdown triggered)
107            let mut sigterm = match signal(SignalKind::terminate()) {
108                Ok(sig) => sig,
109                Err(e) => {
110                    tracing::error!("Failed to register SIGTERM handler: {}", e);
111                    return;
112                }
113            };
114            let mut sigint = match signal(SignalKind::interrupt()) {
115                Ok(sig) => sig,
116                Err(e) => {
117                    tracing::error!("Failed to register SIGINT handler: {}", e);
118                    return;
119                }
120            };
121            let mut sighup = match signal(SignalKind::hangup()) {
122                Ok(sig) => sig,
123                Err(e) => {
124                    tracing::error!("Failed to register SIGHUP handler: {}", e);
125                    return;
126                }
127            };
128
129            tokio::select! {
130                _ = sigterm.recv() => {
131                    tracing::info!("Received SIGTERM, initiating graceful shutdown");
132                }
133                _ = sigint.recv() => {
134                    tracing::info!("Received SIGINT (Ctrl+C), initiating graceful shutdown");
135                }
136                _ = sighup.recv() => {
137                    tracing::info!("Received SIGHUP, initiating graceful shutdown");
138                }
139            }
140
141            on_shutdown();
142        })
143    }
144}
145
146/// Windows signal handler implementation
147///
148/// Handles Ctrl+C and Ctrl+Break on Windows.
149#[cfg(windows)]
150pub struct WindowsSignalHandler;
151
152#[cfg(windows)]
153impl WindowsSignalHandler {
154    /// Create a new Windows signal handler
155    pub fn new() -> Self {
156        Self
157    }
158}
159
160#[cfg(windows)]
161impl Default for WindowsSignalHandler {
162    fn default() -> Self {
163        Self::new()
164    }
165}
166
167#[cfg(windows)]
168impl SystemSignals for WindowsSignalHandler {
169    fn wait_for_signal(&self, on_shutdown: ShutdownCallback) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
170        Box::pin(async move {
171            // On Windows, tokio provides ctrl_c signal
172            if let Err(e) = tokio::signal::ctrl_c().await {
173                tracing::error!("Failed to register Ctrl+C handler: {}", e);
174                return;
175            }
176
177            tracing::info!("Received Ctrl+C, initiating graceful shutdown");
178            on_shutdown();
179        })
180    }
181}
182
183/// No-op signal handler for testing
184///
185/// Never receives signals, allowing tests to control shutdown explicitly.
186pub struct NoOpSignalHandler;
187
188impl NoOpSignalHandler {
189    /// Create a new no-op signal handler
190    pub fn new() -> Self {
191        Self
192    }
193}
194
195impl Default for NoOpSignalHandler {
196    fn default() -> Self {
197        Self::new()
198    }
199}
200
201impl SystemSignals for NoOpSignalHandler {
202    fn wait_for_signal(&self, _on_shutdown: ShutdownCallback) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
203        // Never completes - perfect for testing
204        Box::pin(async move {
205            std::future::pending::<()>().await;
206        })
207    }
208}
209
210/// Create platform-specific signal handler
211///
212/// Returns the appropriate signal handler for the current platform:
213/// - Unix: `UnixSignalHandler`
214/// - Windows: `WindowsSignalHandler`
215pub fn create_signal_handler() -> Box<dyn SystemSignals> {
216    #[cfg(unix)]
217    {
218        Box::new(UnixSignalHandler::new())
219    }
220
221    #[cfg(windows)]
222    {
223        Box::new(WindowsSignalHandler::new())
224    }
225
226    #[cfg(not(any(unix, windows)))]
227    {
228        compile_error!("Unsupported platform for signal handling");
229    }
230}
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235    use std::sync::atomic::{AtomicBool, Ordering};
236    use std::sync::Arc;
237
238    #[tokio::test]
239    async fn test_noop_signal_handler() {
240        let handler = NoOpSignalHandler::new();
241        let called = Arc::new(AtomicBool::new(false));
242        let called_clone = called.clone();
243
244        // Start waiting for signal (will never complete)
245        let callback = Box::new(move || {
246            called_clone.store(true, Ordering::SeqCst);
247        });
248        let wait_future = handler.wait_for_signal(callback);
249
250        // Race the signal wait against a timeout
251        tokio::select! {
252            _ = wait_future => {
253                panic!("NoOp handler should never complete");
254            }
255            _ = tokio::time::sleep(tokio::time::Duration::from_millis(100)) => {
256                // Expected - timeout wins
257            }
258        }
259
260        // Callback should not have been called
261        assert!(!called.load(Ordering::SeqCst));
262    }
263
264    #[test]
265    fn test_create_signal_handler() {
266        // Just verify it doesn't panic
267        let _handler = create_signal_handler();
268    }
269
270    #[cfg(unix)]
271    #[test]
272    fn test_unix_signal_handler_creation() {
273        let _handler = UnixSignalHandler::new();
274        let _handler = UnixSignalHandler;
275    }
276
277    #[cfg(windows)]
278    #[test]
279    fn test_windows_signal_handler_creation() {
280        let _handler = WindowsSignalHandler::new();
281        let _handler = WindowsSignalHandler::default();
282    }
283}