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}