Skip to main content

linera_core/
join_set_ext.rs

1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! An extension trait to allow determining at compile time how tasks are spawned on the Tokio
5//! runtime.
6//!
7//! In most cases the [`Future`] task to be spawned should implement [`Send`], but that's
8//! not possible when compiling for the Web. In that case, the task is spawned on the
9//! browser event loop.
10
11use futures::channel::oneshot;
12
13#[cfg(web)]
14mod implementation {
15    pub use futures::future::AbortHandle;
16    use futures::{future, stream, StreamExt as _};
17
18    use super::*;
19
20    /// The set of tasks spawned on the current thread in a Web environment.
21    #[derive(Default)]
22    pub struct JoinSet(Vec<oneshot::Receiver<()>>);
23
24    /// An extension trait for the [`JoinSet`] type.
25    pub trait JoinSetExt: Sized {
26        /// Spawns a `future` task on this [`JoinSet`] using [`JoinSet::spawn_local`].
27        ///
28        /// Returns a [`oneshot::Receiver`] to receive the `future`'s output, and an
29        /// [`AbortHandle`] to cancel execution of the task.
30        fn spawn_task<F: Future + 'static>(&mut self, future: F) -> TaskHandle<F::Output>;
31
32        /// Awaits all tasks spawned in this [`JoinSet`].
33        ///
34        /// Unlike its native counterpart this cannot re-raise a panic: on the Web a task's
35        /// panic aborts the whole Wasm instance, so there is nothing left to re-raise.
36        fn await_all_tasks(&mut self) -> impl Future<Output = ()>;
37
38        /// Awaits all tasks spawned in this [`JoinSet`].
39        ///
40        /// Identical to [`JoinSetExt::await_all_tasks`] here; the two differ only on
41        /// native targets, where a task's panic does not stop the process.
42        fn await_all_tasks_logging_panics(&mut self) -> impl Future<Output = ()>;
43
44        /// Reaps tasks that have finished.
45        fn reap_finished_tasks(&mut self);
46    }
47
48    impl JoinSetExt for JoinSet {
49        fn spawn_task<F: Future + 'static>(&mut self, future: F) -> TaskHandle<F::Output> {
50            let (abort_handle, abort_registration) = AbortHandle::new_pair();
51            let (send_done, recv_done) = oneshot::channel();
52            let (send_output, recv_output) = oneshot::channel();
53            let future = async move {
54                // Receiver may have been dropped if the task was aborted.
55                send_output.send(future.await).ok();
56                send_done.send(()).ok();
57            };
58            self.0.push(recv_done);
59            wasm_bindgen_futures::spawn_local(
60                future::Abortable::new(future, abort_registration).map(drop),
61            );
62
63            TaskHandle {
64                output_receiver: recv_output,
65                abort_handle,
66            }
67        }
68
69        async fn await_all_tasks(&mut self) {
70            stream::iter(&mut self.0)
71                .then(|x| x)
72                .map(drop)
73                .collect()
74                .await
75        }
76
77        async fn await_all_tasks_logging_panics(&mut self) {
78            self.await_all_tasks().await
79        }
80
81        fn reap_finished_tasks(&mut self) {
82            self.0.retain_mut(|task| task.try_recv() == Ok(None));
83        }
84    }
85}
86
87#[cfg(not(web))]
88mod implementation {
89    pub use tokio::task::AbortHandle;
90
91    use super::*;
92
93    /// The set of tasks spawned on the Tokio runtime.
94    pub type JoinSet = tokio::task::JoinSet<()>;
95
96    /// An extension trait for the [`JoinSet`] type.
97    #[trait_variant::make(Send)]
98    pub trait JoinSetExt: Sized {
99        /// Spawns a `future` task on this [`JoinSet`] using [`JoinSet::spawn`].
100        ///
101        /// Returns a [`oneshot::Receiver`] to receive the `future`'s output, and an
102        /// [`AbortHandle`] to cancel execution of the task.
103        fn spawn_task<F: Future<Output: Send> + Send + 'static>(
104            &mut self,
105            future: F,
106        ) -> TaskHandle<F::Output>;
107
108        /// Awaits all tasks spawned in this [`JoinSet`], re-raising the panic of any task
109        /// that panicked.
110        ///
111        /// Tokio catches a task's panic instead of stopping the runtime, so a set of
112        /// long-lived tasks that is merely drained leaves the process running with one of
113        /// its subsystems silently gone. Re-raising turns that into an exit, which a
114        /// supervisor can act on.
115        ///
116        /// For sets of tasks that each serve a single request or connection, where losing
117        /// one is recoverable and exiting would let one caller stop the process, use
118        /// [`JoinSetExt::await_all_tasks_logging_panics`] instead.
119        async fn await_all_tasks(&mut self);
120
121        /// Awaits all tasks spawned in this [`JoinSet`], logging rather than re-raising
122        /// the panic of any task that panicked.
123        async fn await_all_tasks_logging_panics(&mut self);
124
125        /// Reaps tasks that have finished.
126        fn reap_finished_tasks(&mut self);
127    }
128
129    impl JoinSetExt for JoinSet {
130        fn spawn_task<F>(&mut self, future: F) -> TaskHandle<F::Output>
131        where
132            F: Future + Send + 'static,
133            F::Output: Send,
134        {
135            let (output_sender, output_receiver) = oneshot::channel();
136
137            let abort_handle = self.spawn(async move {
138                // Receiver may have been dropped if the task was aborted.
139                output_sender.send(future.await).ok();
140            });
141
142            TaskHandle {
143                output_receiver,
144                abort_handle,
145            }
146        }
147
148        async fn await_all_tasks(&mut self) {
149            while let Some(result) = self.join_next().await {
150                if let Err(error) = result {
151                    match error.try_into_panic() {
152                        // Tokio contained the panic, so the process is still running
153                        // without whatever this task was doing. Put it back.
154                        Ok(payload) => std::panic::resume_unwind(payload),
155                        Err(error) => tracing::debug!(%error, "Task was cancelled"),
156                    }
157                }
158            }
159        }
160
161        async fn await_all_tasks_logging_panics(&mut self) {
162            while let Some(result) = self.join_next().await {
163                if let Err(error) = result {
164                    if error.is_panic() {
165                        tracing::error!(%error, "Task panicked");
166                    } else {
167                        tracing::debug!(%error, "Task was cancelled");
168                    }
169                }
170            }
171        }
172
173        fn reap_finished_tasks(&mut self) {
174            while self.try_join_next().is_some() {}
175        }
176    }
177}
178
179use std::{
180    future::Future,
181    pin::Pin,
182    task::{Context, Poll},
183};
184
185use futures::FutureExt as _;
186pub use implementation::*;
187
188/// A handle to a task spawned with [`JoinSetExt`].
189///
190/// Dropping a handle detaches its respective task.
191pub struct TaskHandle<Output> {
192    output_receiver: oneshot::Receiver<Output>,
193    abort_handle: AbortHandle,
194}
195
196impl<Output> Future for TaskHandle<Output> {
197    type Output = Result<Output, oneshot::Canceled>;
198
199    fn poll(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Self::Output> {
200        self.as_mut().output_receiver.poll_unpin(context)
201    }
202}
203
204impl<Output> TaskHandle<Output> {
205    /// Aborts the task.
206    pub fn abort(&self) {
207        self.abort_handle.abort();
208    }
209
210    /// Returns [`true`] if the task is still running.
211    pub fn is_running(&mut self) -> bool {
212        self.output_receiver.try_recv().is_err()
213    }
214}
215
216#[cfg(all(test, not(web)))]
217mod tests {
218    use futures::future;
219
220    use super::*;
221
222    /// A set of long-lived tasks must not lose one silently: the panic reaches whoever
223    /// awaits the set, and from there the process.
224    #[tokio::test]
225    async fn test_await_all_tasks_reraises_a_panic() {
226        let joined = tokio::spawn(async {
227            let mut join_set = JoinSet::new();
228            join_set.spawn_task(async {
229                panic!("task panicked");
230            });
231            join_set.await_all_tasks().await;
232        })
233        .await;
234
235        assert!(
236            joined.expect_err("the panic reached the caller").is_panic(),
237            "await_all_tasks re-raised the task's panic",
238        );
239    }
240
241    #[tokio::test]
242    async fn test_await_all_tasks_logging_panics_keeps_going() {
243        let mut join_set = JoinSet::new();
244        join_set.spawn_task(async {
245            panic!("task panicked");
246        });
247        join_set.spawn_task(async {});
248
249        join_set.await_all_tasks_logging_panics().await;
250    }
251
252    /// Aborting a task is how shutdown works, so it must not be mistaken for a failure.
253    #[tokio::test]
254    async fn test_await_all_tasks_ignores_cancelled_tasks() {
255        let mut join_set = JoinSet::new();
256        let handle = join_set.spawn_task(future::pending::<()>());
257        handle.abort();
258
259        join_set.await_all_tasks().await;
260    }
261}