1use std::any::Any;
5use std::marker::PhantomData;
6use std::sync::Arc;
7use std::sync::mpsc::{Receiver, Sender};
8
9#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum TaskOutcome<T> {
15 Completed(T),
17 Cancelled,
19 Panicked(String),
21}
22
23enum ErasedTaskOutcome {
24 Completed(Box<dyn Any + Send>),
25 Cancelled,
26 Panicked(String),
27}
28
29type ResultMsg = (u64, ErasedTaskOutcome);
30
31#[derive(Clone, Copy)]
32struct CancelMsg {
33 id: u64,
34 retain_outcome: bool,
35}
36
37struct TaskJoin {
38 worker_abort: tokio::task::AbortHandle,
39 supervisor: tokio::task::JoinHandle<()>,
40 discard_outcome: bool,
41}
42
43#[must_use = "dropping a TaskHandle cancels the spawned task; store it to poll the result"]
48pub struct TaskHandle<T> {
49 pub(crate) id: u64,
50 cancel: Option<Sender<CancelMsg>>,
51 wake: Option<Arc<tokio::sync::Notify>>,
52 cancellation_requested: bool,
53 _marker: PhantomData<fn() -> T>,
54}
55
56impl<T> TaskHandle<T> {
57 fn new(id: u64, cancel: Sender<CancelMsg>, wake: Option<Arc<tokio::sync::Notify>>) -> Self {
58 Self {
59 id,
60 cancel: Some(cancel),
61 wake,
62 cancellation_requested: false,
63 _marker: PhantomData,
64 }
65 }
66
67 pub(crate) fn id(&self) -> u64 {
68 self.id
69 }
70
71 pub fn cancel(&mut self) {
76 if self.cancellation_requested {
77 return;
78 }
79 self.cancellation_requested = true;
80 if let Some(cancel) = self.cancel.as_ref() {
81 let _ = cancel.send(CancelMsg {
82 id: self.id,
83 retain_outcome: true,
84 });
85 if let Some(wake) = self.wake.as_ref() {
86 wake.notify_one();
87 }
88 }
89 }
90}
91
92impl<T> std::fmt::Debug for TaskHandle<T> {
93 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
94 f.debug_struct("TaskHandle")
95 .field("id", &self.id)
96 .field("cancellation_requested", &self.cancellation_requested)
97 .finish()
98 }
99}
100
101impl<T> Drop for TaskHandle<T> {
102 fn drop(&mut self) {
103 if let Some(cancel) = self.cancel.take() {
104 let _ = cancel.send(CancelMsg {
105 id: self.id,
106 retain_outcome: false,
107 });
108 if let Some(wake) = self.wake.as_ref() {
109 wake.notify_one();
110 }
111 }
112 }
113}
114
115pub(crate) struct AsyncTasks {
119 runtime: Option<tokio::runtime::Handle>,
120 next_id: u64,
121 joins: std::collections::HashMap<u64, TaskJoin>,
122 results: std::collections::HashMap<u64, ErasedTaskOutcome>,
123 result_tx: Option<Sender<ResultMsg>>,
124 result_rx: Option<Receiver<ResultMsg>>,
125 cancel_tx: Sender<CancelMsg>,
126 cancel_rx: Receiver<CancelMsg>,
127 wake: Option<Arc<tokio::sync::Notify>>,
130}
131
132impl Default for AsyncTasks {
133 fn default() -> Self {
134 let (cancel_tx, cancel_rx) = std::sync::mpsc::channel();
135 Self {
136 runtime: None,
137 next_id: 0,
138 joins: std::collections::HashMap::new(),
139 results: std::collections::HashMap::new(),
140 result_tx: None,
141 result_rx: None,
142 cancel_tx,
143 cancel_rx,
144 wake: None,
145 }
146 }
147}
148
149impl Drop for AsyncTasks {
150 fn drop(&mut self) {
151 for (_, join) in self.joins.drain() {
152 join.worker_abort.abort();
153 join.supervisor.abort();
154 }
155 self.results.clear();
156 }
157}
158
159impl AsyncTasks {
160 pub(crate) fn set_runtime(&mut self, handle: tokio::runtime::Handle) {
161 self.runtime = Some(handle);
162 }
163
164 pub(crate) fn set_waker(&mut self, wake: Arc<tokio::sync::Notify>) {
167 self.wake = Some(wake);
168 }
169
170 pub(crate) fn spawn<T: Send + 'static>(
171 &mut self,
172 fut: impl std::future::Future<Output = T> + Send + 'static,
173 ) -> TaskHandle<T> {
174 let runtime = self.runtime.clone().unwrap_or_else(|| {
175 panic!(
176 "Context::spawn requires an active Tokio runtime; call it inside \
177 run_async() / run_async_with()"
178 )
179 });
180
181 if self.result_tx.is_none() {
182 let (tx, rx) = std::sync::mpsc::channel();
183 self.result_tx = Some(tx);
184 self.result_rx = Some(rx);
185 }
186 let result_tx = self
187 .result_tx
188 .clone()
189 .expect("result channel initialized immediately above");
190
191 let id = self.next_id;
192 self.next_id = self
193 .next_id
194 .checked_add(1)
195 .expect("in-frame async task id space exhausted");
196
197 let worker = runtime.spawn(fut);
198 let worker_abort = worker.abort_handle();
199 let completion_wake = self.wake.clone();
200 let supervisor = runtime.spawn(async move {
201 let outcome = match worker.await {
202 Ok(value) => ErasedTaskOutcome::Completed(Box::new(value)),
203 Err(error) if error.is_cancelled() => ErasedTaskOutcome::Cancelled,
204 Err(error) => ErasedTaskOutcome::Panicked(join_panic_message(error)),
205 };
206 let _ = result_tx.send((id, outcome));
207 if let Some(wake) = completion_wake {
208 wake.notify_one();
209 }
210 });
211 self.joins.insert(
212 id,
213 TaskJoin {
214 worker_abort,
215 supervisor,
216 discard_outcome: false,
217 },
218 );
219
220 TaskHandle::new(id, self.cancel_tx.clone(), self.wake.clone())
221 }
222
223 fn drain(&mut self) {
224 if let Some(rx) = self.result_rx.as_ref() {
225 while let Ok((id, outcome)) = rx.try_recv() {
226 let discard = self
227 .joins
228 .remove(&id)
229 .is_some_and(|join| join.discard_outcome);
230 if !discard {
231 self.results.insert(id, outcome);
232 }
233 }
234 }
235 while let Ok(cancel) = self.cancel_rx.try_recv() {
236 self.cancel(cancel);
237 }
238 }
239
240 pub(crate) fn maintain(&mut self) {
241 self.drain();
242 }
243
244 pub(crate) fn poll<T: 'static>(&mut self, id: u64) -> Option<T> {
245 match self.poll_outcome(id)? {
246 TaskOutcome::Completed(value) => Some(value),
247 TaskOutcome::Cancelled | TaskOutcome::Panicked(_) => None,
248 }
249 }
250
251 pub(crate) fn poll_outcome<T: 'static>(&mut self, id: u64) -> Option<TaskOutcome<T>> {
252 self.drain();
253 let outcome = self.results.remove(&id)?;
254 match outcome {
255 ErasedTaskOutcome::Completed(value) => match value.downcast::<T>() {
256 Ok(value) => Some(TaskOutcome::Completed(*value)),
257 Err(value) => {
258 self.results.insert(id, ErasedTaskOutcome::Completed(value));
259 None
260 }
261 },
262 ErasedTaskOutcome::Cancelled => Some(TaskOutcome::Cancelled),
263 ErasedTaskOutcome::Panicked(message) => Some(TaskOutcome::Panicked(message)),
264 }
265 }
266
267 fn cancel(&mut self, cancel: CancelMsg) {
268 if !cancel.retain_outcome {
269 self.results.remove(&cancel.id);
270 }
271 if let Some(join) = self.joins.get_mut(&cancel.id) {
272 join.discard_outcome |= !cancel.retain_outcome;
273 join.worker_abort.abort();
274 }
275 }
276}
277
278fn join_panic_message(error: tokio::task::JoinError) -> String {
279 debug_assert!(error.is_panic());
280 let payload = error.into_panic();
281 if let Some(message) = payload.downcast_ref::<&str>() {
282 (*message).to_owned()
283 } else if let Some(message) = payload.downcast_ref::<String>() {
284 message.clone()
285 } else {
286 "task panicked with a non-string payload".to_owned()
287 }
288}
289
290#[cfg(test)]
291mod tests {
292 use super::*;
293 use std::time::Duration;
294
295 async fn wait_for_outcome<T: 'static>(
296 tasks: &mut AsyncTasks,
297 handle: &TaskHandle<T>,
298 ) -> TaskOutcome<T> {
299 for _ in 0..200 {
300 if let Some(outcome) = tasks.poll_outcome(handle.id()) {
301 return outcome;
302 }
303 tokio::time::sleep(Duration::from_millis(1)).await;
304 }
305 panic!("task outcome was not delivered within timeout");
306 }
307
308 #[tokio::test]
309 async fn completed_cancelled_and_panicked_tasks_are_reaped() {
310 let mut tasks = AsyncTasks::default();
311 tasks.set_runtime(tokio::runtime::Handle::current());
312
313 let completed = tasks.spawn(async { 7u32 });
314 assert_eq!(
315 wait_for_outcome(&mut tasks, &completed).await,
316 TaskOutcome::Completed(7)
317 );
318 assert!(!tasks.joins.contains_key(&completed.id()));
319
320 let mut cancelled = tasks.spawn(async {
321 tokio::time::sleep(Duration::from_secs(60)).await;
322 9u32
323 });
324 cancelled.cancel();
325 assert_eq!(
326 wait_for_outcome(&mut tasks, &cancelled).await,
327 TaskOutcome::Cancelled
328 );
329 assert!(!tasks.joins.contains_key(&cancelled.id()));
330
331 let panicked = tasks.spawn(async {
332 panic!("task exploded");
333 #[allow(unreachable_code)]
334 11u32
335 });
336 let outcome = wait_for_outcome(&mut tasks, &panicked).await;
337 assert!(matches!(
338 outcome,
339 TaskOutcome::Panicked(message) if message.contains("task exploded")
340 ));
341 assert!(!tasks.joins.contains_key(&panicked.id()));
342 }
343
344 #[tokio::test]
345 async fn completion_and_cancellation_coalesce_wake_notifications() {
346 let mut tasks = AsyncTasks::default();
347 tasks.set_runtime(tokio::runtime::Handle::current());
348 let wake = Arc::new(tokio::sync::Notify::new());
349 tasks.set_waker(Arc::clone(&wake));
350
351 let handle = tasks.spawn(async { 1u8 });
352 tokio::time::timeout(Duration::from_secs(1), wake.notified())
353 .await
354 .expect("task completion should wake the dispatcher");
355 assert_eq!(
356 wait_for_outcome(&mut tasks, &handle).await,
357 TaskOutcome::Completed(1)
358 );
359
360 let mut cancelled = tasks.spawn(async {
361 tokio::time::sleep(Duration::from_secs(60)).await;
362 });
363 cancelled.cancel();
364 tokio::time::timeout(Duration::from_secs(1), wake.notified())
365 .await
366 .expect("task cancellation should wake the dispatcher");
367 }
368
369 #[tokio::test]
370 async fn thousands_of_task_ids_leave_no_join_entries() {
371 let mut tasks = AsyncTasks::default();
372 tasks.set_runtime(tokio::runtime::Handle::current());
373 let handles: Vec<_> = (0..2_000u32)
374 .map(|value| tasks.spawn(async move { value }))
375 .collect();
376
377 for handle in &handles {
378 assert!(matches!(
379 wait_for_outcome(&mut tasks, handle).await,
380 TaskOutcome::Completed(_)
381 ));
382 }
383 assert!(tasks.joins.is_empty());
384 assert!(tasks.results.is_empty());
385 }
386}