cja/
tasks.rs

1use std::future::Future;
2
3use color_eyre::eyre::eyre;
4
5/// A named wrapper around a tokio `JoinHandle`, used to track which
6/// long-running task completed (or failed) first.
7pub struct NamedTask {
8    name: &'static str,
9    handle: tokio::task::JoinHandle<crate::Result<()>>,
10}
11
12impl NamedTask {
13    /// Spawn a named async task on the tokio runtime.
14    pub fn spawn<F>(name: &'static str, future: F) -> Self
15    where
16        F: Future<Output = crate::Result<()>> + Send + 'static,
17    {
18        Self {
19            name,
20            handle: tokio::spawn(future),
21        }
22    }
23
24    /// Returns the name of this task.
25    pub fn name(&self) -> &'static str {
26        self.name
27    }
28}
29
30/// Wait for the first task in the set to complete and return its name alongside
31/// the join result.
32///
33/// For long-running services (server, job worker, cron) any task exiting is
34/// typically an error — use this to detect which one stopped.
35pub async fn wait_for_first_task(
36    tasks: Vec<NamedTask>,
37) -> (
38    &'static str,
39    Result<crate::Result<()>, tokio::task::JoinError>,
40) {
41    let (handles, names): (Vec<_>, Vec<_>) = tasks.into_iter().map(|t| (t.handle, t.name)).unzip();
42
43    let (result, index, _remaining) = futures::future::select_all(handles).await;
44    (names[index], result)
45}
46
47/// Wait for the first task to complete and convert the outcome into a
48/// single `Result`.
49///
50/// All three exit conditions (clean exit, error, panic) are treated as
51/// errors because long-running tasks are not expected to return.
52pub async fn wait_for_first_error(tasks: Vec<NamedTask>) -> crate::Result<()> {
53    if tasks.is_empty() {
54        return Ok(());
55    }
56
57    let (name, result) = wait_for_first_task(tasks).await;
58
59    match result {
60        Ok(Ok(())) => {
61            tracing::error!(task = name, "Task exited unexpectedly");
62            Err(eyre!("Task '{}' exited unexpectedly", name))
63        }
64        Ok(Err(e)) => {
65            tracing::error!(task = name, error = ?e, "Task failed with error");
66            Err(e)
67        }
68        Err(join_error) => {
69            tracing::error!(task = name, error = ?join_error, "Task panicked");
70            Err(eyre!("Task '{}' panicked: {}", name, join_error))
71        }
72    }
73}