1use std::{
2 collections::{BinaryHeap, HashMap},
3 future::{poll_fn, Future},
4 ops::ControlFlow,
5 panic::{catch_unwind, AssertUnwindSafe},
6 sync::Arc,
7 task::Poll,
8 time::Duration,
9};
10
11use tokio::{
12 sync::mpsc,
13 task::JoinSet,
14 time::{sleep_until, timeout, Instant},
15};
16use tokio_util::sync::CancellationToken;
17use tracing::{debug, error, info, warn};
18
19use crate::{
20 handle::{Message, SupervisorHandle, SupervisorHandleError},
21 task::{panic_message, Slot, TaskResult, TaskStatus},
22};
23
24#[derive(Clone, Debug, PartialEq, thiserror::Error)]
27pub enum SupervisorError {
28 #[error("too many dead tasks: {dead}/{total} ({:.0}%) >= threshold {:.0}%", *.dead as f64 / *.total as f64 * 100.0, .threshold * 100.0)]
32 TooManyDeadTasks {
33 dead: usize,
35 total: usize,
37 threshold: f64,
39 },
40 #[error("supervisor panicked: {0}")]
42 Panicked(String),
43 #[error("supervisor aborted by runtime shutdown")]
45 Aborted,
46}
47
48pub(crate) type Outcome = Result<(), SupervisorError>;
49
50#[derive(Clone, Copy)]
51pub(crate) struct Config {
52 pub(crate) max_restarts: Option<u32>,
53 pub(crate) restart_window: Duration,
54 pub(crate) base_restart_delay: Duration,
55 pub(crate) max_restart_delay: Duration,
56 pub(crate) dead_tasks_threshold: Option<f64>,
57 pub(crate) stop_timeout: Duration,
58}
59
60struct Timer {
62 deadline: Instant,
63 name: Arc<str>,
64 generation: u64,
65}
66
67impl PartialEq for Timer {
68 fn eq(&self, other: &Self) -> bool {
69 self.deadline == other.deadline
70 }
71}
72
73impl Eq for Timer {}
74
75impl PartialOrd for Timer {
76 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
77 Some(self.cmp(other))
78 }
79}
80
81impl Ord for Timer {
82 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
83 other.deadline.cmp(&self.deadline)
84 }
85}
86
87type RunOutput = (Arc<str>, TaskResult);
88
89pub(crate) struct Supervisor {
90 tasks: HashMap<Arc<str>, Slot>,
91 config: Config,
92 runs: JoinSet<RunOutput>,
93 timers: BinaryHeap<Timer>,
94}
95
96impl Supervisor {
97 pub(crate) fn spawn(tasks: HashMap<Arc<str>, Slot>, config: Config) -> SupervisorHandle {
98 let supervisor = Self {
99 tasks,
100 config,
101 runs: JoinSet::new(),
102 timers: BinaryHeap::new(),
103 };
104 let (tx, rx) = mpsc::unbounded_channel();
105 let join = tokio::spawn(supervisor.supervise(rx));
106 SupervisorHandle::new(join, tx)
107 }
108
109 async fn supervise(mut self, mut rx: mpsc::UnboundedReceiver<Message>) -> Outcome {
110 for slot in self.tasks.values_mut() {
111 start_run(slot, &self.config, &mut self.runs);
112 }
113
114 let outcome = loop {
115 let deadline = self.timers.peek().map(|t| t.deadline);
116 let next_timer = async move {
117 match deadline {
118 Some(deadline) => sleep_until(deadline).await,
119 None => std::future::pending().await,
120 }
121 };
122
123 let flow = tokio::select! {
124 biased;
125 Some(exit) = self.runs.join_next(), if !self.runs.is_empty() => match exit {
126 Ok((name, result)) => self.on_run_exit(&name, result),
127 Err(_) => ControlFlow::Continue(()),
129 },
130 _ = next_timer => {
131 if let Some(timer) = self.timers.pop() {
132 self.on_timer(timer);
133 }
134 ControlFlow::Continue(())
135 }
136 message = rx.recv() => match message {
137 Some(message) => self.on_message(message),
138 None => {
139 info!("all handles dropped, shutting down");
140 ControlFlow::Break(Ok(()))
141 }
142 },
143 };
144
145 if let ControlFlow::Break(outcome) = flow {
146 break outcome;
147 }
148 };
149
150 drop(rx);
152 self.stop_all().await;
153 outcome
154 }
155
156 fn on_message(&mut self, message: Message) -> ControlFlow<Outcome> {
157 match message {
158 Message::AddTask { name, task, reply } => {
159 let result = if self.tasks.contains_key(&*name) {
160 Err(SupervisorHandleError::TaskAlreadyExists(name.to_string()))
161 } else {
162 let slot = self
163 .tasks
164 .entry(Arc::clone(&name))
165 .or_insert_with(|| Slot::new(name, task));
166 start_run(slot, &self.config, &mut self.runs);
167 Ok(())
168 };
169 let _ = reply.send(result);
170 }
171 Message::RestartTask { name, reply } => {
172 let Some(slot) = self.tasks.get_mut(name.as_str()) else {
173 let _ = reply.send(Err(SupervisorHandleError::TaskNotFound(name)));
174 return ControlFlow::Continue(());
175 };
176 info!(task = %name, "restart requested");
177 slot.restarts.clear();
178 match slot.status {
179 TaskStatus::Running | TaskStatus::Stopping => {
180 slot.request_stop(true);
181 slot.stop_waiters.push(reply);
182 }
183 TaskStatus::Restarting | TaskStatus::Completed | TaskStatus::Dead => {
184 start_run(slot, &self.config, &mut self.runs);
185 let _ = reply.send(Ok(()));
186 }
187 }
188 }
189 Message::KillTask { name, reply } => {
190 let Some(slot) = self.tasks.get_mut(name.as_str()) else {
191 let _ = reply.send(Err(SupervisorHandleError::TaskNotFound(name)));
192 return ControlFlow::Continue(());
193 };
194 info!(task = %name, "kill requested");
195 match slot.status {
196 TaskStatus::Running | TaskStatus::Stopping => {
197 slot.request_stop(false);
198 slot.stop_waiters.push(reply);
199 }
200 TaskStatus::Restarting | TaskStatus::Completed => {
201 slot.status = TaskStatus::Dead;
202 let _ = reply.send(Ok(()));
203 return self.check_threshold();
204 }
205 TaskStatus::Dead => {
206 let _ = reply.send(Ok(()));
207 }
208 }
209 }
210 Message::TaskStatus { name, reply } => {
211 let result = self
212 .tasks
213 .get(name.as_str())
214 .map(|slot| slot.status)
215 .ok_or(SupervisorHandleError::TaskNotFound(name));
216 let _ = reply.send(result);
217 }
218 Message::TaskStatuses { reply } => {
219 let statuses = self
220 .tasks
221 .iter()
222 .map(|(name, slot)| (Arc::clone(name), slot.status))
223 .collect();
224 let _ = reply.send(statuses);
225 }
226 Message::Shutdown => {
227 info!("shutdown requested");
228 return ControlFlow::Break(Ok(()));
229 }
230 }
231 ControlFlow::Continue(())
232 }
233
234 fn on_run_exit(&mut self, name: &str, result: TaskResult) -> ControlFlow<Outcome> {
235 let Some(slot) = self.tasks.get_mut(name) else {
236 return ControlFlow::Continue(());
237 };
238 slot.cancel = None;
239
240 if slot.status == TaskStatus::Stopping {
241 match &result {
242 Ok(()) => debug!(task = name, "stopped"),
243 Err(err) => warn!(task = name, error = %err, "stopped with error"),
244 }
245 let died = !slot.restart_after_stop;
246 if died {
247 slot.status = TaskStatus::Dead;
248 } else {
249 start_run(slot, &self.config, &mut self.runs);
250 }
251 for waiter in std::mem::take(&mut slot.stop_waiters) {
252 let _ = waiter.send(Ok(()));
253 }
254 return if died {
255 self.check_threshold()
256 } else {
257 ControlFlow::Continue(())
258 };
259 }
260
261 let Err(err) = result else {
262 info!(task = name, "completed");
263 slot.status = TaskStatus::Completed;
264 return ControlFlow::Continue(());
265 };
266
267 let now = Instant::now();
268 let window = self.config.restart_window;
269 while slot
270 .restarts
271 .front()
272 .is_some_and(|at| now.duration_since(*at) >= window)
273 {
274 slot.restarts.pop_front();
275 }
276 let recent = slot.restarts.len();
277 if self
278 .config
279 .max_restarts
280 .is_some_and(|max| recent >= max as usize)
281 {
282 error!(
283 task = name,
284 error = %err,
285 restarts = recent,
286 window = ?window,
287 "failed; restart limit reached, task is dead"
288 );
289 slot.status = TaskStatus::Dead;
290 return self.check_threshold();
291 }
292
293 let factor = 2u32.saturating_pow(recent.min(31) as u32);
295 let delay = self
296 .config
297 .base_restart_delay
298 .saturating_mul(factor)
299 .min(self.config.max_restart_delay);
300 slot.restarts.push_back(now);
301 if slot.restarts.len() > 32 {
303 slot.restarts.pop_front();
304 }
305 slot.status = TaskStatus::Restarting;
306 warn!(
307 task = name,
308 error = %err,
309 restarts_in_window = recent + 1,
310 delay = ?delay,
311 "failed; restart scheduled"
312 );
313 self.timers.push(Timer {
314 deadline: Instant::now().checked_add(delay).unwrap_or_else(far_future),
315 name: Arc::clone(&slot.name),
316 generation: slot.generation,
317 });
318 ControlFlow::Continue(())
319 }
320
321 fn on_timer(&mut self, timer: Timer) {
322 let Some(slot) = self.tasks.get_mut(&timer.name) else {
323 return;
324 };
325 if slot.status == TaskStatus::Restarting && slot.generation == timer.generation {
326 start_run(slot, &self.config, &mut self.runs);
327 }
328 }
329
330 fn check_threshold(&self) -> ControlFlow<Outcome> {
332 let Some(threshold) = self.config.dead_tasks_threshold else {
333 return ControlFlow::Continue(());
334 };
335 let total = self.tasks.len();
336 let dead = self
337 .tasks
338 .values()
339 .filter(|slot| slot.status == TaskStatus::Dead)
340 .count();
341 if (dead as f64 / total as f64) < threshold {
342 return ControlFlow::Continue(());
343 }
344 let err = SupervisorError::TooManyDeadTasks {
345 dead,
346 total,
347 threshold,
348 };
349 error!("{err}");
350 ControlFlow::Break(Err(err))
351 }
352
353 async fn stop_all(&mut self) {
356 self.timers.clear();
357 for slot in self.tasks.values_mut() {
358 match slot.status {
359 TaskStatus::Running | TaskStatus::Stopping => slot.request_stop(false),
360 TaskStatus::Restarting => slot.status = TaskStatus::Dead,
361 TaskStatus::Completed | TaskStatus::Dead => {}
362 }
363 }
364 while let Some(exit) = self.runs.join_next().await {
365 if let Ok((name, Err(err))) = exit {
366 warn!(task = %name, error = %err, "stopped with error");
367 }
368 }
369 info!("all tasks stopped");
370 }
371}
372
373fn start_run(slot: &mut Slot, config: &Config, runs: &mut JoinSet<RunOutput>) {
377 let cancel = CancellationToken::new();
378 let mut run = slot.task.clone_box().run(cancel.clone());
379 let grace = config.stop_timeout;
380 let name = Arc::clone(&slot.name);
381 let run_cancel = cancel.clone();
382
383 runs.spawn(async move {
384 let run = async {
385 tokio::select! {
386 result = &mut run => result,
387 _ = run_cancel.cancelled() => match timeout(grace, &mut run).await {
388 Ok(result) => result,
389 Err(_) => Err("did not stop within stop_timeout".into()),
390 },
391 }
392 };
393 tokio::pin!(run);
394 let result = poll_fn(
395 |cx| match catch_unwind(AssertUnwindSafe(|| run.as_mut().poll(cx))) {
396 Ok(poll) => poll,
397 Err(payload) => {
398 Poll::Ready(Err(format!("panicked: {}", panic_message(payload)).into()))
399 }
400 },
401 )
402 .await;
403 (name, result)
404 });
405
406 slot.status = TaskStatus::Running;
407 slot.cancel = Some(cancel);
408 slot.generation += 1;
409 slot.restart_after_stop = false;
410 debug!(task = %slot.name, generation = slot.generation, "started");
411}
412
413fn far_future() -> Instant {
415 Instant::now() + Duration::from_secs(30 * 365 * 24 * 3600)
416}