Skip to main content

commonware_runtime/utils/
handle.rs

1use crate::{
2    Error,
3    telemetry::metrics::raw::Gauge,
4    utils::{extract_panic_message, supervision::Tree},
5};
6use commonware_utils::{
7    channel::oneshot,
8    sync::{Mutex, Once},
9};
10use futures::{
11    FutureExt as _,
12    future::{Either, poll_fn, select},
13    pin_mut,
14    stream::{AbortHandle, Abortable, Aborted},
15};
16use std::{
17    any::Any,
18    future::Future,
19    panic::{AssertUnwindSafe, resume_unwind},
20    pin::Pin,
21    sync::Arc,
22    task::{Context, Poll},
23};
24use tracing::error;
25
26/// Handle to an asynchronous result.
27///
28/// Handles returned by [`crate::Spawner::spawn`] abort the spawned task. Completion handles only
29/// stop waiting when aborted, resolving to [`Error::Aborted`]; they do not cancel the underlying
30/// work.
31pub struct Handle<T>
32where
33    T: Send + 'static,
34{
35    state: HandleState<T>,
36}
37
38/// Distinguishes handles that own spawned work from handles that only wait on completion.
39enum HandleState<T>
40where
41    T: Send + 'static,
42{
43    Task {
44        receiver: oneshot::Receiver<Result<T, Error>>,
45        abort_handle: AbortHandle,
46        metric: MetricHandle,
47    },
48    Completion {
49        future: Abortable<Completion<T>>,
50        abort_handle: AbortHandle,
51    },
52}
53
54/// Aborts every owned handle when a group is no longer supervised.
55struct HandleGroup<T>(Vec<Handle<T>>)
56where
57    T: Send + 'static;
58
59impl<T> Drop for HandleGroup<T>
60where
61    T: Send + 'static,
62{
63    fn drop(&mut self) {
64        for handle in &self.0 {
65            handle.abort();
66        }
67    }
68}
69
70/// Normalizes receiver-backed and future-backed completions behind one abortable future.
71enum Completion<T>
72where
73    T: Send + 'static,
74{
75    Receiver(oneshot::Receiver<Result<T, Error>>),
76    Future(Pin<Box<dyn Future<Output = Result<T, Error>> + Send + 'static>>),
77}
78
79impl<T> Unpin for Completion<T> where T: Send + 'static {}
80
81impl<T> Future for Completion<T>
82where
83    T: Send + 'static,
84{
85    type Output = Result<T, Error>;
86
87    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
88        match &mut *self {
89            Self::Receiver(receiver) => Pin::new(receiver)
90                .poll(cx)
91                .map(|result| result.unwrap_or(Err(Error::Closed))),
92            Self::Future(future) => future.as_mut().poll(cx),
93        }
94    }
95}
96
97impl<T> Handle<T>
98where
99    T: Send + 'static,
100{
101    #[inline(always)]
102    pub(crate) fn init<F>(
103        f: F,
104        metric: MetricHandle,
105        panicker: Panicker,
106        tree: Arc<Tree>,
107    ) -> (impl Future<Output = ()>, Self)
108    where
109        F: Future<Output = T> + Send + 'static,
110    {
111        // Initialize channels to handle result/abort
112        let (sender, receiver) = oneshot::channel();
113        let (abort_handle, abort_registration) = AbortHandle::new_pair();
114
115        // Wrap the future with panic catching, abort support, and cleanup.
116        //
117        // Everything is done in a single async block (and the function is marked
118        // #[inline(always)]) so that stack usage is `size_of(F) + constant` rather than
119        // `N * size_of(F)` (which is what a combinator chain produces in debug builds).
120        let metric_handle = metric.clone();
121        let task = async move {
122            // Run future with panic catching and abort support
123            let result =
124                Abortable::new(AssertUnwindSafe(f).catch_unwind(), abort_registration).await;
125
126            // Handle result
127            match result {
128                Ok(Ok(result)) => {
129                    let _ = sender.send(Ok(result));
130                }
131                Ok(Err(panic)) => {
132                    panicker.notify(panic);
133                    let _ = sender.send(Err(Error::Exited));
134                }
135                Err(Aborted) => {}
136            }
137
138            // Mark the task as aborted and abort all descendants.
139            tree.abort();
140
141            // Finish the metric.
142            metric_handle.finish();
143        };
144
145        (
146            task,
147            Self {
148                state: HandleState::Task {
149                    receiver,
150                    abort_handle,
151                    metric,
152                },
153            },
154        )
155    }
156
157    /// Returns a handle backed by a completion receiver.
158    pub fn from_receiver(receiver: oneshot::Receiver<Result<T, Error>>) -> Self {
159        let (abort_handle, abort_registration) = AbortHandle::new_pair();
160        Self {
161            state: HandleState::Completion {
162                future: Abortable::new(Completion::Receiver(receiver), abort_registration),
163                abort_handle,
164            },
165        }
166    }
167
168    /// Returns a handle backed by a completion future.
169    pub fn from_future<F>(future: F) -> Self
170    where
171        F: Future<Output = Result<T, Error>> + Send + 'static,
172    {
173        let (abort_handle, abort_registration) = AbortHandle::new_pair();
174        Self {
175            state: HandleState::Completion {
176                future: Abortable::new(Completion::Future(Box::pin(future)), abort_registration),
177                abort_handle,
178            },
179        }
180    }
181
182    /// Returns a handle that is already complete.
183    pub fn ready(result: Result<T, Error>) -> Self {
184        let (sender, receiver) = oneshot::channel();
185        let _ = sender.send(result);
186        Self::from_receiver(receiver)
187    }
188
189    /// Waits for the first handle to complete and aborts all handles before returning.
190    ///
191    /// Dropping the returned future also aborts every handle. Selection is biased toward handles
192    /// that appear earlier in the iterator.
193    ///
194    /// Runtime supervision already aborts a task's descendants when that task exits. This method
195    /// is intended for an owned group whose teardown boundary is the first handle completion,
196    /// independent of whether the caller exits immediately afterward.
197    ///
198    /// # Examples
199    ///
200    /// ```
201    /// # futures::executor::block_on(async {
202    /// use commonware_runtime::Handle;
203    ///
204    /// let handles = [
205    ///     Handle::ready(Ok(7)),
206    ///     Handle::from_future(futures::future::pending()),
207    /// ];
208    /// assert_eq!(Handle::select(handles).await.unwrap(), 7);
209    /// # });
210    /// ```
211    ///
212    /// # Errors
213    ///
214    /// Returns [`Error::Closed`] if `handles` is empty.
215    pub fn select(
216        handles: impl IntoIterator<Item = Self>,
217    ) -> impl Future<Output = Result<T, Error>> + Send + 'static {
218        // Construct the guard before returning so dropping the future without polling still aborts
219        // every handle.
220        let mut handles = HandleGroup(handles.into_iter().collect::<Vec<_>>());
221
222        async move {
223            if handles.0.is_empty() {
224                return Err(Error::Closed);
225            }
226
227            poll_fn(move |cx| {
228                for handle in &mut handles.0 {
229                    if let Poll::Ready(result) = Pin::new(handle).poll(cx) {
230                        return Poll::Ready(result);
231                    }
232                }
233                Poll::Pending
234            })
235            .await
236        }
237    }
238
239    /// Returns a handle that resolves to [`Error::Closed`] without spawning work.
240    pub(crate) fn closed(metric: MetricHandle) -> Self {
241        // Mark the task as finished immediately so gauges remain accurate.
242        metric.finish();
243
244        // Create a receiver that will yield `Err(Error::Closed)` when awaited.
245        let (sender, receiver) = oneshot::channel();
246        drop(sender);
247
248        Self::from_receiver(receiver)
249    }
250
251    /// Abort the spawned task or stop waiting for a completion.
252    pub fn abort(&self) {
253        match &self.state {
254            HandleState::Task {
255                abort_handle,
256                metric,
257                ..
258            } => {
259                abort_handle.abort();
260
261                // We might never poll the future again after aborting it, so run the
262                // metric cleanup right away.
263                metric.finish();
264            }
265            HandleState::Completion { abort_handle, .. } => {
266                abort_handle.abort();
267            }
268        }
269    }
270
271    /// Returns a helper that aborts the task and updates metrics consistently.
272    pub(crate) fn aborter(&self) -> Option<Aborter> {
273        match &self.state {
274            HandleState::Task {
275                abort_handle,
276                metric,
277                ..
278            } => Some(Aborter::new(abort_handle.clone(), metric.clone())),
279            HandleState::Completion { .. } => None,
280        }
281    }
282}
283
284impl<T> Future for Handle<T>
285where
286    T: Send + 'static,
287{
288    type Output = Result<T, Error>;
289
290    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
291        match &mut self.state {
292            HandleState::Task { receiver, .. } => Pin::new(receiver)
293                .poll(cx)
294                .map(|result| result.unwrap_or_else(|_| Err(Error::Closed))),
295            HandleState::Completion { future, .. } => Pin::new(future)
296                .poll(cx)
297                .map(|result| result.unwrap_or(Err(Error::Aborted))),
298        }
299    }
300}
301
302/// Tracks the metric state associated with a spawned task handle.
303#[derive(Clone)]
304pub(crate) struct MetricHandle {
305    gauge: Gauge,
306    finished: Arc<Once>,
307}
308
309impl MetricHandle {
310    /// Increments the supplied gauge and returns a handle responsible for
311    /// eventually decrementing it.
312    pub(crate) fn new(gauge: Gauge) -> Self {
313        gauge.inc();
314
315        Self {
316            gauge,
317            finished: Arc::new(Once::new()),
318        }
319    }
320
321    /// Marks the task handle as completed and decrements the gauge once.
322    ///
323    /// This method is idempotent, additional calls are ignored so completion
324    /// and abort paths can invoke it independently.
325    pub(crate) fn finish(&self) {
326        let gauge = self.gauge.clone();
327        self.finished.call_once(move || {
328            gauge.dec();
329        });
330    }
331}
332
333/// A panic emitted by a spawned task.
334pub type Panic = Box<dyn Any + Send + 'static>;
335
336/// Notifies the runtime when a spawned task panics, so it can propagate the failure.
337#[derive(Clone)]
338pub(crate) struct Panicker {
339    catch: bool,
340    sender: Arc<Mutex<Option<oneshot::Sender<Panic>>>>,
341}
342
343impl Panicker {
344    /// Creates a new [Panicker].
345    pub(crate) fn new(catch: bool) -> (Self, Panicked) {
346        let (sender, receiver) = oneshot::channel();
347        let panicker = Self {
348            catch,
349            sender: Arc::new(Mutex::new(Some(sender))),
350        };
351        let panicked = Panicked { receiver };
352        (panicker, panicked)
353    }
354
355    /// Returns whether the [Panicker] is configured to catch panics.
356    #[commonware_macros::stability(ALPHA)]
357    pub(crate) const fn catch(&self) -> bool {
358        self.catch
359    }
360
361    /// Notifies the [Panicker] that a panic has occurred.
362    pub(crate) fn notify(&self, panic: Box<dyn Any + Send + 'static>) {
363        // Log the panic
364        let err = extract_panic_message(&*panic);
365        error!(?err, "task panicked");
366
367        // If we are catching panics, just return
368        if self.catch {
369            return;
370        }
371
372        // If we've already sent a panic, ignore the new one
373        let mut sender = self.sender.lock();
374        let Some(sender) = sender.take() else {
375            return;
376        };
377
378        // Send the panic
379        let _ = sender.send(panic);
380    }
381}
382
383/// A handle that will be notified when a panic occurs.
384pub(crate) struct Panicked {
385    receiver: oneshot::Receiver<Panic>,
386}
387
388impl Panicked {
389    /// Polls a task that should be interrupted by a panic.
390    pub(crate) async fn interrupt<Fut>(self, task: Fut) -> Fut::Output
391    where
392        Fut: Future,
393    {
394        // Wait for task to complete or panic
395        let panicked = self.receiver;
396        pin_mut!(panicked);
397        pin_mut!(task);
398        match select(panicked, task).await {
399            Either::Left((panic, task)) => match panic {
400                // If there is a panic, resume the unwind
401                Ok(panic) => {
402                    resume_unwind(panic);
403                }
404                // If there can never be a panic (oneshot is closed), wait for the task to complete
405                // and return the output
406                Err(_) => task.await,
407            },
408            Either::Right((output, _)) => {
409                // Return the output
410                output
411            }
412        }
413    }
414}
415
416/// Couples an [`AbortHandle`] with its metric handle so aborted tasks clean up gauges.
417pub(crate) struct Aborter {
418    inner: AbortHandle,
419    metric: MetricHandle,
420}
421
422impl Aborter {
423    /// Creates a new [`Aborter`] for the provided abort handle and metric handle.
424    pub(crate) const fn new(inner: AbortHandle, metric: MetricHandle) -> Self {
425        Self { inner, metric }
426    }
427
428    /// Aborts the task and records completion in the metric gauge.
429    pub(crate) fn abort(self) {
430        self.inner.abort();
431
432        // We might never poll the future again after aborting it, so run the
433        // metric cleanup right away
434        self.metric.finish();
435    }
436}
437
438#[cfg(test)]
439mod tests {
440    use super::Handle;
441    use crate::{Error, Metrics as _, Runner, Spawner, Supervisor as _, deterministic};
442    use commonware_utils::channel::oneshot;
443    use futures::future;
444
445    const METRIC_PREFIX: &str = "runtime_tasks_running{";
446
447    fn running_tasks_for_label(metrics: &str, label: &str) -> Option<u64> {
448        let label_fragment = format!("name=\"{label}\"");
449        metrics.lines().find_map(|line| {
450            if line.starts_with(METRIC_PREFIX) && line.contains(&label_fragment) {
451                line.rsplit_once(' ')
452                    .and_then(|(_, value)| value.trim().parse::<u64>().ok())
453            } else {
454                None
455            }
456        })
457    }
458
459    #[test]
460    fn tasks_running_decreased_after_completion() {
461        const LABEL: &str = "tasks_running_after_completion";
462
463        let runner = deterministic::Runner::default();
464        runner.start(|context| async move {
465            let handle = context.child(LABEL).spawn(|_| async move { "done" });
466
467            let metrics = context.encode();
468            assert_eq!(
469                running_tasks_for_label(&metrics, LABEL),
470                Some(1),
471                "expected tasks_running gauge to be 1 before completion: {metrics}",
472            );
473
474            let output = handle.await.expect("task failed");
475            assert_eq!(output, "done");
476
477            let metrics = context.encode();
478            assert_eq!(
479                running_tasks_for_label(&metrics, LABEL),
480                Some(0),
481                "expected tasks_running gauge to return to 0 after completion: {metrics}",
482            );
483        });
484    }
485
486    #[test]
487    fn tasks_running_unchanged_when_handle_dropped() {
488        const LABEL: &str = "tasks_running_unchanged";
489
490        let runner = deterministic::Runner::default();
491        runner.start(|context| async move {
492            let handle = context.child(LABEL).spawn(|_| async move {
493                future::pending::<()>().await;
494            });
495
496            let metrics = context.encode();
497            assert_eq!(
498                running_tasks_for_label(&metrics, LABEL),
499                Some(1),
500                "expected tasks_running gauge to be 1 before dropping handle: {metrics}",
501            );
502
503            drop(handle);
504
505            let metrics = context.encode();
506            assert_eq!(
507                running_tasks_for_label(&metrics, LABEL),
508                Some(1),
509                "dropping handle should not finish metrics: {metrics}",
510            );
511        });
512    }
513
514    #[test]
515    fn tasks_running_decreased_immediately_on_abort_via_handle() {
516        const LABEL: &str = "tasks_running_abort_via_handle";
517
518        let runner = deterministic::Runner::default();
519        runner.start(|context| async move {
520            let handle = context.child(LABEL).spawn(|_| async move {
521                future::pending::<()>().await;
522            });
523
524            let metrics = context.encode();
525            assert_eq!(
526                running_tasks_for_label(&metrics, LABEL),
527                Some(1),
528                "expected tasks_running gauge to be 1 before abort: {metrics}",
529            );
530
531            handle.abort();
532
533            let metrics = context.encode();
534            assert_eq!(
535                running_tasks_for_label(&metrics, LABEL),
536                Some(0),
537                "expected tasks_running gauge to return to 0 after abort: {metrics}",
538            );
539        });
540    }
541
542    #[test]
543    fn select_aborts_remaining_tasks() {
544        const LABEL: &str = "tasks_running_select_remaining";
545
546        deterministic::Runner::default().start(|context| async move {
547            let completed = context.child("select_completed").spawn(|_| async {});
548            let pending = context.child(LABEL).spawn(|_| future::pending());
549
550            Handle::select([completed, pending])
551                .await
552                .expect("task failed");
553
554            let metrics = context.encode();
555            assert_eq!(
556                running_tasks_for_label(&metrics, LABEL),
557                Some(0),
558                "select should abort remaining tasks: {metrics}",
559            );
560        });
561    }
562
563    #[test]
564    fn select_empty_returns_closed() {
565        deterministic::Runner::default().start(|_| async move {
566            assert!(matches!(Handle::<()>::select([]).await, Err(Error::Closed)));
567        });
568    }
569
570    #[test]
571    fn dropping_select_aborts_tasks_before_polling() {
572        const FIRST_LABEL: &str = "tasks_running_select_drop_first";
573        const SECOND_LABEL: &str = "tasks_running_select_drop_second";
574
575        deterministic::Runner::default().start(|context| async move {
576            let first = context
577                .child(FIRST_LABEL)
578                .spawn(|_| future::pending::<()>());
579            let second = context
580                .child(SECOND_LABEL)
581                .spawn(|_| future::pending::<()>());
582
583            drop(Handle::select([first, second]));
584
585            let metrics = context.encode();
586            assert_eq!(running_tasks_for_label(&metrics, FIRST_LABEL), Some(0));
587            assert_eq!(running_tasks_for_label(&metrics, SECOND_LABEL), Some(0));
588        });
589    }
590
591    #[test]
592    fn completion_handle_abort_stops_waiting() {
593        deterministic::Runner::default().start(|_| async move {
594            let (sender, receiver) = oneshot::channel();
595            let handle = Handle::from_receiver(receiver);
596
597            handle.abort();
598
599            assert!(sender.send(Ok(())).is_ok());
600            assert!(matches!(handle.await, Err(Error::Aborted)));
601        });
602    }
603
604    #[test]
605    fn tasks_running_decreased_after_blocking_completion() {
606        const LABEL: &str = "tasks_running_after_blocking_completion";
607
608        let runner = deterministic::Runner::default();
609        runner.start(|context| async move {
610            let blocking_handle = context.child(LABEL).shared(true).spawn(|_| async move {
611                // Simulate some blocking work
612                42
613            });
614
615            let metrics = context.encode();
616            assert_eq!(
617                running_tasks_for_label(&metrics, LABEL),
618                Some(1),
619                "expected tasks_running gauge to be 1 while blocking task runs: {metrics}",
620            );
621
622            let result = blocking_handle.await.expect("blocking task failed");
623            assert_eq!(result, 42);
624
625            let metrics = context.encode();
626            assert_eq!(
627                running_tasks_for_label(&metrics, LABEL),
628                Some(0),
629                "expected tasks_running gauge to return to 0 after blocking task completes: {metrics}",
630            );
631        });
632    }
633
634    #[test]
635    fn tasks_running_decreased_immediately_on_abort_via_aborter() {
636        const LABEL: &str = "tasks_running_abort_via_aborter";
637
638        let runner = deterministic::Runner::default();
639        runner.start(|context| async move {
640            let handle = context.child(LABEL).spawn(|_| async move {
641                future::pending::<()>().await;
642            });
643
644            let metrics = context.encode();
645            assert_eq!(
646                running_tasks_for_label(&metrics, LABEL),
647                Some(1),
648                "expected tasks_running gauge to be 1 before abort: {metrics}",
649            );
650
651            let aborter = handle.aborter().unwrap();
652            aborter.abort();
653
654            let metrics = context.encode();
655            assert_eq!(
656                running_tasks_for_label(&metrics, LABEL),
657                Some(0),
658                "expected tasks_running gauge to return to 0 after abort: {metrics}",
659            );
660        });
661    }
662}