Skip to main content

task_supervisor/
supervisor.rs

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