Skip to main content

xds_server/
shutdown.rs

1//! Graceful shutdown handling for the xDS server.
2//!
3//! This module provides signal handling and graceful shutdown coordination
4//! for the xDS server, ensuring in-flight requests complete before termination.
5//!
6//! # Example
7//!
8//! ```rust,no_run
9//! use xds_server::shutdown::ShutdownController;
10//! use std::time::Duration;
11//!
12//! # async fn example() {
13//! let controller = ShutdownController::new();
14//!
15//! // In your server startup
16//! let _shutdown_rx = controller.subscribe();
17//!
18//! // When shutdown is triggered
19//! controller.shutdown(Duration::from_secs(30)).await;
20//! # }
21//! ```
22
23use std::sync::atomic::{AtomicBool, Ordering};
24use std::sync::Arc;
25use std::time::Duration;
26
27use tokio::sync::watch;
28use tokio::time::timeout;
29use tracing::{info, warn};
30
31/// Controller for coordinating graceful shutdown.
32///
33/// Provides mechanisms to:
34/// - Subscribe to shutdown signals
35/// - Trigger shutdown programmatically
36/// - Wait for in-flight operations to complete
37/// - Set shutdown timeout
38#[derive(Debug, Clone)]
39pub struct ShutdownController {
40    inner: Arc<ShutdownInner>,
41}
42
43#[derive(Debug)]
44struct ShutdownInner {
45    /// Whether shutdown has been initiated.
46    initiated: AtomicBool,
47    /// Sender for shutdown signal.
48    tx: watch::Sender<bool>,
49    /// Receiver for shutdown signal.
50    rx: watch::Receiver<bool>,
51    /// Active operation counter.
52    active_ops: AtomicCounter,
53}
54
55/// Atomic counter for tracking active operations.
56#[derive(Debug, Default)]
57struct AtomicCounter {
58    count: std::sync::atomic::AtomicUsize,
59}
60
61impl AtomicCounter {
62    fn increment(&self) -> usize {
63        self.count.fetch_add(1, Ordering::SeqCst) + 1
64    }
65
66    fn decrement(&self) -> usize {
67        self.count.fetch_sub(1, Ordering::SeqCst) - 1
68    }
69
70    fn get(&self) -> usize {
71        self.count.load(Ordering::SeqCst)
72    }
73}
74
75impl Default for ShutdownController {
76    fn default() -> Self {
77        Self::new()
78    }
79}
80
81impl ShutdownController {
82    /// Create a new shutdown controller.
83    pub fn new() -> Self {
84        let (tx, rx) = watch::channel(false);
85        Self {
86            inner: Arc::new(ShutdownInner {
87                initiated: AtomicBool::new(false),
88                tx,
89                rx,
90                active_ops: AtomicCounter::default(),
91            }),
92        }
93    }
94
95    /// Subscribe to shutdown notifications.
96    ///
97    /// Returns a receiver that will be notified when shutdown is initiated.
98    pub fn subscribe(&self) -> watch::Receiver<bool> {
99        self.inner.rx.clone()
100    }
101
102    /// Check if shutdown has been initiated.
103    pub fn is_shutdown(&self) -> bool {
104        self.inner.initiated.load(Ordering::SeqCst)
105    }
106
107    /// Get a future that resolves when shutdown is initiated.
108    pub fn shutdown_signal(&self) -> ShutdownSignal {
109        ShutdownSignal {
110            rx: self.inner.rx.clone(),
111        }
112    }
113
114    /// Initiate graceful shutdown.
115    ///
116    /// This will:
117    /// 1. Set the shutdown flag
118    /// 2. Notify all subscribers
119    /// 3. Wait for active operations to complete (with timeout)
120    ///
121    /// Returns `true` if all operations completed gracefully, `false` if timed out.
122    pub async fn shutdown(&self, grace_period: Duration) -> bool {
123        // Mark as initiated
124        if self
125            .inner
126            .initiated
127            .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
128            .is_err()
129        {
130            // Already initiated
131            return true;
132        }
133
134        info!("initiating graceful shutdown with {:?} grace period", grace_period);
135
136        // Notify all subscribers
137        let _ = self.inner.tx.send(true);
138
139        // Wait for active operations to complete
140        let result = timeout(grace_period, self.wait_for_completion()).await;
141
142        match result {
143            Ok(()) => {
144                info!("graceful shutdown completed");
145                true
146            }
147            Err(_) => {
148                let remaining = self.inner.active_ops.get();
149                warn!(
150                    remaining_ops = remaining,
151                    "graceful shutdown timed out, forcing shutdown"
152                );
153                false
154            }
155        }
156    }
157
158    /// Wait for all active operations to complete.
159    async fn wait_for_completion(&self) {
160        loop {
161            if self.inner.active_ops.get() == 0 {
162                break;
163            }
164            tokio::time::sleep(Duration::from_millis(100)).await;
165        }
166    }
167
168    /// Register an active operation.
169    ///
170    /// Returns a guard that decrements the counter when dropped.
171    pub fn register_operation(&self) -> OperationGuard {
172        self.inner.active_ops.increment();
173        OperationGuard {
174            controller: self.clone(),
175        }
176    }
177
178    /// Get the number of active operations.
179    pub fn active_operations(&self) -> usize {
180        self.inner.active_ops.get()
181    }
182}
183
184/// Guard for tracking an active operation.
185///
186/// Decrements the active operation counter when dropped.
187#[derive(Debug)]
188pub struct OperationGuard {
189    controller: ShutdownController,
190}
191
192impl Drop for OperationGuard {
193    fn drop(&mut self) {
194        self.controller.inner.active_ops.decrement();
195    }
196}
197
198/// Future that resolves when shutdown is initiated.
199#[derive(Debug, Clone)]
200pub struct ShutdownSignal {
201    rx: watch::Receiver<bool>,
202}
203
204impl ShutdownSignal {
205    /// Wait for the shutdown signal.
206    pub async fn wait(mut self) {
207        loop {
208            if *self.rx.borrow() {
209                return;
210            }
211            if self.rx.changed().await.is_err() {
212                // Channel closed, treat as shutdown
213                return;
214            }
215            if *self.rx.borrow() {
216                return;
217            }
218        }
219    }
220}
221
222/// Wait for OS shutdown signals (SIGTERM, SIGINT).
223///
224/// This function returns when either signal is received.
225//
226// `expect()` here is intentional: signal handler installation can only fail
227// at startup under catastrophic OS conditions (e.g. no signal subsystem
228// available), and there is no useful recovery path — a control plane that
229// cannot receive shutdown signals is unsafe to keep running.
230#[allow(clippy::expect_used)]
231pub async fn wait_for_signal() {
232    #[cfg(unix)]
233    {
234        use tokio::signal::unix::{signal, SignalKind};
235
236        let mut sigterm = signal(SignalKind::terminate()).expect("failed to install SIGTERM handler");
237        let mut sigint = signal(SignalKind::interrupt()).expect("failed to install SIGINT handler");
238
239        tokio::select! {
240            _ = sigterm.recv() => {
241                info!("received SIGTERM");
242            }
243            _ = sigint.recv() => {
244                info!("received SIGINT");
245            }
246        }
247    }
248
249    #[cfg(not(unix))]
250    {
251        tokio::signal::ctrl_c()
252            .await
253            .expect("failed to install Ctrl+C handler");
254        info!("received Ctrl+C");
255    }
256}
257
258/// Shutdown configuration.
259#[derive(Debug, Clone)]
260pub struct ShutdownConfig {
261    /// Grace period for shutdown.
262    pub grace_period: Duration,
263    /// Whether to listen for OS signals.
264    pub listen_for_signals: bool,
265}
266
267impl Default for ShutdownConfig {
268    fn default() -> Self {
269        Self {
270            grace_period: Duration::from_secs(30),
271            listen_for_signals: true,
272        }
273    }
274}
275
276#[cfg(test)]
277mod tests {
278    use super::*;
279
280    #[test]
281    fn shutdown_controller_creation() {
282        let controller = ShutdownController::new();
283        assert!(!controller.is_shutdown());
284        assert_eq!(controller.active_operations(), 0);
285    }
286
287    #[test]
288    fn operation_tracking() {
289        let controller = ShutdownController::new();
290
291        {
292            let _guard1 = controller.register_operation();
293            assert_eq!(controller.active_operations(), 1);
294
295            let _guard2 = controller.register_operation();
296            assert_eq!(controller.active_operations(), 2);
297        }
298
299        assert_eq!(controller.active_operations(), 0);
300    }
301
302    #[tokio::test]
303    async fn shutdown_signal() {
304        let controller = ShutdownController::new();
305        let mut rx = controller.subscribe();
306
307        // Trigger shutdown in background
308        let controller_clone = controller.clone();
309        tokio::spawn(async move {
310            tokio::time::sleep(Duration::from_millis(50)).await;
311            controller_clone.shutdown(Duration::from_millis(100)).await;
312        });
313
314        // Wait for signal
315        rx.changed().await.expect("should receive shutdown signal");
316        assert!(controller.is_shutdown());
317    }
318
319    #[tokio::test]
320    async fn shutdown_waits_for_operations() {
321        let controller = ShutdownController::new();
322
323        // Register an operation
324        let guard = controller.register_operation();
325        assert_eq!(controller.active_operations(), 1);
326
327        // Start shutdown in background
328        let controller_clone = controller.clone();
329        let handle = tokio::spawn(async move {
330            controller_clone.shutdown(Duration::from_secs(5)).await
331        });
332
333        // Wait a bit then drop the guard
334        tokio::time::sleep(Duration::from_millis(100)).await;
335        drop(guard);
336
337        // Shutdown should complete
338        let result = handle.await.expect("shutdown task should complete");
339        assert!(result);
340    }
341
342    #[test]
343    fn shutdown_config_defaults() {
344        let config = ShutdownConfig::default();
345        assert_eq!(config.grace_period, Duration::from_secs(30));
346        assert!(config.listen_for_signals);
347    }
348}