scoped_join_set/
lib.rs

1use std::{
2    any::Any,
3    collections::HashMap,
4    error::Error,
5    fmt,
6    future::Future,
7    mem::transmute,
8    pin::Pin,
9    ptr::NonNull,
10    sync::atomic::{AtomicBool, Ordering},
11    task::{Context, Poll},
12};
13
14use tokio::{
15    runtime::{Handle, RuntimeFlavor},
16    task::{block_in_place, AbortHandle, Id, JoinSet},
17};
18
19type TokioJoinError = tokio::task::JoinError;
20
21/// A scoped wrapper around [`tokio::task::JoinSet`] for dynamically spawning
22/// and awaiting multiple asynchronous tasks.
23///
24/// `ScopedJoinSet` allows you to spawn futures that are bound to a specific lifetime `'scope`,
25/// ensuring all tasks outlive the scope in which they are spawned. Unlike a standard
26/// `JoinSet`, this structure internally tracks uniquely-owned pinned futures via `FutureHolder`,
27/// while exposing a weak reference (`WeakFuture`) for safe polling.
28///
29/// # Features
30/// - **Scoped Futures**: Ensures futures cannot outlive the provided `'scope`.
31/// - **Dynamic Spawning**: Tasks can be spawned dynamically at runtime.
32/// - **Cancellation Safe**: `join_next()` is cancellation safe; tasks remain in the set
33///   until they complete and are awaited.
34/// - **Automatic Abort**: Dropping a `FutureHolder` aborts its associated task.
35///
36/// # Safety
37/// - Each spawned task has exactly one `FutureHolder` (owning the `Pin<Box>` of the future)
38///   and one `WeakFuture` (polling handle).  
39/// - `FutureHolder` tracks whether the future is alive using an `AtomicBool`.  
40#[derive(Default)]
41pub struct ScopedJoinSet<'scope, T>
42where
43    T: 'static,
44{
45    join_set: JoinSet<Option<T>>,
46    holders: HashMap<Id, FutureHolder<'scope, T>>,
47}
48
49impl<'scope, T> ScopedJoinSet<'scope, T>
50where
51    T: 'static,
52{
53    pub fn new() -> Self {
54        Self {
55            join_set: JoinSet::new(),
56            holders: HashMap::new(),
57        }
58    }
59
60    pub fn spawn<F>(&mut self, task: F)
61    where
62        F: Future<Output = T> + Send + 'scope,
63        T: Send,
64    {
65        let alive: NonNull<AtomicBool> = Box::leak(Box::new(AtomicBool::new(true))).into();
66        let future: NonNull<F> = Box::leak(Box::new(task)).into();
67        let weak_future = unsafe {
68            type Src<'scope, T> = NonNull<dyn Future<Output = T> + 'scope>;
69            type Dst<T> = NonNull<dyn Future<Output = T>>;
70            let future = transmute::<Src<'scope, T>, Dst<T>>(future);
71            WeakFuture { future, alive }
72        };
73        let handle = self.join_set.spawn(weak_future);
74        let holder = FutureHolder {
75            abort_handle: handle.clone(),
76            future,
77            alive,
78        };
79        self.holders.insert(handle.id(), holder);
80    }
81
82    pub fn is_empty(&self) -> bool {
83        self.join_set.is_empty()
84    }
85
86    pub async fn join_next(&mut self) -> Option<Result<T, JoinError>> {
87        match self.join_set.join_next_with_id().await? {
88            Ok((id, Some(value))) => {
89                self.holders.remove(&id);
90                Some(Ok(value))
91            }
92            Ok((id, None)) => {
93                self.holders.remove(&id);
94                Some(Err(JoinError::Cancelled))
95            }
96            Err(error) => {
97                self.holders.remove(&error.id());
98                Some(Err(error.into()))
99            }
100        }
101    }
102}
103
104unsafe impl<'scope, T> Send for ScopedJoinSet<'scope, T> {}
105
106struct FutureHolder<'scope, T>
107where
108    T: 'static,
109{
110    abort_handle: AbortHandle,
111    future: NonNull<dyn Future<Output = T> + Send + 'scope>,
112    alive: NonNull<AtomicBool>,
113}
114
115impl<'scope, T> Drop for FutureHolder<'scope, T> {
116    fn drop(&mut self) {
117        self.abort_handle.abort();
118
119        if Handle::current().runtime_flavor() == RuntimeFlavor::MultiThread {
120            let alive = unsafe { self.alive.as_ref() };
121            // Wait until the future is not being polled
122            if alive.load(Ordering::Acquire) {
123                block_in_place(|| {
124                    while alive.load(Ordering::Acquire) {
125                        std::hint::spin_loop();
126                    }
127                });
128            }
129            drop(unsafe { Box::from_raw(self.future.as_ptr()) });
130            drop(unsafe { Box::from_raw(self.alive.as_mut()) });
131        } else {
132            drop(unsafe { Box::from_raw(self.future.as_ptr()) });
133        }
134    }
135}
136
137struct WeakFuture<T> {
138    future: NonNull<dyn Future<Output = T>>,
139    alive: NonNull<AtomicBool>,
140}
141
142impl<T> Drop for WeakFuture<T> {
143    fn drop(&mut self) {
144        // Release the polling flag
145        if Handle::current().runtime_flavor() == RuntimeFlavor::MultiThread {
146            unsafe { self.alive.as_ref() }.store(false, Ordering::Release);
147        } else {
148            drop(unsafe { Box::from_raw(self.alive.as_mut()) });
149        }
150    }
151}
152
153unsafe impl<T: Send> Send for WeakFuture<T> {}
154
155impl<T> Future for WeakFuture<T> {
156    type Output = Option<T>;
157
158    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
159        let future_ref = unsafe { self.future.as_mut() };
160        let future_pin = unsafe { Pin::new_unchecked(future_ref) };
161        Future::poll(future_pin, cx).map(Some)
162    }
163}
164
165#[derive(Debug)]
166pub enum JoinError {
167    Cancelled,
168    Panicked(Box<dyn Any + Send + 'static>),
169}
170
171impl fmt::Display for JoinError {
172    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
173        match self {
174            JoinError::Cancelled => write!(f, "task was cancelled"),
175            JoinError::Panicked(_) => write!(f, "task panicked"),
176        }
177    }
178}
179
180impl Error for JoinError {}
181
182impl JoinError {
183    pub fn is_cancelled(&self) -> bool {
184        matches!(self, JoinError::Cancelled)
185    }
186
187    pub fn is_panic(&self) -> bool {
188        matches!(self, JoinError::Panicked(_))
189    }
190}
191
192impl From<TokioJoinError> for JoinError {
193    fn from(err: TokioJoinError) -> Self {
194        if err.is_cancelled() {
195            JoinError::Cancelled
196        } else if err.is_panic() {
197            JoinError::Panicked(err.into_panic())
198        } else {
199            JoinError::Cancelled
200        }
201    }
202}