Skip to main content

task_supervisor/
task.rs

1use std::{collections::VecDeque, future::Future, pin::Pin, sync::Arc};
2
3use tokio::sync::oneshot;
4use tokio_util::sync::CancellationToken;
5
6use crate::handle::SupervisorHandleError;
7
8/// Error type returned by [`SupervisedTask::run`].
9///
10/// Any `std::error::Error + Send + Sync` converts into it with `?`, including
11/// `anyhow::Error`.
12pub type TaskError = Box<dyn std::error::Error + Send + Sync + 'static>;
13
14/// Outcome of one run of a [`SupervisedTask`].
15pub type TaskResult = Result<(), TaskError>;
16
17/// A long-lived unit of work managed by the supervisor.
18///
19/// Every start or restart clones the registered instance and passes the clone
20/// to `run`. Owned fields reset on every run; `Arc` fields are shared.
21///
22/// | `run` outcome | Supervisor reaction |
23/// | --- | --- |
24/// | `Ok(())` | [`Completed`](TaskStatus::Completed); not restarted |
25/// | `Err(_)` or panic | Restarted with backoff, [`Dead`](TaskStatus::Dead) once the restart limit is reached |
26///
27/// `cancel` fires on kill, restart and shutdown. The run then has
28/// `stop_timeout` to return before its future is dropped. Ignore it, wrap the
29/// work in `cancel.run_until_cancelled(..)`, or `select!` on
30/// `cancel.cancelled()` when there is cleanup to do.
31///
32/// # Example
33///
34/// ```rust
35/// use std::sync::Arc;
36/// use std::sync::atomic::{AtomicUsize, Ordering};
37/// use std::time::Duration;
38/// use task_supervisor::{CancellationToken, SupervisedTask, TaskResult};
39///
40/// #[derive(Clone)]
41/// struct Worker {
42///     /// Reset on every run (owned, cloned from the original).
43///     polls: u64,
44///     /// Shared across runs (`Arc`, cloned by reference).
45///     total_polls: Arc<AtomicUsize>,
46///     db: Arc<Db>,
47/// }
48///
49/// impl SupervisedTask for Worker {
50///     async fn run(mut self, cancel: CancellationToken) -> TaskResult {
51///         loop {
52///             tokio::select! {
53///                 _ = cancel.cancelled() => break,
54///                 _ = tokio::time::sleep(Duration::from_secs(1)) => {
55///                     self.polls += 1;
56///                     self.total_polls.fetch_add(1, Ordering::Relaxed);
57///                 }
58///             }
59///         }
60///         self.db.flush().await?; // cleanup after cancellation
61///         Ok(())
62///     }
63/// }
64/// # struct Db;
65/// # impl Db { async fn flush(&self) -> Result<(), std::io::Error> { Ok(()) } }
66/// ```
67pub trait SupervisedTask: Clone + Send + 'static {
68    /// Runs one instance of the task until it completes, fails, or is cancelled.
69    fn run(self, cancel: CancellationToken) -> impl Future<Output = TaskResult> + Send;
70}
71
72/// Object-safe view of [`SupervisedTask`].
73pub(crate) trait DynTask: Send + 'static {
74    fn run(
75        self: Box<Self>,
76        cancel: CancellationToken,
77    ) -> Pin<Box<dyn Future<Output = TaskResult> + Send>>;
78    fn clone_box(&self) -> Box<dyn DynTask>;
79}
80
81impl<T: SupervisedTask> DynTask for T {
82    fn run(
83        self: Box<Self>,
84        cancel: CancellationToken,
85    ) -> Pin<Box<dyn Future<Output = TaskResult> + Send>> {
86        Box::pin(SupervisedTask::run(*self, cancel))
87    }
88
89    fn clone_box(&self) -> Box<dyn DynTask> {
90        Box::new(self.clone())
91    }
92}
93
94/// Observable state of a supervised task.
95#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
96pub enum TaskStatus {
97    /// `run` is executing.
98    Running,
99    /// `run` failed; a restart is scheduled after the backoff delay.
100    Restarting,
101    /// Cancellation was requested; waiting for `run` to return
102    /// (bounded by `stop_timeout`).
103    Stopping,
104    /// `run` returned `Ok(())`. Terminal unless restarted manually.
105    Completed,
106    /// Killed, or restart limit reached. Terminal unless restarted manually.
107    Dead,
108}
109
110impl std::fmt::Display for TaskStatus {
111    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112        f.write_str(match self {
113            Self::Running => "running",
114            Self::Restarting => "restarting",
115            Self::Stopping => "stopping",
116            Self::Completed => "completed",
117            Self::Dead => "dead",
118        })
119    }
120}
121
122pub(crate) type Reply<T> = oneshot::Sender<Result<T, SupervisorHandleError>>;
123
124/// Supervisor-side bookkeeping for one task. At most one run is alive per slot.
125pub(crate) struct Slot {
126    pub(crate) name: Arc<str>,
127    pub(crate) status: TaskStatus,
128    pub(crate) task: Box<dyn DynTask>,
129    pub(crate) cancel: Option<CancellationToken>,
130    /// Incremented on every start; stale backoff timers carry an older value.
131    pub(crate) generation: u64,
132    /// Restart instants inside the current window, oldest first.
133    pub(crate) restarts: VecDeque<tokio::time::Instant>,
134    /// What to do once a `Stopping` run exits.
135    pub(crate) restart_after_stop: bool,
136    /// `kill_task` callers, answered once the run has exited.
137    pub(crate) kill_waiters: Vec<Reply<()>>,
138    /// `restart_task` callers, answered once the new run has started.
139    pub(crate) restart_waiters: Vec<Reply<()>>,
140}
141
142impl Slot {
143    pub(crate) fn new(name: Arc<str>, task: Box<dyn DynTask>) -> Self {
144        Self {
145            name,
146            status: TaskStatus::Dead,
147            task,
148            cancel: None,
149            generation: 0,
150            restarts: VecDeque::new(),
151            restart_after_stop: false,
152            kill_waiters: Vec::new(),
153            restart_waiters: Vec::new(),
154        }
155    }
156
157    pub(crate) fn request_stop(&mut self, restart_after: bool) {
158        if let Some(cancel) = &self.cancel {
159            cancel.cancel();
160        }
161        self.status = TaskStatus::Stopping;
162        self.restart_after_stop = restart_after;
163    }
164}
165
166pub(crate) fn panic_message(payload: Box<dyn std::any::Any + Send>) -> String {
167    payload
168        .downcast_ref::<&str>()
169        .map(|s| (*s).to_owned())
170        .or_else(|| payload.downcast_ref::<String>().cloned())
171        .unwrap_or_else(|| "non-string panic payload".to_owned())
172}