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 FORCE_CLOSE_SETTLE_WINDOW: Duration = Duration::from_millis(500);
19
20#[derive(Clone)]
22pub struct ShutdownHandle {
23 inner: Arc<ShutdownState>,
24}
25
26impl ShutdownHandle {
27 #[must_use]
29 pub fn new() -> Self {
30 Self {
31 inner: Arc::new(ShutdownState::new()),
32 }
33 }
34
35 pub fn initiate(&self) -> bool {
40 if self.inner.initiated.swap(true, Ordering::SeqCst) {
41 tracing::debug!("shutdown request ignored because shutdown is already active");
42 return false;
43 }
44
45 tracing::info!("shutdown requested");
46 self.inner.notify();
47 true
48 }
49
50 pub fn wait(&self) {
52 if self.is_initiated() {
53 return;
54 }
55 let Ok(mut guard) = self.inner.wait_lock.lock() else {
56 return;
57 };
58 while !self.is_initiated() {
59 match self.inner.waiter.wait(guard) {
60 Ok(next_guard) => guard = next_guard,
61 Err(_) => return,
62 }
63 }
64 }
65
66 #[must_use]
68 pub fn is_initiated(&self) -> bool {
69 self.inner.initiated.load(Ordering::SeqCst)
70 }
71}
72
73impl Default for ShutdownHandle {
74 fn default() -> Self {
75 Self::new()
76 }
77}
78
79impl fmt::Debug for ShutdownHandle {
80 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
81 formatter
82 .debug_struct("ShutdownHandle")
83 .field("initiated", &self.is_initiated())
84 .finish()
85 }
86}
87
88#[derive(Debug)]
89struct ShutdownState {
90 initiated: AtomicBool,
91 wait_lock: Mutex<()>,
92 waiter: Condvar,
93}
94
95impl ShutdownState {
96 const fn new() -> Self {
97 Self {
98 initiated: AtomicBool::new(false),
99 wait_lock: Mutex::new(()),
100 waiter: Condvar::new(),
101 }
102 }
103
104 fn notify(&self) {
105 if let Ok(_guard) = self.wait_lock.lock() {
106 self.waiter.notify_all();
107 }
108 }
109}
110
111#[derive(Debug)]
113pub struct SignalShutdownRegistration {
114 signal_handle: SignalIteratorHandle,
115 worker: Option<JoinHandle<()>>,
116}
117
118impl SignalShutdownRegistration {
119 const fn new(signal_handle: SignalIteratorHandle, worker: JoinHandle<()>) -> Self {
120 Self {
121 signal_handle,
122 worker: Some(worker),
123 }
124 }
125}
126
127impl Drop for SignalShutdownRegistration {
128 fn drop(&mut self) {
129 self.signal_handle.close();
130 let Some(worker) = self.worker.take() else {
131 return;
132 };
133 if worker.join().is_err() {
134 tracing::debug!("shutdown signal worker terminated unexpectedly");
135 }
136 }
137}
138
139pub fn register_signal_handlers(
144 handle: ShutdownHandle,
145) -> Result<SignalShutdownRegistration, ServerError> {
146 let mut signals =
147 Signals::new([SIGTERM, SIGINT]).map_err(|error| ServerError::ListenerAccept {
148 message: format!("failed to register shutdown signal handlers: {error}"),
149 })?;
150 let signal_handle = signals.handle();
151 let worker = thread::spawn(move || {
152 for signal in signals.forever() {
153 tracing::info!(signal, "received shutdown signal");
154 handle.initiate();
155 }
156 });
157 Ok(SignalShutdownRegistration::new(signal_handle, worker))
158}
159
160pub fn run_shutdown_sequence(
172 listener: &mut ServerListener,
173 websocket_listener: Option<&mut WebSocketListener>,
174 supervisor: &ConnectionSupervisor,
175 drain_timeout: Duration,
176) -> Result<(), ServerError> {
177 tracing::info!(?drain_timeout, "starting graceful shutdown sequence");
178 if let Some(websocket_listener) = websocket_listener {
181 websocket_listener.stop_accepting()?;
182 }
183 listener.stop_accepting()?;
184
185 let flush_deadline = Instant::now() + drain_timeout;
196 if !supervisor.wait_for_delivery_quiesced(flush_deadline) {
197 tracing::warn!(
198 ?drain_timeout,
199 "delivery flush barrier did not quiesce before its budget; proceeding to shutdown notification"
200 );
201 }
202
203 supervisor.notify_shutdown_subscribers();
204
205 let drained = drain_connections(supervisor, drain_timeout);
206 if !drained {
207 supervisor.force_close_active_connections();
208 wait_after_force_close(supervisor);
209 }
210
211 flush_durable_state(supervisor)?;
212 supervisor.shutdown();
213 tracing::info!("graceful shutdown sequence complete");
214 Ok(())
215}
216
217fn drain_connections(supervisor: &ConnectionSupervisor, drain_timeout: Duration) -> bool {
228 let active_at_start = supervisor.active_connection_count();
229 if active_at_start == 0 {
230 return true;
231 }
232 tracing::info!(
233 active_connections = active_at_start,
234 ?drain_timeout,
235 "waiting for active connections to drain"
236 );
237 let deadline = Instant::now() + drain_timeout;
238 let drained = supervisor.wait_for_connections_drained(deadline);
239 if drained {
240 tracing::info!("all connections drained before timeout");
241 } else {
242 tracing::warn!(
243 active_connections = supervisor.active_connection_count(),
244 ?drain_timeout,
245 "drain timeout expired with active connections"
246 );
247 }
248 drained
249}
250
251pub(crate) fn wait_after_force_close(supervisor: &ConnectionSupervisor) {
252 let deadline = Instant::now() + FORCE_CLOSE_SETTLE_WINDOW;
257 if supervisor.wait_for_connections_drained(deadline) {
258 return;
259 }
260 let remaining = supervisor.active_connection_count();
261 if remaining > 0 {
262 tracing::warn!(
263 active_connections = remaining,
264 "connections remained active after force-close settle window"
265 );
266 }
267}
268
269fn flush_durable_state(supervisor: &ConnectionSupervisor) -> Result<(), ServerError> {
270 tracing::info!("flushing durable channel state");
271 supervisor.flush_durable_state().map_err(|error| {
272 tracing::error!(%error, "durable state flush failed during shutdown");
273 match error {
274 ServerError::ShutdownFlush { .. } => error,
275 other => ServerError::ShutdownFlush {
276 message: other.to_string(),
277 },
278 }
279 })?;
280 tracing::info!("durable channel state flushed");
281 Ok(())
282}
283
284#[cfg(test)]
285mod tests {
286 use std::thread;
287 use std::time::Duration;
288
289 use super::{ShutdownHandle, drain_connections};
290 use crate::server::connection::ConnectionSupervisor;
291
292 #[test]
293 fn shutdown_handle_initiates_once() {
294 let handle = ShutdownHandle::new();
295
296 assert!(!handle.is_initiated());
297 assert!(handle.initiate());
298 assert!(handle.is_initiated());
299 assert!(!handle.initiate());
300 }
301
302 #[test]
303 fn shutdown_handle_wait_unblocks_on_initiate() -> Result<(), Box<dyn std::error::Error>> {
304 let handle = ShutdownHandle::new();
305 let waiter = handle.clone();
306 let worker = thread::spawn(move || {
307 waiter.wait();
308 waiter.is_initiated()
309 });
310
311 thread::sleep(Duration::from_millis(10));
312 assert!(handle.initiate());
313 let observed = worker.join().map_err(|_| "wait worker panicked")?;
314
315 assert!(observed);
316 Ok(())
317 }
318
319 #[test]
320 fn drain_returns_immediately_when_no_connections_are_active()
321 -> Result<(), Box<dyn std::error::Error>> {
322 let supervisor = ConnectionSupervisor::new()?;
323
324 let drained = drain_connections(&supervisor, Duration::from_secs(5));
325
326 assert!(drained);
327 supervisor.shutdown();
328 Ok(())
329 }
330
331 #[test]
337 fn drain_source_has_no_reap_count_sleep_loop() {
338 let source = include_str!("shutdown.rs");
339 let implementation = source.split("mod tests").next().unwrap_or(source);
342 for forbidden in [
343 "DRAIN_PROGRESS_INTERVAL",
344 "FORCE_CLOSE_SETTLE_TIMEOUT",
345 "FORCE_CLOSE_POLL_INTERVAL",
346 "reap_crashed_connections",
347 ] {
348 assert!(
349 !implementation.contains(forbidden),
350 "retired poll/reap token `{forbidden}` must not appear in the drain/settle implementation"
351 );
352 }
353 }
354}