liminal_server/server/
shutdown.rs1use 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
14const 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 if self.is_initiated() {
68 return;
69 }
70 let Ok(mut guard) = self.inner.wait_lock.lock() else {
71 return;
72 };
73 while !self.is_initiated() {
74 match self.inner.waiter.wait(guard) {
75 Ok(next_guard) => guard = next_guard,
76 Err(_) => return,
77 }
78 }
79 }
80
81 #[must_use]
83 pub fn is_initiated(&self) -> bool {
84 self.inner.initiated.load(Ordering::SeqCst)
85 }
86}
87
88impl Default for ShutdownHandle {
89 fn default() -> Self {
90 Self::new()
91 }
92}
93
94impl fmt::Debug for ShutdownHandle {
95 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
96 formatter
97 .debug_struct("ShutdownHandle")
98 .field("initiated", &self.is_initiated())
99 .finish()
100 }
101}
102
103#[derive(Debug)]
104struct ShutdownState {
105 initiated: AtomicBool,
106 wait_lock: Mutex<()>,
107 waiter: Condvar,
108}
109
110impl ShutdownState {
111 const fn new() -> Self {
112 Self {
113 initiated: AtomicBool::new(false),
114 wait_lock: Mutex::new(()),
115 waiter: Condvar::new(),
116 }
117 }
118
119 fn notify(&self) {
120 if let Ok(_guard) = self.wait_lock.lock() {
121 self.waiter.notify_all();
122 }
123 }
124}
125
126#[derive(Debug)]
128pub struct SignalShutdownRegistration {
129 signal_handle: SignalIteratorHandle,
130 worker: Option<JoinHandle<()>>,
131}
132
133impl SignalShutdownRegistration {
134 const fn new(signal_handle: SignalIteratorHandle, worker: JoinHandle<()>) -> Self {
135 Self {
136 signal_handle,
137 worker: Some(worker),
138 }
139 }
140}
141
142impl Drop for SignalShutdownRegistration {
143 fn drop(&mut self) {
144 self.signal_handle.close();
145 let Some(worker) = self.worker.take() else {
146 return;
147 };
148 if worker.join().is_err() {
149 tracing::debug!("shutdown signal worker terminated unexpectedly");
150 }
151 }
152}
153
154pub fn register_signal_handlers(
159 handle: ShutdownHandle,
160) -> Result<SignalShutdownRegistration, ServerError> {
161 let mut signals =
162 Signals::new([SIGTERM, SIGINT]).map_err(|error| ServerError::ListenerAccept {
163 message: format!("failed to register shutdown signal handlers: {error}"),
164 })?;
165 let signal_handle = signals.handle();
166 let worker = thread::spawn(move || {
167 for signal in signals.forever() {
168 tracing::info!(signal, "received shutdown signal");
169 handle.initiate();
170 }
171 });
172 Ok(SignalShutdownRegistration::new(signal_handle, worker))
173}
174
175pub fn run_shutdown_sequence(
206 listener: &mut ServerListener,
207 websocket_listener: Option<&mut WebSocketListener>,
208 supervisor: &ConnectionSupervisor,
209 drain_timeout: Duration,
210) -> Result<(), ServerError> {
211 let started = Instant::now();
212 tracing::info!(
213 ignored_drain_timeout = ?drain_timeout,
214 "starting shutdown sequence; the configured drain timeout has been ignored since 0.14.3 \
215 because every write is durable before it is acknowledged, so no request is in flight to \
216 drain"
217 );
218 if let Some(websocket_listener) = websocket_listener {
221 websocket_listener.stop_accepting()?;
222 }
223 listener.stop_accepting()?;
224
225 if !supervisor.wait_for_delivery_quiesced(Instant::now() + WEDGED_CONNECTION_STOP) {
236 tracing::warn!(
237 stop = ?WEDGED_CONNECTION_STOP,
238 "delivery flush barrier did not quiesce before the wedged-connection stop; proceeding \
239 to shutdown notification"
240 );
241 }
242
243 supervisor.notify_shutdown_subscribers();
244
245 supervisor.force_close_active_connections();
248 wait_after_force_close(supervisor);
249
250 flush_durable_state(supervisor)?;
251 supervisor.shutdown();
252 tracing::info!(
253 elapsed_ms = u64::try_from(started.elapsed().as_millis()).unwrap_or(u64::MAX),
254 "shutdown sequence complete"
255 );
256 Ok(())
257}
258
259pub(crate) fn wait_after_force_close(supervisor: &ConnectionSupervisor) {
271 let deadline = Instant::now() + WEDGED_CONNECTION_STOP;
272 if supervisor.wait_for_connections_drained(deadline) {
273 return;
274 }
275 let remaining = supervisor.active_connection_count();
276 if remaining > 0 {
277 tracing::warn!(
278 active_connections = remaining,
279 stop = ?WEDGED_CONNECTION_STOP,
280 "connections remained active after the wedged-connection stop"
281 );
282 }
283}
284
285fn flush_durable_state(supervisor: &ConnectionSupervisor) -> Result<(), ServerError> {
286 tracing::info!("flushing durable channel state");
287 supervisor.flush_durable_state().map_err(|error| {
288 tracing::error!(%error, "durable state flush failed during shutdown");
289 match error {
290 ServerError::ShutdownFlush { .. } => error,
291 other => ServerError::ShutdownFlush {
292 message: other.to_string(),
293 },
294 }
295 })?;
296 tracing::info!("durable channel state flushed");
297 Ok(())
298}
299
300#[cfg(test)]
301mod tests {
302 use std::thread;
303 use std::time::Duration;
304
305 use super::{ShutdownHandle, wait_after_force_close};
306 use crate::server::connection::ConnectionSupervisor;
307
308 #[test]
309 fn shutdown_handle_initiates_once() {
310 let handle = ShutdownHandle::new();
311
312 assert!(!handle.is_initiated());
313 assert!(handle.initiate());
314 assert!(handle.is_initiated());
315 assert!(!handle.initiate());
316 }
317
318 #[test]
319 fn shutdown_handle_wait_unblocks_on_initiate() -> Result<(), Box<dyn std::error::Error>> {
320 let handle = ShutdownHandle::new();
321 let waiter = handle.clone();
322 let worker = thread::spawn(move || {
323 waiter.wait();
324 waiter.is_initiated()
325 });
326
327 thread::sleep(Duration::from_millis(10));
328 assert!(handle.initiate());
329 let observed = worker.join().map_err(|_| "wait worker panicked")?;
330
331 assert!(observed);
332 Ok(())
333 }
334
335 #[test]
338 fn the_close_settle_returns_immediately_when_no_connections_are_active()
339 -> Result<(), Box<dyn std::error::Error>> {
340 let supervisor = ConnectionSupervisor::new()?;
341
342 let started = std::time::Instant::now();
343 wait_after_force_close(&supervisor);
344 let elapsed = started.elapsed();
345
346 assert!(
347 elapsed < Duration::from_millis(50),
348 "the close settle took {elapsed:?} with no connections tracked"
349 );
350 supervisor.shutdown();
351 Ok(())
352 }
353
354 #[test]
361 fn shutdown_source_has_no_drain_and_no_reap_count_sleep_loop() {
362 let source = include_str!("shutdown.rs");
363 let implementation = source.split("mod tests").next().unwrap_or(source);
366 for forbidden in [
367 "DRAIN_PROGRESS_INTERVAL",
368 "FORCE_CLOSE_SETTLE_TIMEOUT",
369 "FORCE_CLOSE_POLL_INTERVAL",
370 "reap_crashed_connections",
371 "fn drain_connections",
372 ] {
373 assert!(
374 !implementation.contains(forbidden),
375 "retired poll/reap/drain token `{forbidden}` must not appear in the shutdown implementation"
376 );
377 }
378 }
379
380 #[test]
384 fn the_configured_drain_timeout_is_never_turned_into_a_deadline() {
385 let source = include_str!("shutdown.rs");
386 let implementation = source.split("mod tests").next().unwrap_or(source);
387 assert!(
388 !implementation.contains("+ drain_timeout"),
389 "drain_timeout must never be added to an Instant to form a shutdown deadline"
390 );
391 assert!(
392 implementation.contains("ignored_drain_timeout = ?drain_timeout"),
393 "drain_timeout must still be named in the shutdown log line that reports it ignored"
394 );
395 }
396}