1use std::fmt;
2use std::sync::atomic::{AtomicBool, Ordering};
3use std::sync::{Arc, Condvar, Mutex};
4use std::thread::{self, JoinHandle};
5use std::time::{Duration, Instant};
6
7use signal_hook::consts::signal::{SIGINT, SIGTERM};
8use signal_hook::iterator::{Handle as SignalIteratorHandle, Signals};
9
10use crate::ServerError;
11use crate::server::connection::{ConnectionSupervisor, WebSocketListener};
12use crate::server::listener::ServerListener;
13
14pub(crate) const WEDGED_CONNECTION_STOP: Duration = Duration::from_millis(500);
34
35#[derive(Clone)]
37pub struct ShutdownHandle {
38 inner: Arc<ShutdownState>,
39}
40
41impl ShutdownHandle {
42 #[must_use]
44 pub fn new() -> Self {
45 Self {
46 inner: Arc::new(ShutdownState::new()),
47 }
48 }
49
50 pub fn initiate(&self) -> bool {
55 if self.inner.initiated.swap(true, Ordering::SeqCst) {
56 tracing::debug!("shutdown request ignored because shutdown is already active");
57 return false;
58 }
59
60 tracing::info!("shutdown requested");
61 self.inner.notify();
62 true
63 }
64
65 pub fn wait(&self) {
67 #[cfg(test)]
70 let _returned = Returned(&self.inner);
71 if self.is_initiated() {
72 return;
73 }
74 let Ok(mut guard) = self.inner.wait_lock.lock() else {
75 tracing::error!(
78 "shutdown wait lock poisoned; returning from wait as if shutdown was requested"
79 );
80 return;
81 };
82 while !self.is_initiated() {
83 #[cfg(test)]
86 self.inner.signal_parked();
87 let Ok(next_guard) = self.inner.waiter.wait(guard) else {
88 tracing::error!(
89 "shutdown wait condvar reacquired a poisoned lock; returning from wait as if shutdown was requested"
90 );
91 return;
92 };
93 guard = next_guard;
94 }
95 }
96
97 #[must_use]
99 pub fn is_initiated(&self) -> bool {
100 self.inner.initiated.load(Ordering::SeqCst)
101 }
102}
103
104impl Default for ShutdownHandle {
105 fn default() -> Self {
106 Self::new()
107 }
108}
109
110impl fmt::Debug for ShutdownHandle {
111 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
112 formatter
113 .debug_struct("ShutdownHandle")
114 .field("initiated", &self.is_initiated())
115 .finish()
116 }
117}
118
119#[derive(Debug)]
120struct ShutdownState {
121 initiated: AtomicBool,
122 wait_lock: Mutex<()>,
123 waiter: Condvar,
124 #[cfg(test)]
126 parked: std::sync::atomic::AtomicUsize,
127 #[cfg(test)]
129 returned: std::sync::atomic::AtomicUsize,
130 #[cfg(test)]
132 notified: std::sync::atomic::AtomicUsize,
133 #[cfg(test)]
136 signal: Condvar,
137 #[cfg(test)]
145 signal_lock: Mutex<()>,
146}
147
148#[cfg(test)]
152struct Returned<'a>(&'a ShutdownState);
153
154#[cfg(test)]
155impl Drop for Returned<'_> {
156 fn drop(&mut self) {
157 self.0.count_and_signal(&self.0.returned);
158 }
159}
160
161impl ShutdownState {
162 const fn new() -> Self {
163 Self {
164 initiated: AtomicBool::new(false),
165 wait_lock: Mutex::new(()),
166 waiter: Condvar::new(),
167 #[cfg(test)]
168 parked: std::sync::atomic::AtomicUsize::new(0),
169 #[cfg(test)]
170 returned: std::sync::atomic::AtomicUsize::new(0),
171 #[cfg(test)]
172 notified: std::sync::atomic::AtomicUsize::new(0),
173 #[cfg(test)]
174 signal: Condvar::new(),
175 #[cfg(test)]
176 signal_lock: Mutex::new(()),
177 }
178 }
179
180 #[cfg(test)]
183 fn signal_parked(&self) {
184 self.count_and_signal(&self.parked);
185 }
186
187 #[cfg(test)]
197 fn count_and_signal(&self, count: &std::sync::atomic::AtomicUsize) {
198 let guard = self.signal_lock.lock();
199 count.fetch_add(1, Ordering::SeqCst);
200 self.signal.notify_all();
201 drop(guard);
202 }
203
204 fn notify(&self) {
205 match self.wait_lock.lock() {
206 Ok(_guard) => self.waiter.notify_all(),
207 Err(poisoned) => {
208 tracing::error!("shutdown wait lock poisoned; notifying waiters anyway");
209 let _guard = poisoned.into_inner();
216 self.waiter.notify_all();
217 }
218 }
219 #[cfg(test)]
221 self.notified.fetch_add(1, Ordering::SeqCst);
222 }
223}
224
225#[derive(Debug)]
227pub struct SignalShutdownRegistration {
228 signal_handle: SignalIteratorHandle,
229 worker: Option<JoinHandle<()>>,
230}
231
232impl SignalShutdownRegistration {
233 const fn new(signal_handle: SignalIteratorHandle, worker: JoinHandle<()>) -> Self {
234 Self {
235 signal_handle,
236 worker: Some(worker),
237 }
238 }
239}
240
241impl Drop for SignalShutdownRegistration {
242 fn drop(&mut self) {
243 self.signal_handle.close();
244 let Some(worker) = self.worker.take() else {
245 return;
246 };
247 if worker.join().is_err() {
248 tracing::warn!("shutdown signal worker terminated unexpectedly");
249 }
250 }
251}
252
253pub fn register_signal_handlers(
258 handle: ShutdownHandle,
259) -> Result<SignalShutdownRegistration, ServerError> {
260 let mut signals =
261 Signals::new([SIGTERM, SIGINT]).map_err(|error| ServerError::ListenerAccept {
262 message: format!("failed to register shutdown signal handlers: {error}"),
263 })?;
264 let signal_handle = signals.handle();
265 let worker = thread::spawn(move || {
266 for signal in signals.forever() {
267 tracing::info!(signal, "received shutdown signal");
268 handle.initiate();
269 }
270 });
271 Ok(SignalShutdownRegistration::new(signal_handle, worker))
272}
273
274pub fn run_shutdown_sequence(
308 listener: &mut ServerListener,
309 websocket_listener: Option<&mut WebSocketListener>,
310 supervisor: &ConnectionSupervisor,
311 drain_timeout: Duration,
312) -> Result<(), ServerError> {
313 run_shutdown_sequence_before_flush(
314 listener,
315 websocket_listener,
316 supervisor,
317 drain_timeout,
318 || Ok(()),
319 )
320}
321
322pub(in crate::server) fn run_shutdown_sequence_before_flush(
323 listener: &mut ServerListener,
324 websocket_listener: Option<&mut WebSocketListener>,
325 supervisor: &ConnectionSupervisor,
326 drain_timeout: Duration,
327 finalize: impl FnOnce() -> Result<(), ServerError>,
328) -> Result<(), ServerError> {
329 let started = Instant::now();
330 tracing::info!(
331 ignored_drain_timeout = ?drain_timeout,
332 "starting shutdown sequence; the configured drain timeout has been ignored since 0.14.3 \
333 because every write is durable before it is acknowledged, so no request is in flight to \
334 drain"
335 );
336 if let Some(websocket_listener) = websocket_listener {
339 websocket_listener.stop_accepting()?;
340 }
341 listener.stop_accepting()?;
342
343 if !supervisor.wait_for_delivery_quiesced(Instant::now() + WEDGED_CONNECTION_STOP) {
354 tracing::warn!(
355 stop = ?WEDGED_CONNECTION_STOP,
356 "delivery flush barrier completion was not observed before the wedged-connection stop; \
357 proceeding to shutdown notification"
358 );
359 }
360
361 supervisor.notify_shutdown_subscribers();
362
363 supervisor.force_close_active_connections();
366 wait_after_force_close(supervisor);
367
368 let settled = supervisor.settle_parked_connection_fates();
374
375 supervisor.shutdown();
376 let finalized = finalize();
377 let flushed = flush_durable_state(supervisor);
378 tracing::info!(
379 elapsed_ms = u64::try_from(started.elapsed().as_millis()).unwrap_or(u64::MAX),
380 clean = settled.is_ok() && finalized.is_ok() && flushed.is_ok(),
381 "shutdown sequence complete"
382 );
383 let mut failure = None;
384 for result in [finalized, flushed, settled] {
385 if let Err(error) = result {
386 tracing::error!(%error, "shutdown phase failed");
387 if failure.is_none() {
388 failure = Some(error);
389 }
390 }
391 }
392 failure.map_or(Ok(()), Err)
393}
394
395pub(crate) fn wait_after_force_close(supervisor: &ConnectionSupervisor) {
407 let deadline = Instant::now() + WEDGED_CONNECTION_STOP;
408 if supervisor.wait_for_connections_drained(deadline) {
409 return;
410 }
411 let remaining = supervisor.active_connection_count();
412 if remaining > 0 {
413 tracing::warn!(
414 active_connections = remaining,
415 stop = ?WEDGED_CONNECTION_STOP,
416 "connections remained active after the wedged-connection stop"
417 );
418 }
419}
420
421fn flush_durable_state(supervisor: &ConnectionSupervisor) -> Result<(), ServerError> {
422 tracing::info!("flushing durable channel state");
423 supervisor.flush_durable_state().map_err(|error| {
424 tracing::error!(%error, "durable state flush failed during shutdown");
425 match error {
426 ServerError::ShutdownFlush { .. } => error,
427 other => ServerError::ShutdownFlush {
428 message: other.to_string(),
429 },
430 }
431 })?;
432 tracing::info!("durable channel state flushed");
433 Ok(())
434}
435
436#[cfg(test)]
437#[path = "shutdown_inflight_tests.rs"]
438mod shutdown_inflight_tests;
439
440#[cfg(test)]
441mod tests {
442 use std::error::Error;
443 use std::sync::atomic::Ordering;
444 use std::thread;
445 use std::time::Duration;
446
447 use super::{ShutdownHandle, wait_after_force_close};
448 use crate::server::connection::ConnectionSupervisor;
449
450 #[test]
451 fn shutdown_handle_initiates_once() {
452 let handle = ShutdownHandle::new();
453
454 assert!(!handle.is_initiated());
455 assert!(handle.initiate());
456 assert!(handle.is_initiated());
457 assert!(!handle.initiate());
458 }
459
460 const SIGNAL_LOCK_POISONED: &str =
462 "the test signal lock was poisoned: another test thread panicked holding it";
463
464 fn await_parked(handle: &ShutdownHandle) -> Result<(), Box<dyn Error>> {
470 let state = &handle.inner;
471 let mut guard = state.signal_lock.lock().map_err(|_| SIGNAL_LOCK_POISONED)?;
472 loop {
473 if state.returned.load(Ordering::SeqCst) > 0 {
474 return Err("the waiter returned from wait() without parking".into());
475 }
476 if state.parked.load(Ordering::SeqCst) > 0 {
477 drop(guard);
478 return Ok(());
479 }
480 guard = state.signal.wait(guard).map_err(|_| SIGNAL_LOCK_POISONED)?;
481 }
482 }
483
484 fn await_returned(handle: &ShutdownHandle, parked_before: usize) -> Result<(), Box<dyn Error>> {
493 let state = &handle.inner;
494 if state.notified.load(Ordering::SeqCst) == 0 {
495 return Err("initiate() never notified the parked waiter".into());
496 }
497 let mut guard = state.signal_lock.lock().map_err(|_| SIGNAL_LOCK_POISONED)?;
498 while state.returned.load(Ordering::SeqCst) == 0 {
499 if state.parked.load(Ordering::SeqCst) > parked_before {
500 return Err("the waiter woke and parked again: it did not see the shutdown".into());
501 }
502 guard = state.signal.wait(guard).map_err(|_| SIGNAL_LOCK_POISONED)?;
503 }
504 drop(guard);
505 Ok(())
506 }
507
508 #[test]
509 fn shutdown_handle_wait_unblocks_on_initiate() -> Result<(), Box<dyn std::error::Error>> {
510 let handle = ShutdownHandle::new();
511 let waiter = handle.clone();
512 let worker = thread::spawn(move || {
513 waiter.wait();
514 waiter.is_initiated()
515 });
516
517 await_parked(&handle)?;
518 let parked = handle.inner.parked.load(Ordering::SeqCst);
519 assert!(handle.initiate());
520 await_returned(&handle, parked)?;
521 let observed = worker.join().map_err(|_| "wait worker panicked")?;
522
523 assert!(observed);
524 Ok(())
525 }
526
527 fn wait_lock_of(state: &super::ShutdownState) -> &std::sync::Mutex<()> {
529 &state.wait_lock
530 }
531
532 fn signal_lock_of(state: &super::ShutdownState) -> &std::sync::Mutex<()> {
534 &state.signal_lock
535 }
536
537 #[allow(clippy::panic)]
540 fn poison(handle: &ShutdownHandle, lock: fn(&super::ShutdownState) -> &std::sync::Mutex<()>) {
541 let poisoner = handle.clone();
542 let outcome = thread::spawn(move || {
543 let _guard = lock(&poisoner.inner).lock();
544 panic!("poison a shutdown lock");
545 })
546 .join();
547 assert!(outcome.is_err(), "the poisoner thread must have panicked");
548 }
549
550 fn poison_wait_lock(handle: &ShutdownHandle) {
552 poison(handle, wait_lock_of);
553 }
554
555 #[test]
558 fn a_poisoned_test_signal_lock_is_refused_by_name() {
559 let handle = ShutdownHandle::new();
560 poison(&handle, signal_lock_of);
561 assert!(handle.initiate());
562
563 let refused =
564 await_returned(&handle, 0).map_or_else(|error| error.to_string(), |()| String::new());
565 assert_eq!(refused, SIGNAL_LOCK_POISONED);
566 let refused =
567 await_parked(&handle).map_or_else(|error| error.to_string(), |()| String::new());
568 assert_eq!(refused, SIGNAL_LOCK_POISONED);
569 }
570
571 #[test]
575 fn wait_on_poisoned_lock_returns_and_logs_error() {
576 let handle = ShutdownHandle::new();
577 poison_wait_lock(&handle);
578
579 let log = crate::test_log::CapturedLog::default();
580 log.capture(|| handle.wait());
581
582 let output = log.contents();
583 assert!(
584 output.contains("ERROR"),
585 "a poisoned wait lock must be logged at error level, got: {output}"
586 );
587 }
588
589 #[test]
593 fn notify_on_poisoned_lock_still_wakes_parked_waiter() -> Result<(), Box<dyn std::error::Error>>
594 {
595 let handle = ShutdownHandle::new();
596 let waiter = handle.clone();
597 let worker = thread::spawn(move || waiter.wait());
598 await_parked(&handle)?;
601 poison_wait_lock(&handle);
602
603 let parked = handle.inner.parked.load(Ordering::SeqCst);
604 assert!(handle.initiate());
605
606 await_returned(&handle, parked)?;
608 worker.join().map_err(|_| "wait worker panicked")?;
609 Ok(())
610 }
611
612 #[test]
615 fn the_close_settle_returns_immediately_when_no_connections_are_active()
616 -> Result<(), Box<dyn std::error::Error>> {
617 let supervisor = ConnectionSupervisor::new()?;
618
619 let started = std::time::Instant::now();
620 wait_after_force_close(&supervisor);
621 let elapsed = started.elapsed();
622
623 assert!(
624 elapsed < Duration::from_millis(50),
625 "the close settle took {elapsed:?} with no connections tracked"
626 );
627 supervisor.shutdown();
628 Ok(())
629 }
630
631 #[test]
638 fn shutdown_source_has_no_drain_and_no_reap_count_sleep_loop() {
639 let source = include_str!("shutdown.rs");
640 let implementation = source.split("mod tests").next().unwrap_or(source);
643 for forbidden in [
644 "DRAIN_PROGRESS_INTERVAL",
645 "FORCE_CLOSE_SETTLE_TIMEOUT",
646 "FORCE_CLOSE_POLL_INTERVAL",
647 "reap_crashed_connections",
648 "fn drain_connections",
649 ] {
650 assert!(
651 !implementation.contains(forbidden),
652 "retired poll/reap/drain token `{forbidden}` must not appear in the shutdown implementation"
653 );
654 }
655 }
656
657 #[test]
661 fn the_configured_drain_timeout_is_never_turned_into_a_deadline() {
662 let source = include_str!("shutdown.rs");
663 let implementation = source.split("mod tests").next().unwrap_or(source);
664 assert!(
665 !implementation.contains("+ drain_timeout"),
666 "drain_timeout must never be added to an Instant to form a shutdown deadline"
667 );
668 assert!(
669 implementation.contains("ignored_drain_timeout = ?drain_timeout"),
670 "drain_timeout must still be named in the shutdown log line that reports it ignored"
671 );
672 }
673}