Skip to main content

commonware_runtime/utils/
handle.rs

1use crate::{
2    telemetry::metrics::raw::Gauge,
3    utils::{extract_panic_message, supervision::Tree},
4    Error,
5};
6use commonware_utils::{
7    channel::oneshot,
8    sync::{Mutex, Once},
9};
10use futures::{
11    future::{select, Either},
12    pin_mut,
13    stream::{AbortHandle, Abortable, Aborted},
14    FutureExt as _,
15};
16use std::{
17    any::Any,
18    future::Future,
19    panic::{resume_unwind, AssertUnwindSafe},
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/// Normalizes receiver-backed and future-backed completions behind one abortable future.
55enum Completion<T>
56where
57    T: Send + 'static,
58{
59    Receiver(oneshot::Receiver<Result<T, Error>>),
60    Future(Pin<Box<dyn Future<Output = Result<T, Error>> + Send + 'static>>),
61}
62
63impl<T> Unpin for Completion<T> where T: Send + 'static {}
64
65impl<T> Future for Completion<T>
66where
67    T: Send + 'static,
68{
69    type Output = Result<T, Error>;
70
71    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
72        match &mut *self {
73            Self::Receiver(receiver) => Pin::new(receiver)
74                .poll(cx)
75                .map(|result| result.unwrap_or(Err(Error::Closed))),
76            Self::Future(future) => future.as_mut().poll(cx),
77        }
78    }
79}
80
81impl<T> Handle<T>
82where
83    T: Send + 'static,
84{
85    #[inline(always)]
86    pub(crate) fn init<F>(
87        f: F,
88        metric: MetricHandle,
89        panicker: Panicker,
90        tree: Arc<Tree>,
91    ) -> (impl Future<Output = ()>, Self)
92    where
93        F: Future<Output = T> + Send + 'static,
94    {
95        // Initialize channels to handle result/abort
96        let (sender, receiver) = oneshot::channel();
97        let (abort_handle, abort_registration) = AbortHandle::new_pair();
98
99        // Wrap the future with panic catching, abort support, and cleanup.
100        //
101        // Everything is done in a single async block (and the function is marked
102        // #[inline(always)]) so that stack usage is `size_of(F) + constant` rather than
103        // `N * size_of(F)` (which is what a combinator chain produces in debug builds).
104        let metric_handle = metric.clone();
105        let task = async move {
106            // Run future with panic catching and abort support
107            let result =
108                Abortable::new(AssertUnwindSafe(f).catch_unwind(), abort_registration).await;
109
110            // Handle result
111            match result {
112                Ok(Ok(result)) => {
113                    let _ = sender.send(Ok(result));
114                }
115                Ok(Err(panic)) => {
116                    panicker.notify(panic);
117                    let _ = sender.send(Err(Error::Exited));
118                }
119                Err(Aborted) => {}
120            }
121
122            // Mark the task as aborted and abort all descendants.
123            tree.abort();
124
125            // Finish the metric.
126            metric_handle.finish();
127        };
128
129        (
130            task,
131            Self {
132                state: HandleState::Task {
133                    receiver,
134                    abort_handle,
135                    metric,
136                },
137            },
138        )
139    }
140
141    /// Returns a handle backed by a completion receiver.
142    pub fn from_receiver(receiver: oneshot::Receiver<Result<T, Error>>) -> Self {
143        let (abort_handle, abort_registration) = AbortHandle::new_pair();
144        Self {
145            state: HandleState::Completion {
146                future: Abortable::new(Completion::Receiver(receiver), abort_registration),
147                abort_handle,
148            },
149        }
150    }
151
152    /// Returns a handle backed by a completion future.
153    pub fn from_future<F>(future: F) -> Self
154    where
155        F: Future<Output = Result<T, Error>> + Send + 'static,
156    {
157        let (abort_handle, abort_registration) = AbortHandle::new_pair();
158        Self {
159            state: HandleState::Completion {
160                future: Abortable::new(Completion::Future(Box::pin(future)), abort_registration),
161                abort_handle,
162            },
163        }
164    }
165
166    /// Returns a handle that is already complete.
167    pub fn ready(result: Result<T, Error>) -> Self {
168        let (sender, receiver) = oneshot::channel();
169        let _ = sender.send(result);
170        Self::from_receiver(receiver)
171    }
172
173    /// Returns a handle that resolves to [`Error::Closed`] without spawning work.
174    pub(crate) fn closed(metric: MetricHandle) -> Self {
175        // Mark the task as finished immediately so gauges remain accurate.
176        metric.finish();
177
178        // Create a receiver that will yield `Err(Error::Closed)` when awaited.
179        let (sender, receiver) = oneshot::channel();
180        drop(sender);
181
182        Self::from_receiver(receiver)
183    }
184
185    /// Abort the spawned task or stop waiting for a completion.
186    pub fn abort(&self) {
187        match &self.state {
188            HandleState::Task {
189                abort_handle,
190                metric,
191                ..
192            } => {
193                abort_handle.abort();
194
195                // We might never poll the future again after aborting it, so run the
196                // metric cleanup right away.
197                metric.finish();
198            }
199            HandleState::Completion { abort_handle, .. } => {
200                abort_handle.abort();
201            }
202        }
203    }
204
205    /// Returns a helper that aborts the task and updates metrics consistently.
206    pub(crate) fn aborter(&self) -> Option<Aborter> {
207        match &self.state {
208            HandleState::Task {
209                abort_handle,
210                metric,
211                ..
212            } => Some(Aborter::new(abort_handle.clone(), metric.clone())),
213            HandleState::Completion { .. } => None,
214        }
215    }
216}
217
218impl<T> Future for Handle<T>
219where
220    T: Send + 'static,
221{
222    type Output = Result<T, Error>;
223
224    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
225        match &mut self.state {
226            HandleState::Task { receiver, .. } => Pin::new(receiver)
227                .poll(cx)
228                .map(|result| result.unwrap_or_else(|_| Err(Error::Closed))),
229            HandleState::Completion { future, .. } => Pin::new(future)
230                .poll(cx)
231                .map(|result| result.unwrap_or(Err(Error::Aborted))),
232        }
233    }
234}
235
236/// Tracks the metric state associated with a spawned task handle.
237#[derive(Clone)]
238pub(crate) struct MetricHandle {
239    gauge: Gauge,
240    finished: Arc<Once>,
241}
242
243impl MetricHandle {
244    /// Increments the supplied gauge and returns a handle responsible for
245    /// eventually decrementing it.
246    pub(crate) fn new(gauge: Gauge) -> Self {
247        gauge.inc();
248
249        Self {
250            gauge,
251            finished: Arc::new(Once::new()),
252        }
253    }
254
255    /// Marks the task handle as completed and decrements the gauge once.
256    ///
257    /// This method is idempotent, additional calls are ignored so completion
258    /// and abort paths can invoke it independently.
259    pub(crate) fn finish(&self) {
260        let gauge = self.gauge.clone();
261        self.finished.call_once(move || {
262            gauge.dec();
263        });
264    }
265}
266
267/// A panic emitted by a spawned task.
268pub type Panic = Box<dyn Any + Send + 'static>;
269
270/// Notifies the runtime when a spawned task panics, so it can propagate the failure.
271#[derive(Clone)]
272pub(crate) struct Panicker {
273    catch: bool,
274    sender: Arc<Mutex<Option<oneshot::Sender<Panic>>>>,
275}
276
277impl Panicker {
278    /// Creates a new [Panicker].
279    pub(crate) fn new(catch: bool) -> (Self, Panicked) {
280        let (sender, receiver) = oneshot::channel();
281        let panicker = Self {
282            catch,
283            sender: Arc::new(Mutex::new(Some(sender))),
284        };
285        let panicked = Panicked { receiver };
286        (panicker, panicked)
287    }
288
289    /// Returns whether the [Panicker] is configured to catch panics.
290    #[commonware_macros::stability(ALPHA)]
291    pub(crate) const fn catch(&self) -> bool {
292        self.catch
293    }
294
295    /// Notifies the [Panicker] that a panic has occurred.
296    pub(crate) fn notify(&self, panic: Box<dyn Any + Send + 'static>) {
297        // Log the panic
298        let err = extract_panic_message(&*panic);
299        error!(?err, "task panicked");
300
301        // If we are catching panics, just return
302        if self.catch {
303            return;
304        }
305
306        // If we've already sent a panic, ignore the new one
307        let mut sender = self.sender.lock();
308        let Some(sender) = sender.take() else {
309            return;
310        };
311
312        // Send the panic
313        let _ = sender.send(panic);
314    }
315}
316
317/// A handle that will be notified when a panic occurs.
318pub(crate) struct Panicked {
319    receiver: oneshot::Receiver<Panic>,
320}
321
322impl Panicked {
323    /// Polls a task that should be interrupted by a panic.
324    pub(crate) async fn interrupt<Fut>(self, task: Fut) -> Fut::Output
325    where
326        Fut: Future,
327    {
328        // Wait for task to complete or panic
329        let panicked = self.receiver;
330        pin_mut!(panicked);
331        pin_mut!(task);
332        match select(panicked, task).await {
333            Either::Left((panic, task)) => match panic {
334                // If there is a panic, resume the unwind
335                Ok(panic) => {
336                    resume_unwind(panic);
337                }
338                // If there can never be a panic (oneshot is closed), wait for the task to complete
339                // and return the output
340                Err(_) => task.await,
341            },
342            Either::Right((output, _)) => {
343                // Return the output
344                output
345            }
346        }
347    }
348}
349
350/// Couples an [`AbortHandle`] with its metric handle so aborted tasks clean up gauges.
351pub(crate) struct Aborter {
352    inner: AbortHandle,
353    metric: MetricHandle,
354}
355
356impl Aborter {
357    /// Creates a new [`Aborter`] for the provided abort handle and metric handle.
358    pub(crate) const fn new(inner: AbortHandle, metric: MetricHandle) -> Self {
359        Self { inner, metric }
360    }
361
362    /// Aborts the task and records completion in the metric gauge.
363    pub(crate) fn abort(self) {
364        self.inner.abort();
365
366        // We might never poll the future again after aborting it, so run the
367        // metric cleanup right away
368        self.metric.finish();
369    }
370}
371
372#[cfg(test)]
373mod tests {
374    use super::Handle;
375    use crate::{deterministic, Error, Metrics as _, Runner, Spawner, Supervisor as _};
376    use commonware_utils::channel::oneshot;
377    use futures::future;
378
379    const METRIC_PREFIX: &str = "runtime_tasks_running{";
380
381    fn running_tasks_for_label(metrics: &str, label: &str) -> Option<u64> {
382        let label_fragment = format!("name=\"{label}\"");
383        metrics.lines().find_map(|line| {
384            if line.starts_with(METRIC_PREFIX) && line.contains(&label_fragment) {
385                line.rsplit_once(' ')
386                    .and_then(|(_, value)| value.trim().parse::<u64>().ok())
387            } else {
388                None
389            }
390        })
391    }
392
393    #[test]
394    fn tasks_running_decreased_after_completion() {
395        const LABEL: &str = "tasks_running_after_completion";
396
397        let runner = deterministic::Runner::default();
398        runner.start(|context| async move {
399            let handle = context.child(LABEL).spawn(|_| async move { "done" });
400
401            let metrics = context.encode();
402            assert_eq!(
403                running_tasks_for_label(&metrics, LABEL),
404                Some(1),
405                "expected tasks_running gauge to be 1 before completion: {metrics}",
406            );
407
408            let output = handle.await.expect("task failed");
409            assert_eq!(output, "done");
410
411            let metrics = context.encode();
412            assert_eq!(
413                running_tasks_for_label(&metrics, LABEL),
414                Some(0),
415                "expected tasks_running gauge to return to 0 after completion: {metrics}",
416            );
417        });
418    }
419
420    #[test]
421    fn tasks_running_unchanged_when_handle_dropped() {
422        const LABEL: &str = "tasks_running_unchanged";
423
424        let runner = deterministic::Runner::default();
425        runner.start(|context| async move {
426            let handle = context.child(LABEL).spawn(|_| async move {
427                future::pending::<()>().await;
428            });
429
430            let metrics = context.encode();
431            assert_eq!(
432                running_tasks_for_label(&metrics, LABEL),
433                Some(1),
434                "expected tasks_running gauge to be 1 before dropping handle: {metrics}",
435            );
436
437            drop(handle);
438
439            let metrics = context.encode();
440            assert_eq!(
441                running_tasks_for_label(&metrics, LABEL),
442                Some(1),
443                "dropping handle should not finish metrics: {metrics}",
444            );
445        });
446    }
447
448    #[test]
449    fn tasks_running_decreased_immediately_on_abort_via_handle() {
450        const LABEL: &str = "tasks_running_abort_via_handle";
451
452        let runner = deterministic::Runner::default();
453        runner.start(|context| async move {
454            let handle = context.child(LABEL).spawn(|_| async move {
455                future::pending::<()>().await;
456            });
457
458            let metrics = context.encode();
459            assert_eq!(
460                running_tasks_for_label(&metrics, LABEL),
461                Some(1),
462                "expected tasks_running gauge to be 1 before abort: {metrics}",
463            );
464
465            handle.abort();
466
467            let metrics = context.encode();
468            assert_eq!(
469                running_tasks_for_label(&metrics, LABEL),
470                Some(0),
471                "expected tasks_running gauge to return to 0 after abort: {metrics}",
472            );
473        });
474    }
475
476    #[test]
477    fn completion_handle_abort_stops_waiting() {
478        deterministic::Runner::default().start(|_| async move {
479            let (sender, receiver) = oneshot::channel();
480            let handle = Handle::from_receiver(receiver);
481
482            handle.abort();
483
484            assert!(sender.send(Ok(())).is_ok());
485            assert!(matches!(handle.await, Err(Error::Aborted)));
486        });
487    }
488
489    #[test]
490    fn tasks_running_decreased_after_blocking_completion() {
491        const LABEL: &str = "tasks_running_after_blocking_completion";
492
493        let runner = deterministic::Runner::default();
494        runner.start(|context| async move {
495            let blocking_handle = context.child(LABEL).shared(true).spawn(|_| async move {
496                // Simulate some blocking work
497                42
498            });
499
500            let metrics = context.encode();
501            assert_eq!(
502                running_tasks_for_label(&metrics, LABEL),
503                Some(1),
504                "expected tasks_running gauge to be 1 while blocking task runs: {metrics}",
505            );
506
507            let result = blocking_handle.await.expect("blocking task failed");
508            assert_eq!(result, 42);
509
510            let metrics = context.encode();
511            assert_eq!(
512                running_tasks_for_label(&metrics, LABEL),
513                Some(0),
514                "expected tasks_running gauge to return to 0 after blocking task completes: {metrics}",
515            );
516        });
517    }
518
519    #[test]
520    fn tasks_running_decreased_immediately_on_abort_via_aborter() {
521        const LABEL: &str = "tasks_running_abort_via_aborter";
522
523        let runner = deterministic::Runner::default();
524        runner.start(|context| async move {
525            let handle = context.child(LABEL).spawn(|_| async move {
526                future::pending::<()>().await;
527            });
528
529            let metrics = context.encode();
530            assert_eq!(
531                running_tasks_for_label(&metrics, LABEL),
532                Some(1),
533                "expected tasks_running gauge to be 1 before abort: {metrics}",
534            );
535
536            let aborter = handle.aborter().unwrap();
537            aborter.abort();
538
539            let metrics = context.encode();
540            assert_eq!(
541                running_tasks_for_label(&metrics, LABEL),
542                Some(0),
543                "expected tasks_running gauge to return to 0 after abort: {metrics}",
544            );
545        });
546    }
547}