Skip to main content

tokio_util/task/
join_queue.rs

1use super::AbortOnDropHandle;
2use std::{
3    collections::VecDeque,
4    future::Future,
5    pin::Pin,
6    task::{Context, Poll},
7};
8use tokio::{
9    runtime::Handle,
10    task::{AbortHandle, Id, JoinError, JoinHandle},
11};
12
13/// A FIFO queue for of tasks spawned on a Tokio runtime.
14///
15/// A [`JoinQueue`] can be used to await the completion of the tasks in FIFO
16/// order. That is, if tasks are spawned in the order A, B, C, then
17/// awaiting the next completed task will always return A first, then B,
18/// then C, regardless of the order in which the tasks actually complete.
19///
20/// All of the tasks must have the same return type `T`.
21///
22/// When the [`JoinQueue`] is dropped, all tasks in the [`JoinQueue`] are
23/// immediately aborted.
24pub struct JoinQueue<T>(VecDeque<AbortOnDropHandle<T>>);
25
26impl<T> JoinQueue<T> {
27    /// Create a new empty [`JoinQueue`].
28    pub const fn new() -> Self {
29        Self(VecDeque::new())
30    }
31
32    /// Creates an empty [`JoinQueue`] with space for at least `capacity` tasks.
33    pub fn with_capacity(capacity: usize) -> Self {
34        Self(VecDeque::with_capacity(capacity))
35    }
36
37    /// Returns the number of tasks currently in the [`JoinQueue`].
38    ///
39    /// This includes both tasks that are currently running and tasks that have
40    /// completed but not yet been removed from the queue because outputting of
41    /// them waits for FIFO order.
42    pub fn len(&self) -> usize {
43        self.0.len()
44    }
45
46    /// Returns whether the [`JoinQueue`] is empty.
47    pub fn is_empty(&self) -> bool {
48        self.0.is_empty()
49    }
50
51    /// Spawn the provided task on the [`JoinQueue`], returning an [`AbortHandle`]
52    /// that can be used to remotely cancel the task.
53    ///
54    /// The provided future will start running in the background immediately
55    /// when this method is called, even if you don't await anything on this
56    /// [`JoinQueue`].
57    ///
58    /// # Panics
59    ///
60    /// This method panics if called outside of a Tokio runtime.
61    ///
62    /// [`AbortHandle`]: tokio::task::AbortHandle
63    #[track_caller]
64    pub fn spawn<F>(&mut self, task: F) -> AbortHandle
65    where
66        F: Future<Output = T> + Send + 'static,
67        T: Send + 'static,
68    {
69        self.push_back(tokio::spawn(task))
70    }
71
72    /// Spawn the provided task on the provided runtime and store it in this
73    /// [`JoinQueue`] returning an [`AbortHandle`] that can be used to remotely
74    /// cancel the task.
75    ///
76    /// The provided future will start running in the background immediately
77    /// when this method is called, even if you don't await anything on this
78    /// [`JoinQueue`].
79    ///
80    /// [`AbortHandle`]: tokio::task::AbortHandle
81    #[track_caller]
82    pub fn spawn_on<F>(&mut self, task: F, handle: &Handle) -> AbortHandle
83    where
84        F: Future<Output = T> + Send + 'static,
85        T: Send + 'static,
86    {
87        self.push_back(handle.spawn(task))
88    }
89
90    /// Spawn the provided task on the current [`LocalSet`] or [`LocalRuntime`]
91    /// and store it in this [`JoinQueue`], returning an [`AbortHandle`] that
92    /// can be used to remotely cancel the task.
93    ///
94    /// The provided future will start running in the background immediately
95    /// when this method is called, even if you don't await anything on this
96    /// [`JoinQueue`].
97    ///
98    /// # Panics
99    ///
100    /// This method panics if it is called outside of a `LocalSet` or `LocalRuntime`.
101    ///
102    /// [`LocalSet`]: tokio::task::LocalSet
103    /// [`LocalRuntime`]: tokio::runtime::LocalRuntime
104    /// [`AbortHandle`]: tokio::task::AbortHandle
105    #[track_caller]
106    pub fn spawn_local<F>(&mut self, task: F) -> AbortHandle
107    where
108        F: Future<Output = T> + 'static,
109        T: 'static,
110    {
111        self.push_back(tokio::task::spawn_local(task))
112    }
113
114    /// Spawn the blocking code on the blocking threadpool and store
115    /// it in this [`JoinQueue`], returning an [`AbortHandle`] that can be
116    /// used to remotely cancel the task.
117    ///
118    /// # Panics
119    ///
120    /// This method panics if called outside of a Tokio runtime.
121    ///
122    /// [`AbortHandle`]: tokio::task::AbortHandle
123    #[track_caller]
124    pub fn spawn_blocking<F>(&mut self, f: F) -> AbortHandle
125    where
126        F: FnOnce() -> T + Send + 'static,
127        T: Send + 'static,
128    {
129        self.push_back(tokio::task::spawn_blocking(f))
130    }
131
132    /// Spawn the blocking code on the blocking threadpool of the
133    /// provided runtime and store it in this [`JoinQueue`], returning an
134    /// [`AbortHandle`] that can be used to remotely cancel the task.
135    ///
136    /// [`AbortHandle`]: tokio::task::AbortHandle
137    #[track_caller]
138    pub fn spawn_blocking_on<F>(&mut self, f: F, handle: &Handle) -> AbortHandle
139    where
140        F: FnOnce() -> T + Send + 'static,
141        T: Send + 'static,
142    {
143        self.push_back(handle.spawn_blocking(f))
144    }
145
146    fn push_back(&mut self, jh: JoinHandle<T>) -> AbortHandle {
147        let jh = AbortOnDropHandle::new(jh);
148        let abort_handle = jh.abort_handle();
149        self.0.push_back(jh);
150        abort_handle
151    }
152
153    /// Waits until the next task in FIFO order completes and returns its output.
154    ///
155    /// Returns `None` if the queue is empty.
156    ///
157    /// # Cancel Safety
158    ///
159    /// This method is cancel safe. If `join_next` is used as the event in a `tokio::select!`
160    /// statement and some other branch completes first, it is guaranteed that no tasks were
161    /// removed from this [`JoinQueue`].
162    pub async fn join_next(&mut self) -> Option<Result<T, JoinError>> {
163        std::future::poll_fn(|cx| self.poll_join_next(cx)).await
164    }
165
166    /// Waits until the next task in FIFO order completes and returns its output,
167    /// along with the [task ID] of the completed task.
168    ///
169    /// Returns `None` if the queue is empty.
170    ///
171    /// When this method returns an error, then the id of the task that failed can be accessed
172    /// using the [`JoinError::id`] method.
173    ///
174    /// # Cancel Safety
175    ///
176    /// This method is cancel safe. If `join_next_with_id` is used as the event in a `tokio::select!`
177    /// statement and some other branch completes first, it is guaranteed that no tasks were
178    /// removed from this [`JoinQueue`].
179    ///
180    /// [task ID]: tokio::task::Id
181    /// [`JoinError::id`]: fn@tokio::task::JoinError::id
182    pub async fn join_next_with_id(&mut self) -> Option<Result<(Id, T), JoinError>> {
183        std::future::poll_fn(|cx| self.poll_join_next_with_id(cx)).await
184    }
185
186    /// Tries to poll an `AbortOnDropHandle` without blocking or yielding.
187    ///
188    /// Note that on success the handle will panic on subsequent polls
189    /// since it becomes consumed.
190    fn try_poll_handle(jh: &mut AbortOnDropHandle<T>) -> Option<Result<T, JoinError>> {
191        if !jh.is_finished() {
192            return None;
193        }
194
195        let waker = futures_util::task::noop_waker();
196        let mut cx = Context::from_waker(&waker);
197
198        // Since this function is not async and cannot be forced to yield, we should
199        // disable budgeting when we want to check for the `JoinHandle` readiness.
200        let jh = std::pin::pin!(tokio::task::coop::unconstrained(jh));
201        if let Poll::Ready(res) = jh.poll(&mut cx) {
202            Some(res)
203        } else {
204            None
205        }
206    }
207
208    /// Tries to join the next task in FIFO order if it has completed.
209    ///
210    /// Returns `None` if the queue is empty or if the next task is not yet ready.
211    pub fn try_join_next(&mut self) -> Option<Result<T, JoinError>> {
212        let jh = self.0.front_mut()?;
213        let res = Self::try_poll_handle(jh)?;
214        // Use `detach` to avoid calling `abort` on a task that has already completed.
215        // Dropping `AbortOnDropHandle` would abort the task, but since it is finished,
216        // we only need to drop the `JoinHandle` for cleanup.
217        drop(self.0.pop_front().unwrap().detach());
218        Some(res)
219    }
220
221    /// Tries to join the next task in FIFO order if it has completed and return its output,
222    /// along with its [task ID].
223    ///
224    /// Returns `None` if the queue is empty or if the next task is not yet ready.
225    ///
226    /// When this method returns an error, then the id of the task that failed can be accessed
227    /// using the [`JoinError::id`] method.
228    ///
229    /// [task ID]: tokio::task::Id
230    /// [`JoinError::id`]: fn@tokio::task::JoinError::id
231    pub fn try_join_next_with_id(&mut self) -> Option<Result<(Id, T), JoinError>> {
232        let jh = self.0.front_mut()?;
233        let res = Self::try_poll_handle(jh)?;
234        // Use `detach` to avoid calling `abort` on a task that has already completed.
235        // Dropping `AbortOnDropHandle` would abort the task, but since it is finished,
236        // we only need to drop the `JoinHandle` for cleanup.
237        let jh = self.0.pop_front().unwrap().detach();
238        let id = jh.id();
239        drop(jh);
240        Some(res.map(|output| (id, output)))
241    }
242
243    /// Aborts all tasks and waits for them to finish shutting down.
244    ///
245    /// Calling this method is equivalent to calling [`abort_all`] and then calling [`join_next`] in
246    /// a loop until it returns `None`.
247    ///
248    /// This method ignores any panics in the tasks shutting down. When this call returns, the
249    /// [`JoinQueue`] will be empty.
250    ///
251    /// [`abort_all`]: fn@Self::abort_all
252    /// [`join_next`]: fn@Self::join_next
253    pub async fn shutdown(&mut self) {
254        self.abort_all();
255        while self.join_next().await.is_some() {}
256    }
257
258    /// Awaits the completion of all tasks in this [`JoinQueue`], returning a vector of their results.
259    ///
260    /// The results will be stored in the order they were spawned, not the order they completed.
261    /// This is a convenience method that is equivalent to calling [`join_next`] in
262    /// a loop. If any tasks on the [`JoinQueue`] fail with an [`JoinError`], then this call
263    /// to `join_all` will panic and all remaining tasks on the [`JoinQueue`] are
264    /// cancelled. To handle errors in any other way, manually call [`join_next`]
265    /// in a loop.
266    ///
267    /// # Cancel Safety
268    ///
269    /// This method is not cancel safe as it calls `join_next` in a loop. If you need
270    /// cancel safety, manually call `join_next` in a loop with `Vec` accumulator.
271    ///
272    /// [`join_next`]: fn@Self::join_next
273    /// [`JoinError::id`]: fn@tokio::task::JoinError::id
274    pub async fn join_all(mut self) -> Vec<T> {
275        let mut output = Vec::with_capacity(self.len());
276
277        while let Some(res) = self.join_next().await {
278            match res {
279                Ok(t) => output.push(t),
280                Err(err) if err.is_panic() => std::panic::resume_unwind(err.into_panic()),
281                Err(err) => panic!("{err}"),
282            }
283        }
284        output
285    }
286
287    /// Aborts all tasks on this [`JoinQueue`].
288    ///
289    /// This does not remove the tasks from the [`JoinQueue`]. To wait for the tasks to complete
290    /// cancellation, you should call `join_next` in a loop until the [`JoinQueue`] is empty.
291    pub fn abort_all(&mut self) {
292        self.0.iter().for_each(|jh| jh.abort());
293    }
294
295    /// Removes all tasks from this [`JoinQueue`] without aborting them.
296    ///
297    /// The tasks removed by this call will continue to run in the background even if the [`JoinQueue`]
298    /// is dropped.
299    pub fn detach_all(&mut self) {
300        self.0.drain(..).for_each(|jh| drop(jh.detach()));
301    }
302
303    /// Polls for the next task in [`JoinQueue`] to complete.
304    ///
305    /// If this returns `Poll::Ready(Some(_))`, then the task that completed is removed from the queue.
306    ///
307    /// When the method returns `Poll::Pending`, the `Waker` in the provided `Context` is scheduled
308    /// to receive a wakeup when a task in the [`JoinQueue`] completes. Note that on multiple calls to
309    /// `poll_join_next`, only the `Waker` from the `Context` passed to the most recent call is
310    /// scheduled to receive a wakeup.
311    ///
312    /// # Returns
313    ///
314    /// This function returns:
315    ///
316    ///  * `Poll::Pending` if the [`JoinQueue`] is not empty but there is no task whose output is
317    ///    available right now.
318    ///  * `Poll::Ready(Some(Ok(value)))` if the next task in this [`JoinQueue`] has completed.
319    ///    The `value` is the return value that task.
320    ///  * `Poll::Ready(Some(Err(err)))` if the next task in this [`JoinQueue`] has panicked or been
321    ///    aborted. The `err` is the `JoinError` from the panicked/aborted task.
322    ///  * `Poll::Ready(None)` if the [`JoinQueue`] is empty.
323    pub fn poll_join_next(&mut self, cx: &mut Context<'_>) -> Poll<Option<Result<T, JoinError>>> {
324        let jh = match self.0.front_mut() {
325            None => return Poll::Ready(None),
326            Some(jh) => jh,
327        };
328        if let Poll::Ready(res) = Pin::new(jh).poll(cx) {
329            // Use `detach` to avoid calling `abort` on a task that has already completed.
330            // Dropping `AbortOnDropHandle` would abort the task, but since it is finished,
331            // we only need to drop the `JoinHandle` for cleanup.
332            drop(self.0.pop_front().unwrap().detach());
333            Poll::Ready(Some(res))
334        } else {
335            Poll::Pending
336        }
337    }
338
339    /// Polls for the next task in [`JoinQueue`] to complete.
340    ///
341    /// If this returns `Poll::Ready(Some(_))`, then the task that completed is removed from the queue.
342    ///
343    /// When the method returns `Poll::Pending`, the `Waker` in the provided `Context` is scheduled
344    /// to receive a wakeup when a task in the [`JoinQueue`] completes. Note that on multiple calls to
345    /// `poll_join_next`, only the `Waker` from the `Context` passed to the most recent call is
346    /// scheduled to receive a wakeup.
347    ///
348    /// # Returns
349    ///
350    /// This function returns:
351    ///
352    ///  * `Poll::Pending` if the [`JoinQueue`] is not empty but there is no task whose output is
353    ///    available right now.
354    ///  * `Poll::Ready(Some(Ok((id, value))))` if the next task in this [`JoinQueue`] has completed.
355    ///    The `value` is the return value that task, and `id` is its [task ID].
356    ///  * `Poll::Ready(Some(Err(err)))` if the next task in this [`JoinQueue`] has panicked or been
357    ///    aborted. The `err` is the `JoinError` from the panicked/aborted task.
358    ///  * `Poll::Ready(None)` if the [`JoinQueue`] is empty.
359    ///
360    /// [task ID]: tokio::task::Id
361    pub fn poll_join_next_with_id(
362        &mut self,
363        cx: &mut Context<'_>,
364    ) -> Poll<Option<Result<(Id, T), JoinError>>> {
365        let jh = match self.0.front_mut() {
366            None => return Poll::Ready(None),
367            Some(jh) => jh,
368        };
369        if let Poll::Ready(res) = Pin::new(jh).poll(cx) {
370            // Use `detach` to avoid calling `abort` on a task that has already completed.
371            // Dropping `AbortOnDropHandle` would abort the task, but since it is finished,
372            // we only need to drop the `JoinHandle` for cleanup.
373            let jh = self.0.pop_front().unwrap().detach();
374            let id = jh.id();
375            drop(jh);
376            // If the task succeeded, add the task ID to the output. Otherwise, the
377            // `JoinError` will already have the task's ID.
378            Poll::Ready(Some(res.map(|output| (id, output))))
379        } else {
380            Poll::Pending
381        }
382    }
383}
384
385impl<T> std::fmt::Debug for JoinQueue<T> {
386    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
387        f.debug_list()
388            .entries(self.0.iter().map(|jh| JoinHandle::id(jh.as_ref())))
389            .finish()
390    }
391}
392
393impl<T> Default for JoinQueue<T> {
394    fn default() -> Self {
395        Self::new()
396    }
397}
398
399/// Collect an iterator of futures into a [`JoinQueue`].
400///
401/// This is equivalent to calling [`JoinQueue::spawn`] on each element of the iterator.
402impl<T, F> std::iter::FromIterator<F> for JoinQueue<T>
403where
404    F: Future<Output = T> + Send + 'static,
405    T: Send + 'static,
406{
407    fn from_iter<I: IntoIterator<Item = F>>(iter: I) -> Self {
408        let mut set = Self::new();
409        iter.into_iter().for_each(|task| {
410            set.spawn(task);
411        });
412        set
413    }
414}
415
416#[cfg(test)]
417mod tests {
418    use super::*;
419
420    /// A simple type that does not implement [`std::fmt::Debug`].
421    struct NotDebug;
422
423    fn is_debug<T: std::fmt::Debug>() {}
424
425    #[test]
426    fn assert_debug() {
427        is_debug::<JoinQueue<NotDebug>>();
428    }
429}