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