Skip to main content

karyon_core/async_util/
task_group.rs

1use std::{
2    collections::HashMap,
3    future::Future,
4    sync::{
5        atomic::{AtomicUsize, Ordering},
6        Arc, Weak,
7    },
8};
9
10use parking_lot::Mutex;
11
12use crate::async_runtime::{global_executor, Executor, Task};
13
14use super::{select, CondWait, Either};
15
16/// Identifies a spawned task within a [`TaskGroup`].
17pub type TaskID = usize;
18
19/// TaskGroup A group that contains spawned tasks.
20///
21/// # Example
22///
23/// ```
24///
25/// use std::sync::Arc;
26///
27/// use karyon_core::async_util::{TaskGroup, sleep};
28///
29/// async {
30///     let group = TaskGroup::new();
31///
32///     group.spawn(sleep(std::time::Duration::MAX));
33///
34///     group.cancel().await;
35///
36/// };
37///
38/// ```
39pub struct TaskGroup {
40    inner: Arc<Inner>,
41}
42
43/// Shared state of a [`TaskGroup`]. Held behind an `Arc` so each task can
44/// keep a `Weak` reference back to the group and remove itself on
45/// completion, without forcing callers to wrap the group in an `Arc`.
46struct Inner {
47    tasks: Mutex<HashMap<TaskID, TaskHandler>>,
48    next_id: AtomicUsize,
49    executor: Executor,
50}
51
52impl Inner {
53    /// Removes a task by id, returning its handler if still present.
54    fn remove(&self, id: TaskID) -> Option<TaskHandler> {
55        self.tasks.lock().remove(&id)
56    }
57}
58
59impl TaskGroup {
60    /// Creates a new TaskGroup without providing an executor
61    ///
62    /// This will spawn a task onto a global executor (single-threaded by default).
63    pub fn new() -> Self {
64        Self::with_inner(global_executor())
65    }
66
67    /// Creates a new TaskGroup by providing an executor
68    pub fn with_executor(executor: Executor) -> Self {
69        Self::with_inner(executor)
70    }
71
72    fn with_inner(executor: Executor) -> Self {
73        Self {
74            inner: Arc::new(Inner {
75                tasks: Mutex::new(HashMap::new()),
76                next_id: AtomicUsize::new(0),
77                executor,
78            }),
79        }
80    }
81
82    /// Spawns a new task and ignores its result.
83    ///
84    /// Returns the task's [`TaskID`]. The task removes itself from the
85    /// group when it finishes, so the group does not grow without bound.
86    pub fn spawn<T, Fut>(&self, fut: Fut) -> TaskID
87    where
88        T: Send + Sync + 'static,
89        Fut: Future<Output = T> + Send + 'static,
90    {
91        self.spawn_then(fut, |_| async {})
92    }
93
94    /// Spawns a new task and calls the callback after it has completed
95    /// or been canceled. The callback will have the `TaskResult` as a
96    /// parameter, indicating whether the task completed or was canceled.
97    ///
98    /// Returns the task's [`TaskID`].
99    pub fn spawn_then<T, Fut, CallbackF, CallbackFut>(
100        &self,
101        fut: Fut,
102        callback: CallbackF,
103    ) -> TaskID
104    where
105        T: Send + Sync + 'static,
106        Fut: Future<Output = T> + Send + 'static,
107        CallbackF: FnOnce(TaskResult<T>) -> CallbackFut + Send + 'static,
108        CallbackFut: Future<Output = ()> + Send + 'static,
109    {
110        let id = self.inner.next_id.fetch_add(1, Ordering::Relaxed);
111        // Hold the lock across spawn and insert so the task cannot try to
112        // remove itself before it has been inserted.
113        let mut tasks = self.inner.tasks.lock();
114        let task = TaskHandler::new(
115            self.inner.executor.clone(),
116            fut,
117            callback,
118            Arc::downgrade(&self.inner),
119            id,
120        );
121        tasks.insert(id, task);
122        id
123    }
124
125    /// Removes a task by id, returning its handler if still present. The
126    /// caller takes ownership and may cancel it.
127    pub fn remove(&self, id: TaskID) -> Option<TaskHandler> {
128        self.inner.remove(id)
129    }
130
131    /// Checks if the TaskGroup is empty.
132    pub fn is_empty(&self) -> bool {
133        self.inner.tasks.lock().is_empty()
134    }
135
136    /// Get the number of the tasks in the group.
137    pub fn len(&self) -> usize {
138        self.inner.tasks.lock().len()
139    }
140
141    /// Cancels all tasks in the group.
142    pub async fn cancel(&self) {
143        // Take all handlers out, then cancel them without holding the lock.
144        let handlers: Vec<TaskHandler> = self.inner.tasks.lock().drain().map(|(_, h)| h).collect();
145        for handler in handlers {
146            handler.cancel().await;
147        }
148    }
149}
150
151impl Default for TaskGroup {
152    fn default() -> Self {
153        Self::new()
154    }
155}
156
157/// The result of a spawned task.
158#[derive(Debug)]
159pub enum TaskResult<T> {
160    Completed(T),
161    Cancelled,
162}
163
164impl<T: std::fmt::Debug> std::fmt::Display for TaskResult<T> {
165    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
166        match self {
167            TaskResult::Cancelled => write!(f, "Task cancelled"),
168            TaskResult::Completed(res) => write!(f, "Task completed: {res:?}"),
169        }
170    }
171}
172
173/// TaskHandler
174pub struct TaskHandler {
175    task: Task<()>,
176    /// Per-task stop signal. Signaling it makes the task stop and run its
177    /// callback with `Cancelled`.
178    stop_signal: Arc<CondWait>,
179    /// Set once the task has finished running its callback.
180    cancel_flag: Arc<CondWait>,
181}
182
183impl TaskHandler {
184    /// Creates a new task handler
185    fn new<T, Fut, CallbackF, CallbackFut>(
186        ex: Executor,
187        fut: Fut,
188        callback: CallbackF,
189        group: Weak<Inner>,
190        id: TaskID,
191    ) -> TaskHandler
192    where
193        T: Send + Sync + 'static,
194        Fut: Future<Output = T> + Send + 'static,
195        CallbackF: FnOnce(TaskResult<T>) -> CallbackFut + Send + 'static,
196        CallbackFut: Future<Output = ()> + Send + 'static,
197    {
198        let stop_signal = Arc::new(CondWait::new());
199        let stop_signal_c = stop_signal.clone();
200        let cancel_flag = Arc::new(CondWait::new());
201        let cancel_flag_c = cancel_flag.clone();
202        let task = ex.spawn(async move {
203            // Waits for either the stop signal or the task to complete.
204            let result = select(stop_signal_c.wait(), fut).await;
205
206            let result = match result {
207                Either::Left(_) => TaskResult::Cancelled,
208                Either::Right(res) => TaskResult::Completed(res),
209            };
210
211            // Call the callback
212            callback(result).await;
213
214            cancel_flag_c.signal().await;
215
216            // Remove ourselves from the group. Detach instead of dropping
217            // the handler, so we are not cancelled from within our own
218            // task. If `cancel` already took us out, this is a no-op.
219            if let Some(group) = group.upgrade() {
220                if let Some(handler) = group.remove(id) {
221                    handler.detach();
222                }
223            }
224        });
225
226        TaskHandler {
227            task,
228            stop_signal,
229            cancel_flag,
230        }
231    }
232
233    /// Detaches the task, so dropping the handler does not cancel it.
234    fn detach(self) {
235        self.task.detach();
236    }
237
238    /// Cancels the task: tells it to stop, waits for its callback to run,
239    /// then aborts whatever is left.
240    async fn cancel(self) {
241        self.stop_signal.signal().await;
242        self.cancel_flag.wait().await;
243        self.task.cancel().await;
244    }
245}
246
247#[cfg(test)]
248mod tests {
249    use std::{future, sync::Arc};
250
251    use crate::async_runtime::block_on;
252    use crate::async_util::sleep;
253
254    use super::*;
255
256    #[cfg(feature = "tokio")]
257    #[test]
258    fn test_task_group_with_tokio_executor() {
259        let ex = Arc::new(tokio::runtime::Runtime::new().unwrap());
260        ex.clone().block_on(async move {
261            let group = Arc::new(TaskGroup::with_executor(ex.into()));
262
263            group.spawn_then(future::ready(0), |res| async move {
264                assert!(matches!(res, TaskResult::Completed(0)));
265            });
266
267            group.spawn_then(future::pending::<()>(), |res| async move {
268                assert!(matches!(res, TaskResult::Cancelled));
269            });
270
271            let groupc = group.clone();
272            group.spawn_then(
273                async move {
274                    groupc.spawn_then(future::pending::<()>(), |res| async move {
275                        assert!(matches!(res, TaskResult::Cancelled));
276                    });
277                },
278                |res| async move {
279                    assert!(matches!(res, TaskResult::Completed(_)));
280                },
281            );
282
283            // Do something
284            tokio::time::sleep(std::time::Duration::from_millis(50)).await;
285            group.cancel().await;
286        });
287    }
288
289    #[cfg(feature = "smol")]
290    #[test]
291    fn test_task_group_with_smol_executor() {
292        let ex = Arc::new(smol::Executor::new());
293        smol::block_on(ex.clone().run(async move {
294            let group = Arc::new(TaskGroup::with_executor(ex.into()));
295
296            group.spawn_then(future::ready(0), |res| async move {
297                assert!(matches!(res, TaskResult::Completed(0)));
298            });
299
300            group.spawn_then(future::pending::<()>(), |res| async move {
301                assert!(matches!(res, TaskResult::Cancelled));
302            });
303
304            let groupc = group.clone();
305            group.spawn_then(
306                async move {
307                    groupc.spawn_then(future::pending::<()>(), |res| async move {
308                        assert!(matches!(res, TaskResult::Cancelled));
309                    });
310                },
311                |res| async move {
312                    assert!(matches!(res, TaskResult::Completed(_)));
313                },
314            );
315
316            // Do something
317            smol::Timer::after(std::time::Duration::from_millis(50)).await;
318            group.cancel().await;
319        }));
320    }
321
322    #[test]
323    fn test_task_group() {
324        block_on(async {
325            let group = Arc::new(TaskGroup::new());
326
327            group.spawn_then(future::ready(0), |res| async move {
328                assert!(matches!(res, TaskResult::Completed(0)));
329            });
330
331            group.spawn_then(future::pending::<()>(), |res| async move {
332                assert!(matches!(res, TaskResult::Cancelled));
333            });
334
335            let groupc = group.clone();
336            group.spawn_then(
337                async move {
338                    groupc.spawn_then(future::pending::<()>(), |res| async move {
339                        assert!(matches!(res, TaskResult::Cancelled));
340                    });
341                },
342                |res| async move {
343                    assert!(matches!(res, TaskResult::Completed(_)));
344                },
345            );
346
347            // Do something
348            sleep(std::time::Duration::from_millis(50)).await;
349            group.cancel().await;
350        });
351    }
352
353    #[test]
354    fn test_task_group_removes_finished_tasks() {
355        block_on(async {
356            let group = Arc::new(TaskGroup::new());
357
358            // A finished task removes itself; a pending one stays.
359            group.spawn(future::ready(0));
360            group.spawn(future::pending::<()>());
361
362            sleep(std::time::Duration::from_millis(50)).await;
363            assert_eq!(group.len(), 1);
364
365            group.cancel().await;
366            assert!(group.is_empty());
367        });
368    }
369
370    #[test]
371    fn test_task_group_remove_by_id() {
372        block_on(async {
373            let group = Arc::new(TaskGroup::new());
374
375            let id = group.spawn(future::pending::<()>());
376            assert_eq!(group.len(), 1);
377
378            let handler = group.remove(id).expect("task is present");
379            assert!(group.is_empty());
380            handler.cancel().await;
381        });
382    }
383}