confium_coordinator/
shutdown.rs1use std::sync::Arc;
21use std::sync::atomic::{AtomicBool, Ordering};
22use std::time::{Duration, Instant};
23
24#[derive(Debug, Default)]
29pub struct ShutdownSignal {
30 triggered: Arc<AtomicBool>,
31}
32
33impl Clone for ShutdownSignal {
34 fn clone(&self) -> Self {
35 Self {
36 triggered: Arc::clone(&self.triggered),
37 }
38 }
39}
40
41impl ShutdownSignal {
42 pub fn new() -> Self {
44 Self::default()
45 }
46
47 pub fn is_triggered(&self) -> bool {
49 self.triggered.load(Ordering::SeqCst)
50 }
51
52 pub fn trigger(&self) {
54 self.triggered.store(true, Ordering::SeqCst);
55 tracing::info!("shutdown signal triggered");
56 }
57
58 #[cfg(unix)]
62 #[allow(unsafe_code)] pub fn install(&self, _drain_timeout: Duration) {
64 let signal = self.clone();
65 unsafe {
66 libc_signal(libc_signum::SIGINT, move || {
67 signal.trigger();
68 });
69 let signal2 = self.clone();
70 libc_signal(libc_signum::SIGTERM, move || {
71 signal2.trigger();
72 });
73 }
74 }
75
76 #[cfg(not(unix))]
77 pub fn install(&self, _drain_timeout: Duration) {}
78
79 pub fn wait_for_trigger(&self, timeout: Duration) -> bool {
82 let deadline = Instant::now() + timeout;
83 while Instant::now() < deadline {
84 if self.is_triggered() {
85 return true;
86 }
87 std::thread::sleep(Duration::from_millis(100));
88 }
89 self.is_triggered()
90 }
91}
92
93#[derive(Debug, Clone, PartialEq, Eq)]
95pub enum DrainResult {
96 Drained { sessions_completed: usize },
98 TimedOut { force_expired: usize },
100}
101
102pub struct ShutdownCoordinator {
104 drain_timeout: Duration,
105}
106
107impl ShutdownCoordinator {
108 pub fn new(drain_timeout: Duration) -> Self {
110 Self { drain_timeout }
111 }
112
113 pub fn drain_timeout(&self) -> Duration {
115 self.drain_timeout
116 }
117
118 pub fn await_signal(&self, signal: &ShutdownSignal) -> bool {
121 signal.wait_for_trigger(self.drain_timeout)
122 }
123}
124
125#[cfg(unix)]
126mod libc_signum {
127 pub const SIGINT: i32 = 2;
128 pub const SIGTERM: i32 = 15;
129}
130
131#[cfg(unix)]
132#[allow(unsafe_code)] unsafe fn libc_signal(signum: i32, handler: impl Fn() + Send + 'static) {
134 use std::sync::OnceLock;
139 static FLAG: OnceLock<Arc<AtomicBool>> = OnceLock::new();
140
141 let _ = signum;
145 let _ = handler;
146}
147
148#[cfg(not(unix))]
149mod libc_signum {
150 pub const SIGINT: i32 = 0;
151 pub const SIGTERM: i32 = 0;
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157
158 #[test]
159 fn new_signal_starts_untriggered() {
160 let signal = ShutdownSignal::new();
161 assert!(!signal.is_triggered());
162 }
163
164 #[test]
165 fn trigger_sets_flag() {
166 let signal = ShutdownSignal::new();
167 signal.trigger();
168 assert!(signal.is_triggered());
169 }
170
171 #[test]
172 fn clone_shares_state() {
173 let signal = ShutdownSignal::new();
174 let clone = signal.clone();
175 signal.trigger();
176 assert!(clone.is_triggered());
177 }
178
179 #[test]
180 fn trigger_is_idempotent() {
181 let signal = ShutdownSignal::new();
182 signal.trigger();
183 signal.trigger();
184 signal.trigger();
185 assert!(signal.is_triggered());
186 }
187
188 #[test]
189 fn wait_for_trigger_returns_immediately_if_triggered() {
190 let signal = ShutdownSignal::new();
191 signal.trigger();
192 let result = signal.wait_for_trigger(Duration::from_secs(10));
193 assert!(result);
194 }
195
196 #[test]
197 fn wait_for_trigger_times_out() {
198 let signal = ShutdownSignal::new();
199 let result = signal.wait_for_trigger(Duration::from_millis(50));
200 assert!(!result);
201 }
202
203 #[test]
204 fn wait_for_trigger_returns_after_external_trigger() {
205 let signal = ShutdownSignal::new();
206 let clone = signal.clone();
207 std::thread::spawn(move || {
208 std::thread::sleep(Duration::from_millis(50));
209 clone.trigger();
210 });
211 let result = signal.wait_for_trigger(Duration::from_secs(2));
212 assert!(result);
213 }
214
215 #[test]
216 fn shutdown_coordinator_has_drain_timeout() {
217 let sc = ShutdownCoordinator::new(Duration::from_secs(30));
218 assert_eq!(sc.drain_timeout(), Duration::from_secs(30));
219 }
220
221 #[test]
222 fn drain_result_drained_variant() {
223 let r = DrainResult::Drained {
224 sessions_completed: 5,
225 };
226 assert_eq!(
227 r,
228 DrainResult::Drained {
229 sessions_completed: 5
230 }
231 );
232 }
233
234 #[test]
235 fn drain_result_timed_out_variant() {
236 let r = DrainResult::TimedOut { force_expired: 2 };
237 assert_eq!(r, DrainResult::TimedOut { force_expired: 2 });
238 }
239
240 #[test]
241 fn install_does_not_panic_on_unix() {
242 let signal = ShutdownSignal::new();
243 signal.install(Duration::from_secs(5));
244 }
245}