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}