Skip to main content

task_supervisor/
supervisor.rs

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/// Why the supervisor exited abnormally. Returned by
25/// [`SupervisorHandle::wait`] and [`SupervisorHandle::shutdown`].
26#[derive(Clone, Debug, PartialEq, thiserror::Error)]
27pub enum SupervisorError {
28    /// The dead-task threshold configured with
29    /// [`with_dead_tasks_threshold`](crate::SupervisorBuilder::with_dead_tasks_threshold)
30    /// was reached; every remaining task was stopped.
31    #[error("too many dead tasks: {dead}/{total} ({:.0}%) >= threshold {:.0}%", *.dead as f64 / *.total as f64 * 100.0, .threshold * 100.0)]
32    TooManyDeadTasks {
33        /// Tasks in [`TaskStatus::Dead`] when the threshold was reached.
34        dead: usize,
35        /// Tasks registered at that moment.
36        total: usize,
37        /// Configured threshold, as a fraction.
38        threshold: f64,
39    },
40    /// The supervisor task itself panicked (a bug in this crate).
41    #[error("supervisor panicked: {0}")]
42    Panicked(String),
43    /// The Tokio runtime shut down before the supervisor exited.
44    #[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
60/// Scheduled restart, ordered earliest-deadline-first for `BinaryHeap`.
61struct 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                    // Only happens when the runtime aborts tasks while shutting down.
128                    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        // Requests sent from now on fail with `Closed` instead of waiting for the drain.
151        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        // ponytail: no jitter; add if many tasks share a failing dependency.
294        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        // Unlimited mode: the exponent saturates anyway, keep the deque bounded.
302        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    /// Only called after a transition to `Dead`, the sole way the ratio can rise.
331    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    /// Cancels every run and waits for all of them to exit. Each run bounds
354    /// itself with `stop_timeout`, so this returns within that delay.
355    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
373/// Spawns one run of `slot`'s task. The run yields `(name, result)`; a panic
374/// inside `run` is caught and reported as `Err`, and after cancellation the
375/// run is dropped once `stop_timeout` elapses.
376fn 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
413/// Stand-in deadline for delays that overflow the clock: effectively "never".
414fn far_future() -> Instant {
415    Instant::now() + Duration::from_secs(30 * 365 * 24 * 3600)
416}