1use crate::config::{backoff_delay, SupervisedOutcome, SupervisorConfig};
6use crate::health::HealthFlag;
7use crate::time::now_unix_ms;
8use std::panic::{catch_unwind, AssertUnwindSafe};
9use std::sync::atomic::{AtomicBool, Ordering};
10use std::sync::Arc;
11use std::thread::JoinHandle;
12
13pub struct SupervisedThread {
17 handle: Option<JoinHandle<()>>,
18 health: HealthFlag,
19 shutdown: Arc<AtomicBool>,
20}
21
22impl SupervisedThread {
23 pub fn spawn<F>(config: SupervisorConfig, worker: F) -> Self
29 where
30 F: FnMut(&Arc<AtomicBool>) -> SupervisedOutcome + Send + 'static,
31 {
32 let health = HealthFlag::new(config.tcb_critical);
33 let shutdown = Arc::new(AtomicBool::new(false));
34 let worker_health = health.clone();
35 let worker_shutdown = Arc::clone(&shutdown);
36 let handle = match std::thread::Builder::new()
37 .name(config.name.to_string())
38 .spawn(move || supervise_loop(config, worker, worker_health, worker_shutdown))
39 {
40 Ok(handle) => Some(handle),
41 Err(error) => {
42 let now = now_unix_ms();
43 health.record_failure(
44 format!("supervisor could not spawn thread: {error}"),
45 now,
46 0,
47 );
48 health.escalate_failed(now);
49 None
50 }
51 };
52 Self {
53 handle,
54 health,
55 shutdown,
56 }
57 }
58
59 #[must_use]
61 pub fn health(&self) -> HealthFlag {
62 self.health.clone()
63 }
64
65 pub fn shutdown(self) -> crate::HealthLevel {
67 self.signal_shutdown();
68 self.join()
69 }
70
71 pub fn join(mut self) -> crate::HealthLevel {
75 if let Some(handle) = self.handle.take() {
76 let _ = handle.join();
77 }
78 self.health.level()
79 }
80
81 fn signal_shutdown(&self) {
82 self.shutdown.store(true, Ordering::SeqCst);
83 }
84}
85
86impl Drop for SupervisedThread {
87 fn drop(&mut self) {
88 self.signal_shutdown();
91 }
92}
93
94fn supervise_loop<F>(
95 config: SupervisorConfig,
96 mut worker: F,
97 health: HealthFlag,
98 shutdown: Arc<AtomicBool>,
99) where
100 F: FnMut(&Arc<AtomicBool>) -> SupervisedOutcome,
101{
102 loop {
103 if shutdown.load(Ordering::SeqCst) {
104 return;
105 }
106 let outcome = catch_unwind(AssertUnwindSafe(|| worker(&shutdown)));
107 match outcome {
108 Ok(SupervisedOutcome::Shutdown) => return,
109 Ok(SupervisedOutcome::Continue) => {
110 health.record_ok(now_unix_ms());
116 continue;
117 }
118 Ok(SupervisedOutcome::Restart) | Err(_) => {
119 if shutdown.load(Ordering::SeqCst) {
120 return;
121 }
122 let now = now_unix_ms();
123 let reason = match &outcome {
124 Err(_) => format!("{} worker panicked", config.name),
125 _ => format!("{} worker exited", config.name),
126 };
127 let count = health.record_failure(reason, now, config.trip_after);
128 if count >= config.max_restarts {
129 health.escalate_failed(now);
130 return;
131 }
132 std::thread::sleep(backoff_delay(&config, count));
133 }
134 }
135 }
136}
137
138#[cfg(all(test, not(loom)))]
139mod tests {
140 use super::*;
141 use crate::HealthLevel;
142 use std::sync::atomic::AtomicU32;
143 use std::time::Duration;
144
145 fn fast_config(name: &'static str, tcb: bool, max_restarts: u32) -> SupervisorConfig {
146 SupervisorConfig {
147 name,
148 tcb_critical: tcb,
149 trip_after: 2,
150 max_restarts,
151 base_backoff: Duration::from_millis(1),
152 max_backoff: Duration::from_millis(2),
153 }
154 }
155
156 #[test]
157 fn clean_shutdown_leaves_flag_healthy() {
158 let thread = SupervisedThread::spawn(fast_config("clean", false, 5), |shutdown| {
159 while !shutdown.load(Ordering::SeqCst) {
160 std::thread::sleep(Duration::from_millis(1));
161 }
162 SupervisedOutcome::Shutdown
163 });
164 let level = thread.shutdown();
165 assert_eq!(level, HealthLevel::Healthy);
166 }
167
168 #[test]
169 fn panicking_worker_restarts_then_escalates_to_failed() {
170 let attempts = Arc::new(AtomicU32::new(0));
171 let worker_attempts = Arc::clone(&attempts);
172 let thread = SupervisedThread::spawn(fast_config("panic", true, 3), move |_shutdown| {
173 worker_attempts.fetch_add(1, Ordering::SeqCst);
174 panic!("worker blew up");
175 });
176 let health = thread.health();
177 let deadline = std::time::Instant::now() + Duration::from_secs(5);
179 while health.level() != HealthLevel::Failed && std::time::Instant::now() < deadline {
180 std::thread::sleep(Duration::from_millis(2));
181 }
182 assert_eq!(health.level(), HealthLevel::Failed);
183 assert!(health.is_serving_closed());
184 assert_eq!(attempts.load(Ordering::SeqCst), 3);
186 thread.shutdown();
187 }
188
189 #[test]
190 fn restart_outcome_trips_to_degraded_at_trip_after() {
191 let thread = SupervisedThread::spawn(fast_config("restart", false, 100), |_shutdown| {
192 SupervisedOutcome::Restart
193 });
194 let health = thread.health();
195 let deadline = std::time::Instant::now() + Duration::from_secs(5);
196 while health.level() == HealthLevel::Healthy && std::time::Instant::now() < deadline {
197 std::thread::sleep(Duration::from_millis(2));
198 }
199 assert_eq!(health.level(), HealthLevel::Degraded);
200 thread.shutdown();
201 }
202
203 #[test]
204 fn interleaved_restart_and_continue_never_trips() {
205 let phase = Arc::new(AtomicU32::new(0));
211 let worker_phase = Arc::clone(&phase);
212 let done = Arc::new(AtomicBool::new(false));
213 let worker_done = Arc::clone(&done);
214 let thread = SupervisedThread::spawn(fast_config("interleave", false, 1000), move |_s| {
215 let n = worker_phase.fetch_add(1, Ordering::SeqCst);
216 match n {
217 0 | 2 | 4 => SupervisedOutcome::Restart,
218 1 | 3 => SupervisedOutcome::Continue,
219 _ => {
220 worker_done.store(true, Ordering::SeqCst);
221 std::thread::sleep(Duration::from_millis(1));
222 SupervisedOutcome::Continue
223 }
224 }
225 });
226 let health = thread.health();
227 let deadline = std::time::Instant::now() + Duration::from_secs(5);
228 while !done.load(Ordering::SeqCst) && std::time::Instant::now() < deadline {
229 std::thread::sleep(Duration::from_millis(1));
230 }
231 assert_eq!(health.level(), HealthLevel::Healthy);
232 thread.shutdown();
233 }
234
235 #[test]
236 fn continue_outcome_never_trips() {
237 let ticks = Arc::new(AtomicU32::new(0));
238 let worker_ticks = Arc::clone(&ticks);
239 let thread = SupervisedThread::spawn(fast_config("continue", false, 5), move |shutdown| {
240 if shutdown.load(Ordering::SeqCst) {
241 return SupervisedOutcome::Shutdown;
242 }
243 worker_ticks.fetch_add(1, Ordering::SeqCst);
244 std::thread::sleep(Duration::from_millis(1));
245 SupervisedOutcome::Continue
246 });
247 std::thread::sleep(Duration::from_millis(20));
248 assert_eq!(thread.health().level(), HealthLevel::Healthy);
249 thread.shutdown();
250 assert!(ticks.load(Ordering::SeqCst) >= 1);
251 }
252
253 #[test]
254 fn join_does_not_cancel_before_the_worker_enters() {
255 let (release_tx, release_rx) = std::sync::mpsc::sync_channel(1);
256 let (entered_tx, entered_rx) = std::sync::mpsc::sync_channel(1);
257 let thread = SupervisedThread::spawn(fast_config("join-entry", true, 5), move |shutdown| {
258 release_rx.recv().unwrap_or(());
259 assert!(!shutdown.load(Ordering::SeqCst));
260 entered_tx.send(()).unwrap_or(());
261 SupervisedOutcome::Shutdown
262 });
263
264 let joiner = std::thread::spawn(move || thread.join());
265 release_tx.send(()).unwrap_or(());
266 let level = joiner.join().unwrap_or(HealthLevel::Failed);
267 assert_eq!(entered_rx.try_recv(), Ok(()));
268 assert_eq!(level, HealthLevel::Healthy);
269 }
270
271 #[test]
272 fn join_waits_through_restart_backoff() {
273 let attempts = Arc::new(AtomicU32::new(0));
274 let worker_attempts = Arc::clone(&attempts);
275 let (restarting_tx, restarting_rx) = std::sync::mpsc::sync_channel(1);
276 let (restarted_tx, restarted_rx) = std::sync::mpsc::sync_channel(1);
277 let thread = SupervisedThread::spawn(
278 SupervisorConfig {
279 base_backoff: Duration::from_millis(25),
280 max_backoff: Duration::from_millis(25),
281 ..fast_config("join-backoff", true, 5)
282 },
283 move |shutdown| {
284 let attempt = worker_attempts.fetch_add(1, Ordering::SeqCst);
285 if attempt == 0 {
286 restarting_tx.send(()).unwrap_or(());
287 SupervisedOutcome::Restart
288 } else {
289 assert!(!shutdown.load(Ordering::SeqCst));
290 restarted_tx.send(()).unwrap_or(());
291 SupervisedOutcome::Shutdown
292 }
293 },
294 );
295
296 restarting_rx.recv().unwrap_or(());
297 let level = thread.join();
298 assert_eq!(restarted_rx.try_recv(), Ok(()));
299 assert_eq!(level, HealthLevel::Healthy);
300 assert_eq!(attempts.load(Ordering::SeqCst), 2);
301 }
302}