1mod 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
15pub struct ShutdownCoordinator {
17 shutdown: Arc<AtomicBool>,
19 in_flight: Arc<AtomicU64>,
21 notify: broadcast::Sender<()>,
23 drain_timeout: Duration,
25 initiated: AtomicBool,
27}
28
29impl ShutdownCoordinator {
30 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 pub fn with_default_timeout() -> Self {
44 Self::new(Duration::from_secs(30))
45 }
46
47 pub fn shutdown(&self) {
49 if self.initiated.swap(true, Ordering::SeqCst) {
50 return;
52 }
53 self.shutdown.store(true, Ordering::SeqCst);
54 let _ = self.notify.send(());
55 }
56
57 pub fn is_shutdown(&self) -> bool {
59 self.shutdown.load(Ordering::SeqCst)
60 }
61
62 pub fn in_flight_count(&self) -> u64 {
64 self.in_flight.load(Ordering::Acquire)
65 }
66
67 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 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 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 pub fn try_request_start(&self) -> Option<RequestGuard> {
113 let guard = self.request_start();
114 guard.is_active().then_some(guard)
115 }
116
117 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 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 pub fn subscribe(&self) -> broadcast::Receiver<()> {
139 self.notify.subscribe()
140 }
141
142 pub fn drain_timeout(&self) -> Duration {
144 self.drain_timeout
145 }
146
147 pub fn set_drain_timeout(&mut self, timeout: Duration) {
149 self.drain_timeout = timeout;
150 }
151
152 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
164pub struct RequestGuard {
167 counter: Option<Arc<AtomicU64>>,
168}
169
170impl RequestGuard {
171 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#[derive(Debug, Clone)]
189pub struct ShutdownProgress {
190 pub initiated: bool,
192 pub in_flight: u64,
194 pub elapsed: Option<Duration>,
196 pub timeout: Duration,
198}
199
200impl ShutdownProgress {
201 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 pub fn is_complete(&self) -> bool {
226 self.initiated && self.in_flight == 0
227 }
228
229 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
239pub struct ShutdownLogger {
241 coordinator: Arc<ShutdownCoordinator>,
243 start_time: std::sync::Mutex<Option<Instant>>,
245 log_interval: Duration,
247}
248
249impl ShutdownLogger {
250 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 pub fn with_default_interval(coordinator: Arc<ShutdownCoordinator>) -> Self {
261 Self::new(coordinator, Duration::from_secs(1))
262 }
263
264 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 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 pub fn log_progress(&self) {
285 let progress = self.progress();
286 eprintln!("[SHUTDOWN] {}", progress.to_log_message());
287 }
288
289 pub async fn run_logging(&self) {
291 self.start();
292
293 loop {
294 let progress = self.progress();
295
296 eprintln!("[SHUTDOWN] {}", progress.to_log_message());
298
299 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 tokio::time::sleep(self.log_interval).await;
315 }
316 }
317
318 pub fn log_interval(&self) -> Duration {
320 self.log_interval
321 }
322}
323
324impl ShutdownCoordinator {
325 pub fn progress(&self) -> ShutdownProgress {
327 ShutdownProgress {
328 initiated: self.is_shutdown(),
329 in_flight: self.in_flight_count(),
330 elapsed: None, timeout: self.drain_timeout,
332 }
333 }
334
335 pub fn create_logger(self: &Arc<Self>) -> ShutdownLogger {
337 ShutdownLogger::with_default_interval(Arc::clone(self))
338 }
339
340 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 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 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 let guard = coord.request_start();
421
422 tokio::spawn(async move {
424 tokio::time::sleep(Duration::from_millis(50)).await;
425 drop(guard);
426 });
427
428 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 let _guard = coord.request_start();
440
441 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 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 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}