linera_core/
join_set_ext.rs1use 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 #[derive(Default)]
22 pub struct JoinSet(Vec<oneshot::Receiver<()>>);
23
24 pub trait JoinSetExt: Sized {
26 fn spawn_task<F: Future + 'static>(&mut self, future: F) -> TaskHandle<F::Output>;
31
32 fn await_all_tasks(&mut self) -> impl Future<Output = ()>;
37
38 fn await_all_tasks_logging_panics(&mut self) -> impl Future<Output = ()>;
43
44 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 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 pub type JoinSet = tokio::task::JoinSet<()>;
95
96 #[trait_variant::make(Send)]
98 pub trait JoinSetExt: Sized {
99 fn spawn_task<F: Future<Output: Send> + Send + 'static>(
104 &mut self,
105 future: F,
106 ) -> TaskHandle<F::Output>;
107
108 async fn await_all_tasks(&mut self);
120
121 async fn await_all_tasks_logging_panics(&mut self);
124
125 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 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 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
188pub 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 pub fn abort(&self) {
207 self.abort_handle.abort();
208 }
209
210 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 #[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 #[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}