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 supervisor.notify_shutdown_subscribers();
185
186 let drained = drain_connections(supervisor, drain_timeout);
187 if !drained {
188 supervisor.force_close_active_connections();
189 wait_after_force_close(supervisor);
190 }
191
192 flush_durable_state(supervisor)?;
193 supervisor.shutdown();
194 tracing::info!("graceful shutdown sequence complete");
195 Ok(())
196}
197
198fn drain_connections(supervisor: &ConnectionSupervisor, drain_timeout: Duration) -> bool {
209 let active_at_start = supervisor.active_connection_count();
210 if active_at_start == 0 {
211 return true;
212 }
213 tracing::info!(
214 active_connections = active_at_start,
215 ?drain_timeout,
216 "waiting for active connections to drain"
217 );
218 let deadline = Instant::now() + drain_timeout;
219 let drained = supervisor.wait_for_connections_drained(deadline);
220 if drained {
221 tracing::info!("all connections drained before timeout");
222 } else {
223 tracing::warn!(
224 active_connections = supervisor.active_connection_count(),
225 ?drain_timeout,
226 "drain timeout expired with active connections"
227 );
228 }
229 drained
230}
231
232pub(crate) fn wait_after_force_close(supervisor: &ConnectionSupervisor) {
233 let deadline = Instant::now() + FORCE_CLOSE_SETTLE_WINDOW;
238 if supervisor.wait_for_connections_drained(deadline) {
239 return;
240 }
241 let remaining = supervisor.active_connection_count();
242 if remaining > 0 {
243 tracing::warn!(
244 active_connections = remaining,
245 "connections remained active after force-close settle window"
246 );
247 }
248}
249
250fn flush_durable_state(supervisor: &ConnectionSupervisor) -> Result<(), ServerError> {
251 tracing::info!("flushing durable channel state");
252 supervisor.flush_durable_state().map_err(|error| {
253 tracing::error!(%error, "durable state flush failed during shutdown");
254 match error {
255 ServerError::ShutdownFlush { .. } => error,
256 other => ServerError::ShutdownFlush {
257 message: other.to_string(),
258 },
259 }
260 })?;
261 tracing::info!("durable channel state flushed");
262 Ok(())
263}
264
265#[cfg(test)]
266mod tests {
267 use std::thread;
268 use std::time::Duration;
269
270 use super::{ShutdownHandle, drain_connections};
271 use crate::server::connection::ConnectionSupervisor;
272
273 #[test]
274 fn shutdown_handle_initiates_once() {
275 let handle = ShutdownHandle::new();
276
277 assert!(!handle.is_initiated());
278 assert!(handle.initiate());
279 assert!(handle.is_initiated());
280 assert!(!handle.initiate());
281 }
282
283 #[test]
284 fn shutdown_handle_wait_unblocks_on_initiate() -> Result<(), Box<dyn std::error::Error>> {
285 let handle = ShutdownHandle::new();
286 let waiter = handle.clone();
287 let worker = thread::spawn(move || {
288 waiter.wait();
289 waiter.is_initiated()
290 });
291
292 thread::sleep(Duration::from_millis(10));
293 assert!(handle.initiate());
294 let observed = worker.join().map_err(|_| "wait worker panicked")?;
295
296 assert!(observed);
297 Ok(())
298 }
299
300 #[test]
301 fn drain_returns_immediately_when_no_connections_are_active()
302 -> Result<(), Box<dyn std::error::Error>> {
303 let supervisor = ConnectionSupervisor::new()?;
304
305 let drained = drain_connections(&supervisor, Duration::from_secs(5));
306
307 assert!(drained);
308 supervisor.shutdown();
309 Ok(())
310 }
311
312 #[test]
318 fn drain_source_has_no_reap_count_sleep_loop() {
319 let source = include_str!("shutdown.rs");
320 let implementation = source.split("mod tests").next().unwrap_or(source);
323 for forbidden in [
324 "DRAIN_PROGRESS_INTERVAL",
325 "FORCE_CLOSE_SETTLE_TIMEOUT",
326 "FORCE_CLOSE_POLL_INTERVAL",
327 "reap_crashed_connections",
328 ] {
329 assert!(
330 !implementation.contains(forbidden),
331 "retired poll/reap token `{forbidden}` must not appear in the drain/settle implementation"
332 );
333 }
334 }
335}