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#[derive(Clone, Debug, PartialEq, thiserror::Error)]
28pub enum SupervisorError {
29 #[error("too many dead tasks: {dead}/{total} ({:.0}%) >= threshold {:.0}%", *.dead as f64 / *.total as f64 * 100.0, .threshold * 100.0)]
33 TooManyDeadTasks {
34 dead: usize,
36 total: usize,
38 threshold: f64,
40 },
41 #[error("supervisor panicked: {0}")]
43 Panicked(String),
44 #[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
62struct 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 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 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.restart_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.kill_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 let waiters = std::mem::take(&mut slot.kill_waiters)
254 .into_iter()
255 .chain(std::mem::take(&mut slot.restart_waiters));
256 for waiter in waiters {
257 let _ = waiter.send(Ok(()));
258 }
259 return if died {
260 self.check_threshold()
261 } else {
262 ControlFlow::Continue(())
263 };
264 }
265
266 let Err(err) = result else {
267 info!(task = name, "completed");
268 slot.status = TaskStatus::Completed;
269 return ControlFlow::Continue(());
270 };
271
272 let now = Instant::now();
273 let window = self.config.restart_window;
274 while slot
275 .restarts
276 .front()
277 .is_some_and(|at| now.duration_since(*at) >= window)
278 {
279 slot.restarts.pop_front();
280 }
281 let recent = slot.restarts.len();
282 if self
283 .config
284 .max_restarts
285 .is_some_and(|max| recent >= max as usize)
286 {
287 error!(
288 task = name,
289 error = %err,
290 restarts = recent,
291 window = ?window,
292 "failed; restart limit reached, task is dead"
293 );
294 slot.status = TaskStatus::Dead;
295 return self.check_threshold();
296 }
297
298 let factor = 2u32.saturating_pow(recent.min(31) as u32);
299 let delay = self
300 .config
301 .base_restart_delay
302 .saturating_mul(factor)
303 .min(self.config.max_restart_delay);
304 let delay = jitter(delay, self.config.restart_jitter);
305 slot.restarts.push_back(now);
306 if self.config.max_restarts.is_none() && slot.restarts.len() > 32 {
309 slot.restarts.pop_front();
310 }
311 slot.status = TaskStatus::Restarting;
312 warn!(
313 task = name,
314 error = %err,
315 restarts_in_window = recent + 1,
316 delay = ?delay,
317 "failed; restart scheduled"
318 );
319 self.timers.push(Timer {
320 deadline: Instant::now().checked_add(delay).unwrap_or_else(far_future),
321 name: Arc::clone(&slot.name),
322 generation: slot.generation,
323 });
324 ControlFlow::Continue(())
325 }
326
327 fn on_timer(&mut self, timer: Timer) {
328 let Some(slot) = self.tasks.get_mut(&timer.name) else {
329 return;
330 };
331 if slot.status == TaskStatus::Restarting && slot.generation == timer.generation {
332 start_run(slot, &self.config, &mut self.runs);
333 }
334 }
335
336 fn check_threshold(&self) -> ControlFlow<Outcome> {
338 let Some(threshold) = self.config.dead_tasks_threshold else {
339 return ControlFlow::Continue(());
340 };
341 let total = self.tasks.len();
342 let dead = self
343 .tasks
344 .values()
345 .filter(|slot| slot.status == TaskStatus::Dead)
346 .count();
347 if (dead as f64 / total as f64) < threshold {
348 return ControlFlow::Continue(());
349 }
350 let err = SupervisorError::TooManyDeadTasks {
351 dead,
352 total,
353 threshold,
354 };
355 error!("{err}");
356 ControlFlow::Break(Err(err))
357 }
358
359 async fn stop_all(&mut self) {
362 self.timers.clear();
363 for slot in self.tasks.values_mut() {
364 match slot.status {
365 TaskStatus::Running | TaskStatus::Stopping => slot.request_stop(false),
366 TaskStatus::Restarting => slot.status = TaskStatus::Dead,
367 TaskStatus::Completed | TaskStatus::Dead => {}
368 }
369 }
370 while let Some(exit) = self.runs.join_next().await {
371 if let Ok((name, Err(err))) = exit {
372 warn!(task = %name, error = %err, "stopped with error");
373 }
374 }
375 for slot in self.tasks.values_mut() {
378 if slot.status == TaskStatus::Stopping {
379 slot.status = TaskStatus::Dead;
380 }
381 for waiter in slot.kill_waiters.drain(..) {
382 let _ = waiter.send(Ok(()));
383 }
384 slot.restart_waiters.clear();
385 }
386 info!("all tasks stopped");
387 }
388}
389
390fn start_run(slot: &mut Slot, config: &Config, runs: &mut JoinSet<RunOutput>) {
394 let cancel = CancellationToken::new();
395 let mut run = slot.task.clone_box().run(cancel.clone());
396 let grace = config.stop_timeout;
397 let name = Arc::clone(&slot.name);
398 let run_cancel = cancel.clone();
399
400 runs.spawn(async move {
401 let run = async {
402 tokio::select! {
403 result = &mut run => result,
404 _ = run_cancel.cancelled() => match timeout(grace, &mut run).await {
405 Ok(result) => result,
406 Err(_) => Err("did not stop within stop_timeout".into()),
407 },
408 }
409 };
410 tokio::pin!(run);
411 let result = poll_fn(
412 |cx| match catch_unwind(AssertUnwindSafe(|| run.as_mut().poll(cx))) {
413 Ok(poll) => poll,
414 Err(payload) => {
415 Poll::Ready(Err(format!("panicked: {}", panic_message(payload)).into()))
416 }
417 },
418 )
419 .await;
420 (name, result)
421 });
422
423 slot.status = TaskStatus::Running;
424 slot.cancel = Some(cancel);
425 slot.generation += 1;
426 slot.restart_after_stop = false;
427 debug!(task = %slot.name, generation = slot.generation, "started");
428}
429
430fn far_future() -> Instant {
432 Instant::now() + Duration::from_secs(30 * 365 * 24 * 3600)
433}
434
435fn jitter(delay: Duration, fraction: f64) -> Duration {
438 if fraction <= 0.0 || delay.is_zero() {
439 return delay;
440 }
441 let draw = RandomState::new().build_hasher().finish() as f64 / u64::MAX as f64;
444 delay.mul_f64(1.0 - fraction * draw)
445}