Skip to main content

dcp/shutdown/
mod.rs

1//! Graceful shutdown coordination for DCP server.
2//!
3//! Provides shutdown coordination with in-flight request tracking,
4//! configurable drain timeout, and signal handling.
5
6mod signals;
7
8use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
9use std::sync::Arc;
10use std::time::{Duration, Instant};
11use tokio::sync::broadcast;
12
13pub use signals::{setup_default_handlers, wait_for_ctrl_c, Signal, SignalHandler};
14
15/// Shutdown coordinator for graceful server shutdown
16pub struct ShutdownCoordinator {
17    /// Shutdown signal flag
18    shutdown: Arc<AtomicBool>,
19    /// In-flight request counter
20    in_flight: Arc<AtomicU64>,
21    /// Shutdown notify channel
22    notify: broadcast::Sender<()>,
23    /// Drain timeout
24    drain_timeout: Duration,
25    /// Whether shutdown has been initiated
26    initiated: AtomicBool,
27}
28
29impl ShutdownCoordinator {
30    /// Create a new shutdown coordinator
31    pub fn new(drain_timeout: Duration) -> Self {
32        let (notify, _) = broadcast::channel(16);
33        Self {
34            shutdown: Arc::new(AtomicBool::new(false)),
35            in_flight: Arc::new(AtomicU64::new(0)),
36            notify,
37            drain_timeout,
38            initiated: AtomicBool::new(false),
39        }
40    }
41
42    /// Create with default 30 second drain timeout
43    pub fn with_default_timeout() -> Self {
44        Self::new(Duration::from_secs(30))
45    }
46
47    /// Signal shutdown
48    pub fn shutdown(&self) {
49        if self.initiated.swap(true, Ordering::SeqCst) {
50            // Already initiated
51            return;
52        }
53        self.shutdown.store(true, Ordering::SeqCst);
54        let _ = self.notify.send(());
55    }
56
57    /// Check if shutdown has been requested
58    pub fn is_shutdown(&self) -> bool {
59        self.shutdown.load(Ordering::SeqCst)
60    }
61
62    /// Get current in-flight request count
63    pub fn in_flight_count(&self) -> u64 {
64        self.in_flight.load(Ordering::Acquire)
65    }
66
67    /// Wait for all in-flight requests to complete with timeout
68    /// Returns true if all requests completed, false if timeout
69    pub async fn wait_drain(&self) -> bool {
70        let deadline = Instant::now() + self.drain_timeout;
71
72        while self.in_flight.load(Ordering::Acquire) > 0 {
73            if Instant::now() > deadline {
74                return false;
75            }
76            tokio::time::sleep(Duration::from_millis(10)).await;
77        }
78        true
79    }
80
81    /// Wait for drain with custom timeout
82    pub async fn wait_drain_with_timeout(&self, timeout: Duration) -> bool {
83        let deadline = Instant::now() + timeout;
84
85        while self.in_flight.load(Ordering::Acquire) > 0 {
86            if Instant::now() > deadline {
87                return false;
88            }
89            tokio::time::sleep(Duration::from_millis(10)).await;
90        }
91        true
92    }
93
94    /// Track request start - returns a guard that decrements on drop
95    pub fn request_start(&self) -> RequestGuard {
96        if self.is_shutdown() {
97            return RequestGuard { counter: None };
98        }
99
100        self.in_flight.fetch_add(1, Ordering::AcqRel);
101        if self.is_shutdown() {
102            self.decrement_in_flight();
103            return RequestGuard { counter: None };
104        }
105
106        RequestGuard {
107            counter: Some(Arc::clone(&self.in_flight)),
108        }
109    }
110
111    /// Try to track request start, rejecting new work after shutdown begins.
112    pub fn try_request_start(&self) -> Option<RequestGuard> {
113        let guard = self.request_start();
114        guard.is_active().then_some(guard)
115    }
116
117    /// Manually increment in-flight counter
118    pub fn increment_in_flight(&self) {
119        if self.is_shutdown() {
120            return;
121        }
122        self.in_flight.fetch_add(1, Ordering::AcqRel);
123        if self.is_shutdown() {
124            self.decrement_in_flight();
125        }
126    }
127
128    /// Manually decrement in-flight counter
129    pub fn decrement_in_flight(&self) {
130        let _ = self
131            .in_flight
132            .fetch_update(Ordering::AcqRel, Ordering::Acquire, |value| {
133                value.checked_sub(1)
134            });
135    }
136
137    /// Subscribe to shutdown notifications
138    pub fn subscribe(&self) -> broadcast::Receiver<()> {
139        self.notify.subscribe()
140    }
141
142    /// Get the drain timeout
143    pub fn drain_timeout(&self) -> Duration {
144        self.drain_timeout
145    }
146
147    /// Set the drain timeout
148    pub fn set_drain_timeout(&mut self, timeout: Duration) {
149        self.drain_timeout = timeout;
150    }
151
152    /// Create a shared reference
153    pub fn into_arc(self) -> Arc<Self> {
154        Arc::new(self)
155    }
156}
157
158impl Default for ShutdownCoordinator {
159    fn default() -> Self {
160        Self::with_default_timeout()
161    }
162}
163
164/// RAII guard for request tracking
165/// Automatically decrements the in-flight counter when dropped
166pub struct RequestGuard {
167    counter: Option<Arc<AtomicU64>>,
168}
169
170impl RequestGuard {
171    /// Whether this guard represents admitted in-flight work.
172    pub fn is_active(&self) -> bool {
173        self.counter.is_some()
174    }
175}
176
177impl Drop for RequestGuard {
178    fn drop(&mut self) {
179        if let Some(counter) = &self.counter {
180            let _ = counter.fetch_update(Ordering::AcqRel, Ordering::Acquire, |value| {
181                value.checked_sub(1)
182            });
183        }
184    }
185}
186
187/// Shutdown progress for logging
188#[derive(Debug, Clone)]
189pub struct ShutdownProgress {
190    /// Whether shutdown has been initiated
191    pub initiated: bool,
192    /// Current in-flight request count
193    pub in_flight: u64,
194    /// Time since shutdown was initiated
195    pub elapsed: Option<Duration>,
196    /// Drain timeout
197    pub timeout: Duration,
198}
199
200impl ShutdownProgress {
201    /// Format progress as a log message
202    pub fn to_log_message(&self) -> String {
203        if !self.initiated {
204            return "Shutdown not initiated".to_string();
205        }
206
207        let elapsed_str = self
208            .elapsed
209            .map(|d| format!("{:.1}s", d.as_secs_f64()))
210            .unwrap_or_else(|| "unknown".to_string());
211
212        let timeout_str = format!("{:.1}s", self.timeout.as_secs_f64());
213
214        if self.in_flight == 0 {
215            format!("Shutdown complete after {}", elapsed_str)
216        } else {
217            format!(
218                "Shutdown in progress: {} in-flight requests, elapsed: {}, timeout: {}",
219                self.in_flight, elapsed_str, timeout_str
220            )
221        }
222    }
223
224    /// Check if shutdown is complete
225    pub fn is_complete(&self) -> bool {
226        self.initiated && self.in_flight == 0
227    }
228
229    /// Check if shutdown has timed out
230    pub fn is_timed_out(&self) -> bool {
231        if let Some(elapsed) = self.elapsed {
232            elapsed >= self.timeout
233        } else {
234            false
235        }
236    }
237}
238
239/// Shutdown logger for progress reporting
240pub struct ShutdownLogger {
241    /// Shutdown coordinator
242    coordinator: Arc<ShutdownCoordinator>,
243    /// Shutdown start time
244    start_time: std::sync::Mutex<Option<Instant>>,
245    /// Log interval
246    log_interval: Duration,
247}
248
249impl ShutdownLogger {
250    /// Create a new shutdown logger
251    pub fn new(coordinator: Arc<ShutdownCoordinator>, log_interval: Duration) -> Self {
252        Self {
253            coordinator,
254            start_time: std::sync::Mutex::new(None),
255            log_interval,
256        }
257    }
258
259    /// Create with default 1 second log interval
260    pub fn with_default_interval(coordinator: Arc<ShutdownCoordinator>) -> Self {
261        Self::new(coordinator, Duration::from_secs(1))
262    }
263
264    /// Mark shutdown as started
265    pub fn start(&self) {
266        let mut start_time = self.start_time.lock().unwrap();
267        if start_time.is_none() {
268            *start_time = Some(Instant::now());
269        }
270    }
271
272    /// Get current progress with elapsed time
273    pub fn progress(&self) -> ShutdownProgress {
274        let elapsed = self.start_time.lock().unwrap().map(|t| t.elapsed());
275        ShutdownProgress {
276            initiated: self.coordinator.is_shutdown(),
277            in_flight: self.coordinator.in_flight_count(),
278            elapsed,
279            timeout: self.coordinator.drain_timeout(),
280        }
281    }
282
283    /// Log current progress to stderr
284    pub fn log_progress(&self) {
285        let progress = self.progress();
286        eprintln!("[SHUTDOWN] {}", progress.to_log_message());
287    }
288
289    /// Run periodic progress logging until shutdown completes or times out
290    pub async fn run_logging(&self) {
291        self.start();
292
293        loop {
294            let progress = self.progress();
295
296            // Log progress
297            eprintln!("[SHUTDOWN] {}", progress.to_log_message());
298
299            // Check if complete or timed out
300            if progress.is_complete() {
301                eprintln!("[SHUTDOWN] Graceful shutdown complete");
302                break;
303            }
304
305            if progress.is_timed_out() {
306                eprintln!(
307                    "[SHUTDOWN] Timeout reached with {} requests still in-flight",
308                    progress.in_flight
309                );
310                break;
311            }
312
313            // Wait for next log interval
314            tokio::time::sleep(self.log_interval).await;
315        }
316    }
317
318    /// Get the log interval
319    pub fn log_interval(&self) -> Duration {
320        self.log_interval
321    }
322}
323
324impl ShutdownCoordinator {
325    /// Get current shutdown progress
326    pub fn progress(&self) -> ShutdownProgress {
327        ShutdownProgress {
328            initiated: self.is_shutdown(),
329            in_flight: self.in_flight_count(),
330            elapsed: None, // Use ShutdownLogger for elapsed time tracking
331            timeout: self.drain_timeout,
332        }
333    }
334
335    /// Create a logger for this coordinator
336    pub fn create_logger(self: &Arc<Self>) -> ShutdownLogger {
337        ShutdownLogger::with_default_interval(Arc::clone(self))
338    }
339
340    /// Create a logger with custom interval
341    pub fn create_logger_with_interval(self: &Arc<Self>, interval: Duration) -> ShutdownLogger {
342        ShutdownLogger::new(Arc::clone(self), interval)
343    }
344}
345
346#[cfg(test)]
347mod tests {
348    use super::*;
349
350    #[test]
351    fn test_shutdown_coordinator_creation() {
352        let coord = ShutdownCoordinator::new(Duration::from_secs(10));
353        assert!(!coord.is_shutdown());
354        assert_eq!(coord.in_flight_count(), 0);
355    }
356
357    #[test]
358    fn test_shutdown_signal() {
359        let coord = ShutdownCoordinator::default();
360        assert!(!coord.is_shutdown());
361
362        coord.shutdown();
363        assert!(coord.is_shutdown());
364
365        // Multiple shutdowns should be idempotent
366        coord.shutdown();
367        assert!(coord.is_shutdown());
368    }
369
370    #[test]
371    fn test_request_guard() {
372        let coord = ShutdownCoordinator::default();
373        assert_eq!(coord.in_flight_count(), 0);
374
375        {
376            let _guard1 = coord.request_start();
377            assert_eq!(coord.in_flight_count(), 1);
378
379            {
380                let _guard2 = coord.request_start();
381                assert_eq!(coord.in_flight_count(), 2);
382            }
383
384            assert_eq!(coord.in_flight_count(), 1);
385        }
386
387        assert_eq!(coord.in_flight_count(), 0);
388    }
389
390    #[test]
391    fn test_manual_increment_decrement() {
392        let coord = ShutdownCoordinator::default();
393
394        coord.increment_in_flight();
395        coord.increment_in_flight();
396        assert_eq!(coord.in_flight_count(), 2);
397
398        coord.decrement_in_flight();
399        assert_eq!(coord.in_flight_count(), 1);
400
401        coord.decrement_in_flight();
402        assert_eq!(coord.in_flight_count(), 0);
403    }
404
405    #[tokio::test]
406    async fn test_wait_drain_immediate() {
407        let coord = ShutdownCoordinator::new(Duration::from_millis(100));
408
409        // No in-flight requests, should return immediately
410        let result = coord.wait_drain().await;
411        assert!(result);
412    }
413
414    #[tokio::test]
415    async fn test_wait_drain_with_requests() {
416        let coord = Arc::new(ShutdownCoordinator::new(Duration::from_secs(1)));
417        let coord_clone = Arc::clone(&coord);
418
419        // Start a request
420        let guard = coord.request_start();
421
422        // Spawn task to complete request after delay
423        tokio::spawn(async move {
424            tokio::time::sleep(Duration::from_millis(50)).await;
425            drop(guard);
426        });
427
428        // Wait for drain
429        let result = coord_clone.wait_drain().await;
430        assert!(result);
431        assert_eq!(coord_clone.in_flight_count(), 0);
432    }
433
434    #[tokio::test]
435    async fn test_wait_drain_timeout() {
436        let coord = ShutdownCoordinator::new(Duration::from_millis(50));
437
438        // Start a request that won't complete
439        let _guard = coord.request_start();
440
441        // Wait for drain should timeout
442        let result = coord.wait_drain().await;
443        assert!(!result);
444        assert_eq!(coord.in_flight_count(), 1);
445    }
446
447    #[tokio::test]
448    async fn test_shutdown_notification() {
449        let coord = ShutdownCoordinator::default();
450        let mut rx = coord.subscribe();
451
452        // Shutdown in another task
453        let coord_clone = Arc::new(coord);
454        let coord_for_shutdown = Arc::clone(&coord_clone);
455
456        tokio::spawn(async move {
457            tokio::time::sleep(Duration::from_millis(10)).await;
458            coord_for_shutdown.shutdown();
459        });
460
461        // Wait for notification
462        let result = tokio::time::timeout(Duration::from_millis(100), rx.recv()).await;
463        assert!(result.is_ok());
464    }
465
466    #[test]
467    fn test_shutdown_progress() {
468        let coord = ShutdownCoordinator::new(Duration::from_secs(30));
469
470        let progress = coord.progress();
471        assert!(!progress.initiated);
472        assert_eq!(progress.in_flight, 0);
473        assert_eq!(progress.timeout, Duration::from_secs(30));
474
475        coord.increment_in_flight();
476        coord.shutdown();
477
478        let progress = coord.progress();
479        assert!(progress.initiated);
480        assert_eq!(progress.in_flight, 1);
481    }
482}