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::{Exclusive, Mutex, Once},
9};
10use futures::{
11    future::{Either, poll_fn, select},
12    pin_mut,
13    stream::{AbortHandle, Abortable, Aborted},
14};
15use std::{
16    any::Any,
17    cell::UnsafeCell,
18    future::Future,
19    panic::{AssertUnwindSafe, catch_unwind, resume_unwind},
20    pin::Pin,
21    sync::Arc,
22    task::{Context, Poll},
23    vec::IntoIter,
24};
25use tracing::error;
26
27/// Handle to an asynchronous result.
28///
29/// Handles returned by [`crate::Spawner::spawn`] abort the spawned task. Completion handles only
30/// stop waiting when aborted, resolving to [`Error::Aborted`]; they do not cancel the underlying
31/// work.
32pub struct Handle<T>
33where
34    T: Send + 'static,
35{
36    state: HandleState<T>,
37}
38
39/// A boxed future that completes a handle.
40type BoxedCompletion<T> = Pin<Box<dyn Future<Output = Result<T, Error>> + Send + 'static>>;
41
42/// Distinguishes handles that own spawned work from handles that only wait on completion.
43enum HandleState<T>
44where
45    T: Send + 'static,
46{
47    Task {
48        receiver: oneshot::Receiver<Result<T, Error>>,
49        abort_handle: AbortHandle,
50        metric: MetricHandle,
51    },
52    Completion {
53        future: Abortable<Completion<T>>,
54        abort_handle: AbortHandle,
55    },
56}
57
58/// Signals all aborts before dropping any handle, whose result or completion future may depend
59/// on another task in the group during destruction.
60struct HandleGroup<T>(IntoIter<AbortOnDrop<T>>)
61where
62    T: Send + 'static;
63
64impl<T: Send + 'static> HandleGroup<T> {
65    fn abort(&self) {
66        for guard in self.0.as_slice() {
67            guard.0.abort();
68        }
69    }
70}
71
72impl<T> Drop for HandleGroup<T>
73where
74    T: Send + 'static,
75{
76    fn drop(&mut self) {
77        self.abort();
78    }
79}
80
81/// Closes supervision and finishes metrics when a task exits or is discarded.
82struct TaskGuard {
83    /// Supervision subtree owned by the task.
84    tree: Arc<Tree>,
85    /// Running-task metric, finished once even if cancellation already updated it.
86    metric: MetricHandle,
87}
88
89impl Drop for TaskGuard {
90    fn drop(&mut self) {
91        self.tree.abort();
92        self.metric.finish();
93    }
94}
95
96/// Closes supervision and finishes metrics if task construction unwinds.
97///
98/// A spawn holds this guard while running the user's task factory, then
99/// disarms it by transferring the metric to the execution wrapper.
100pub(crate) struct FactoryGuard<'a> {
101    /// Supervision node closed if construction fails.
102    tree: &'a Arc<Tree>,
103    /// Metric transferred to the execution wrapper after construction succeeds.
104    metric: Option<MetricHandle>,
105}
106
107impl<'a> FactoryGuard<'a> {
108    /// Arm the guard around a task factory call.
109    pub(crate) const fn new(tree: &'a Arc<Tree>, metric: MetricHandle) -> Self {
110        Self {
111            tree,
112            metric: Some(metric),
113        }
114    }
115
116    /// Hand the metric to the execution wrapper once construction has succeeded.
117    pub(crate) fn disarm(mut self) -> MetricHandle {
118        self.metric.take().expect("factory guard disarmed twice")
119    }
120}
121
122impl Drop for FactoryGuard<'_> {
123    fn drop(&mut self) {
124        if let Some(metric) = &self.metric {
125            metric.finish();
126            self.tree.abort();
127        }
128    }
129}
130
131/// Normalizes receiver-backed and future-backed completions behind one abortable future.
132enum Completion<T>
133where
134    T: Send + 'static,
135{
136    Receiver(oneshot::Receiver<Result<T, Error>>),
137    /// The future is polled only through exclusive access, so the handle stays `Sync`.
138    Future(Exclusive<BoxedCompletion<T>>),
139}
140
141impl<T> Unpin for Completion<T> where T: Send + 'static {}
142
143impl<T> Future for Completion<T>
144where
145    T: Send + 'static,
146{
147    type Output = Result<T, Error>;
148
149    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
150        match &mut *self {
151            Self::Receiver(receiver) => Pin::new(receiver)
152                .poll(cx)
153                .map(|result| result.unwrap_or(Err(Error::Closed))),
154            Self::Future(future) => future.get_mut().as_mut().poll(cx),
155        }
156    }
157}
158
159/// Polls a task's future, catching any panic it raises.
160///
161/// The future lives in an [`UnsafeCell`], which has the same layout as `F`, so the shared
162/// reference [`Abortable`] takes to check for cancellation does not invalidate borrows the future
163/// holds into its own state. Catching panics here spares a task separate wrappers for that, each
164/// of which would add a stack slot the size of the future to every poll in unoptimized builds.
165// Miri treats creating a shared reference as a read of everything outside an `UnsafeCell`, which
166// conflicts with a suspended future's borrows of itself (rust-lang/rust#137750) until coroutines
167// are built on `UnsafePinned` (rust-lang/rust#125735).
168struct FutureCell<F>(UnsafeCell<F>);
169
170impl<F: Future> Future for FutureCell<F> {
171    type Output = std::thread::Result<F::Output>;
172
173    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
174        // SAFETY: The future is structurally pinned: `FutureCell` has no `Drop` impl, is `Unpin`
175        // only when `F` is, and reaches the future only through this pinned projection.
176        let future = unsafe { self.map_unchecked_mut(|cell| cell.0.get_mut()) };
177        catch_unwind(AssertUnwindSafe(|| future.poll(cx)))?.map(Ok)
178    }
179}
180
181impl<T> Handle<T>
182where
183    T: Send + 'static,
184{
185    /// Wraps `f` as a task supervised by `tree`.
186    ///
187    /// The task's cancellation is registered on `tree` before this returns, so a closed `tree`
188    /// cancels the task before `f` can be polled.
189    #[inline(always)]
190    pub(crate) fn init<F>(
191        f: F,
192        metric: MetricHandle,
193        panicker: Panicker,
194        tree: Arc<Tree>,
195    ) -> (impl Future<Output = ()>, Self)
196    where
197        F: Future<Output = T> + Send + 'static,
198    {
199        // Initialize channels to handle result/abort
200        let (sender, receiver) = oneshot::channel();
201        let (abort_handle, abort_registration) = AbortHandle::new_pair();
202
203        // Attach cancellation before the caller can publish the task.
204        tree.register(Aborter::new(abort_handle.clone(), metric.clone()));
205
206        // Install cleanup before the first poll so rejected tasks also close
207        // supervision and finish their metrics when the future is dropped.
208        let guard = TaskGuard {
209            tree,
210            metric: metric.clone(),
211        };
212
213        // Wrap the future with panic catching, abort support, and cleanup.
214        //
215        // Everything is done in a single async block (and the function is marked
216        // #[inline(always)]) so that stack usage is `size_of(F) + constant` rather than
217        // `N * size_of(F)` (which is what a combinator chain produces in debug builds).
218        let task = async move {
219            // Cancellation can destroy the user future during this poll. Close
220            // its supervision subtree even if that destruction unwinds.
221            let _guard = guard;
222
223            // Run future with panic catching and abort support
224            let result = Abortable::new(FutureCell(UnsafeCell::new(f)), abort_registration).await;
225
226            // Handle result
227            match result {
228                Ok(Ok(result)) => {
229                    let _ = sender.send(Ok(result));
230                }
231                Ok(Err(panic)) => {
232                    panicker.notify(panic);
233                    let _ = sender.send(Err(Error::Exited));
234                }
235                Err(Aborted) => {}
236            }
237        };
238
239        (
240            task,
241            Self {
242                state: HandleState::Task {
243                    receiver,
244                    abort_handle,
245                    metric,
246                },
247            },
248        )
249    }
250
251    /// Returns a handle backed by a completion receiver.
252    pub fn from_receiver(receiver: oneshot::Receiver<Result<T, Error>>) -> Self {
253        let (abort_handle, abort_registration) = AbortHandle::new_pair();
254        Self {
255            state: HandleState::Completion {
256                future: Abortable::new(Completion::Receiver(receiver), abort_registration),
257                abort_handle,
258            },
259        }
260    }
261
262    /// Returns a handle backed by a completion future.
263    pub fn from_future<F>(future: F) -> Self
264    where
265        F: Future<Output = Result<T, Error>> + Send + 'static,
266    {
267        let (abort_handle, abort_registration) = AbortHandle::new_pair();
268        Self {
269            state: HandleState::Completion {
270                future: Abortable::new(
271                    Completion::Future(Exclusive::new(Box::pin(future))),
272                    abort_registration,
273                ),
274                abort_handle,
275            },
276        }
277    }
278
279    /// Returns a handle that is already complete.
280    pub fn ready(result: Result<T, Error>) -> Self {
281        let (sender, receiver) = oneshot::channel();
282        let _ = sender.send(result);
283        Self::from_receiver(receiver)
284    }
285
286    /// Waits for the first handle to complete and aborts all handles before returning.
287    ///
288    /// Dropping the returned future also aborts every handle. Selection is biased toward handles
289    /// that appear earlier in the iterator.
290    ///
291    /// Runtime supervision already aborts a task's descendants when that task exits. This method
292    /// is intended for an owned group whose teardown boundary is the first handle completion,
293    /// independent of whether the caller exits immediately afterward.
294    ///
295    /// # Examples
296    ///
297    /// ```
298    /// # futures::executor::block_on(async {
299    /// use commonware_runtime::Handle;
300    ///
301    /// let handles = [
302    ///     Handle::ready(Ok(7)),
303    ///     Handle::from_future(futures::future::pending()),
304    /// ];
305    /// assert_eq!(Handle::select(handles).await.unwrap(), 7);
306    /// # });
307    /// ```
308    ///
309    /// # Errors
310    ///
311    /// Returns [`Error::Closed`] if `handles` is empty.
312    pub fn select(
313        handles: impl IntoIterator<Item = Self>,
314    ) -> impl Future<Output = Result<T, Error>> + Send + 'static {
315        // Construct the guard before returning so dropping the future without polling still aborts
316        // every handle.
317        let mut handles = HandleGroup(
318            handles
319                .into_iter()
320                .map(Self::abort_on_drop)
321                .collect::<Vec<_>>()
322                .into_iter(),
323        );
324
325        async move {
326            if handles.0.as_slice().is_empty() {
327                return Err(Error::Closed);
328            }
329
330            poll_fn(move |cx| {
331                for guard in handles.0.as_mut_slice() {
332                    if let Poll::Ready(result) = Pin::new(&mut guard.0).poll(cx) {
333                        return Poll::Ready(result);
334                    }
335                }
336                Poll::Pending
337            })
338            .await
339        }
340    }
341
342    /// Returns a handle that resolves to [`Error::Closed`] without spawning work.
343    pub(crate) fn closed(metric: MetricHandle) -> Self {
344        // Mark the task as finished immediately so gauges remain accurate.
345        metric.finish();
346
347        // Create a receiver that will yield `Err(Error::Closed)` when awaited.
348        let (sender, receiver) = oneshot::channel();
349        drop(sender);
350
351        Self::from_receiver(receiver)
352    }
353
354    /// Abort the spawned task or stop waiting for a completion.
355    pub fn abort(&self) {
356        match &self.state {
357            HandleState::Task {
358                abort_handle,
359                metric,
360                ..
361            } => {
362                abort_handle.abort();
363
364                // We might never poll the future again after aborting it, so run the
365                // metric cleanup right away.
366                metric.finish();
367            }
368            HandleState::Completion { abort_handle, .. } => {
369                abort_handle.abort();
370            }
371        }
372    }
373
374    /// Returns a helper that aborts the task and updates metrics consistently.
375    #[cfg(test)]
376    pub(crate) fn aborter(&self) -> Option<Aborter> {
377        match &self.state {
378            HandleState::Task {
379                abort_handle,
380                metric,
381                ..
382            } => Some(Aborter::new(abort_handle.clone(), metric.clone())),
383            HandleState::Completion { .. } => None,
384        }
385    }
386
387    /// Wrap this handle so dropping the wrapper calls [`Handle::abort`].
388    pub const fn abort_on_drop(self) -> AbortOnDrop<T> {
389        AbortOnDrop(self)
390    }
391}
392
393/// Calls [`Handle::abort`] when dropped.
394///
395/// Supervision ties spawned tasks to their spawning task's lifetime, but work spawned from a plain
396/// future outlives that future's drop. Wrapping the handle (see [`Handle::abort_on_drop`]) ties the
397/// task's cancellation to the guard instead. Dropping the guard signals cancellation. Use
398/// [`AbortOnDrop::abort`] to also join the handle. Completion handles only stop waiting and
399/// do not cancel the underlying work (see [`Handle`]).
400pub struct AbortOnDrop<T: Send + 'static>(Handle<T>);
401
402impl<T: Send + 'static> AbortOnDrop<T> {
403    /// Abort and join the handle. Completion handles only stop waiting for the underlying work.
404    pub async fn abort(mut self) {
405        self.0.abort();
406        let _ = (&mut self.0).await;
407    }
408
409    /// Join the handle.
410    pub async fn join(mut self) -> Result<T, Error> {
411        // Poll the handle by reference: the guard must keep owning it so dropping this future
412        // mid-join still aborts the task.
413        (&mut self.0).await
414    }
415}
416
417impl<T, E1> AbortOnDrop<Result<T, E1>>
418where
419    T: Send + 'static,
420    E1: Send + 'static,
421{
422    /// Join `guards` in order, collecting each task's output.
423    ///
424    /// On the first failure (a task error or a failed join), abort every remaining guard, discard
425    /// collected outputs, and drain the remaining guards concurrently before surfacing the error.
426    /// Joining in order makes the surfaced failure deterministic.
427    ///
428    /// Spawned tasks are cancelled and joined. Completion handles only stop waiting for their
429    /// underlying work.
430    ///
431    /// Dropping the returned future signals every cancellation before destroying any owned
432    /// outputs or completion futures.
433    pub fn join_all<E2>(guards: Vec<Self>) -> impl Future<Output = Result<Vec<T>, E2>>
434    where
435        E2: From<E1> + From<Error>,
436    {
437        let guards = HandleGroup(guards.into_iter());
438        async move {
439            // Cancellation must drop the group before destroying collected outputs.
440            let mut joined = Vec::with_capacity(guards.0.len());
441            let mut guards = guards;
442            while let Some(guard) = guards.0.as_mut_slice().first_mut() {
443                // Error conversion may destroy resources, so signal all cancellations first.
444                let result = (&mut guard.0).await;
445                if !matches!(result, Ok(Ok(_))) {
446                    guards.abort();
447                }
448                match result
449                    .map_err(E2::from)
450                    .and_then(|result| result.map_err(E2::from))
451                {
452                    Ok(output) => joined.push(output),
453                    Err(error) => {
454                        // Release collected outputs before waiting on dependent tasks.
455                        drop(joined);
456                        drop(guards.0.next());
457                        futures::future::join_all(guards.0.by_ref().map(Self::abort)).await;
458                        return Err(error);
459                    }
460                }
461                drop(guards.0.next());
462            }
463            Ok(joined)
464        }
465    }
466}
467
468impl<T: Send + 'static> Drop for AbortOnDrop<T> {
469    fn drop(&mut self) {
470        self.0.abort();
471    }
472}
473
474impl<T> Future for Handle<T>
475where
476    T: Send + 'static,
477{
478    type Output = Result<T, Error>;
479
480    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
481        match &mut self.state {
482            HandleState::Task { receiver, .. } => Pin::new(receiver)
483                .poll(cx)
484                .map(|result| result.unwrap_or_else(|_| Err(Error::Closed))),
485            HandleState::Completion { future, .. } => Pin::new(future)
486                .poll(cx)
487                .map(|result| result.unwrap_or(Err(Error::Aborted))),
488        }
489    }
490}
491
492/// Tracks the metric state associated with a spawned task handle.
493#[derive(Clone)]
494pub(crate) struct MetricHandle {
495    gauge: Gauge,
496    finished: Arc<Once>,
497}
498
499impl MetricHandle {
500    /// Increments the supplied gauge and returns a handle responsible for
501    /// eventually decrementing it.
502    pub(crate) fn new(gauge: Gauge) -> Self {
503        gauge.inc();
504
505        Self {
506            gauge,
507            finished: Arc::new(Once::new()),
508        }
509    }
510
511    /// Marks the task handle as completed and decrements the gauge once.
512    ///
513    /// This method is idempotent, additional calls are ignored so completion
514    /// and abort paths can invoke it independently.
515    pub(crate) fn finish(&self) {
516        let gauge = self.gauge.clone();
517        self.finished.call_once(move || {
518            gauge.dec();
519        });
520    }
521}
522
523/// A panic emitted by a spawned task.
524pub type Panic = Box<dyn Any + Send + 'static>;
525
526/// Notifies the runtime when a spawned task panics, so it can propagate the failure.
527#[derive(Clone)]
528pub(crate) struct Panicker {
529    catch: bool,
530    sender: Arc<Mutex<Option<oneshot::Sender<Panic>>>>,
531}
532
533impl Panicker {
534    /// Creates a new [Panicker].
535    pub(crate) fn new(catch: bool) -> (Self, Panicked) {
536        let (sender, receiver) = oneshot::channel();
537        let panicker = Self {
538            catch,
539            sender: Arc::new(Mutex::new(Some(sender))),
540        };
541        let panicked = Panicked { receiver };
542        (panicker, panicked)
543    }
544
545    /// Returns whether the [Panicker] is configured to catch panics.
546    #[commonware_macros::stability(ALPHA)]
547    pub(crate) const fn catch(&self) -> bool {
548        self.catch
549    }
550
551    /// Notifies the [Panicker] that a panic has occurred.
552    pub(crate) fn notify(&self, panic: Box<dyn Any + Send + 'static>) {
553        // Log the panic
554        let err = extract_panic_message(&*panic);
555        error!(?err, "task panicked");
556
557        // If we are catching panics, just return
558        if self.catch {
559            return;
560        }
561
562        // If we've already sent a panic, ignore the new one
563        let mut sender = self.sender.lock();
564        let Some(sender) = sender.take() else {
565            return;
566        };
567
568        // Send the panic
569        let _ = sender.send(panic);
570    }
571}
572
573/// A handle that will be notified when a panic occurs.
574pub(crate) struct Panicked {
575    receiver: oneshot::Receiver<Panic>,
576}
577
578impl Panicked {
579    /// Polls a task that should be interrupted by a panic.
580    pub(crate) async fn interrupt<Fut>(self, task: Fut) -> Fut::Output
581    where
582        Fut: Future,
583    {
584        // Wait for task to complete or panic
585        let panicked = self.receiver;
586        pin_mut!(panicked);
587        pin_mut!(task);
588        match select(panicked, task).await {
589            Either::Left((panic, task)) => match panic {
590                // If there is a panic, resume the unwind
591                Ok(panic) => {
592                    resume_unwind(panic);
593                }
594                // If there can never be a panic (oneshot is closed), wait for the task to complete
595                // and return the output
596                Err(_) => task.await,
597            },
598            Either::Right((output, mut panicked)) => {
599                // A panic sent while the task completed wins over its output
600                if let Ok(panic) = panicked.try_recv() {
601                    resume_unwind(panic);
602                }
603                output
604            }
605        }
606    }
607
608    /// Take a panic sent and not yet observed.
609    #[cfg(target_os = "linux")]
610    #[commonware_macros::stability(ALPHA)]
611    pub(crate) fn try_take(&mut self) -> Option<Panic> {
612        self.receiver.try_recv().ok()
613    }
614}
615
616/// Couples an [`AbortHandle`] with its metric handle so aborted tasks clean up gauges.
617pub(crate) struct Aborter {
618    inner: AbortHandle,
619    metric: MetricHandle,
620}
621
622impl Aborter {
623    /// Creates a new [`Aborter`] for the provided abort handle and metric handle.
624    pub(crate) const fn new(inner: AbortHandle, metric: MetricHandle) -> Self {
625        Self { inner, metric }
626    }
627
628    /// Aborts the task and records completion in the metric gauge.
629    pub(crate) fn abort(self) {
630        self.inner.abort();
631
632        // We might never poll the future again after aborting it, so run the
633        // metric cleanup right away
634        self.metric.finish();
635    }
636}
637
638#[cfg(test)]
639mod tests {
640    use super::{AbortOnDrop, Handle, MetricHandle, Panicker};
641    use crate::{
642        Error, Metrics as _, Runner, Spawner, Supervisor as _, deterministic,
643        telemetry::metrics::raw::Gauge,
644        utils::{extract_panic_message, reschedule, supervision::Tree},
645    };
646    use commonware_utils::{channel::oneshot, sync::Mutex};
647    use futures::{FutureExt as _, future, poll, stream::AbortHandle};
648    use rstest::rstest;
649    use std::{
650        panic::{AssertUnwindSafe, catch_unwind},
651        sync::{
652            Arc,
653            atomic::{AtomicBool, Ordering},
654        },
655    };
656
657    const METRIC_PREFIX: &str = "runtime_tasks_running{";
658
659    fn running_tasks_for_label(metrics: &str, label: &str) -> Option<u64> {
660        let label_fragment = format!("name=\"{label}\"");
661        metrics.lines().find_map(|line| {
662            if line.starts_with(METRIC_PREFIX) && line.contains(&label_fragment) {
663                line.rsplit_once(' ')
664                    .and_then(|(_, value)| value.trim().parse::<u64>().ok())
665            } else {
666                None
667            }
668        })
669    }
670
671    #[test]
672    fn tasks_running_decreased_after_completion() {
673        const LABEL: &str = "tasks_running_after_completion";
674
675        let runner = deterministic::Runner::default();
676        runner.start(|context| async move {
677            let handle = context.child(LABEL).spawn(|_| async move { "done" });
678
679            let metrics = context.encode();
680            assert_eq!(
681                running_tasks_for_label(&metrics, LABEL),
682                Some(1),
683                "expected tasks_running gauge to be 1 before completion: {metrics}",
684            );
685
686            let output = handle.await.expect("task failed");
687            assert_eq!(output, "done");
688
689            let metrics = context.encode();
690            assert_eq!(
691                running_tasks_for_label(&metrics, LABEL),
692                Some(0),
693                "expected tasks_running gauge to return to 0 after completion: {metrics}",
694            );
695        });
696    }
697
698    #[test]
699    fn tasks_running_unchanged_when_handle_dropped() {
700        const LABEL: &str = "tasks_running_unchanged";
701
702        let runner = deterministic::Runner::default();
703        runner.start(|context| async move {
704            let handle = context.child(LABEL).spawn(|_| async move {
705                future::pending::<()>().await;
706            });
707
708            let metrics = context.encode();
709            assert_eq!(
710                running_tasks_for_label(&metrics, LABEL),
711                Some(1),
712                "expected tasks_running gauge to be 1 before dropping handle: {metrics}",
713            );
714
715            drop(handle);
716
717            let metrics = context.encode();
718            assert_eq!(
719                running_tasks_for_label(&metrics, LABEL),
720                Some(1),
721                "dropping handle should not finish metrics: {metrics}",
722            );
723        });
724    }
725
726    #[test]
727    fn tasks_running_decreased_immediately_on_abort_via_handle() {
728        const LABEL: &str = "tasks_running_abort_via_handle";
729
730        let runner = deterministic::Runner::default();
731        runner.start(|context| async move {
732            let handle = context.child(LABEL).spawn(|_| async move {
733                future::pending::<()>().await;
734            });
735
736            let metrics = context.encode();
737            assert_eq!(
738                running_tasks_for_label(&metrics, LABEL),
739                Some(1),
740                "expected tasks_running gauge to be 1 before abort: {metrics}",
741            );
742
743            handle.abort();
744
745            let metrics = context.encode();
746            assert_eq!(
747                running_tasks_for_label(&metrics, LABEL),
748                Some(0),
749                "expected tasks_running gauge to return to 0 after abort: {metrics}",
750            );
751        });
752    }
753
754    #[test]
755    fn select_aborts_remaining_tasks() {
756        const LABEL: &str = "tasks_running_select_remaining";
757
758        deterministic::Runner::default().start(|context| async move {
759            let completed = context.child("select_completed").spawn(|_| async {});
760            let pending = context.child(LABEL).spawn(|_| future::pending());
761
762            Handle::select([completed, pending])
763                .await
764                .expect("task failed");
765
766            let metrics = context.encode();
767            assert_eq!(
768                running_tasks_for_label(&metrics, LABEL),
769                Some(0),
770                "select should abort remaining tasks: {metrics}",
771            );
772        });
773    }
774
775    #[test]
776    fn select_empty_returns_closed() {
777        deterministic::Runner::default().start(|_| async move {
778            assert!(matches!(Handle::<()>::select([]).await, Err(Error::Closed)));
779        });
780    }
781
782    #[test]
783    fn dropping_select_aborts_tasks_before_polling() {
784        const FIRST_LABEL: &str = "tasks_running_select_drop_first";
785        const SECOND_LABEL: &str = "tasks_running_select_drop_second";
786
787        deterministic::Runner::default().start(|context| async move {
788            let first = context
789                .child(FIRST_LABEL)
790                .spawn(|_| future::pending::<()>());
791            let second = context
792                .child(SECOND_LABEL)
793                .spawn(|_| future::pending::<()>());
794
795            drop(Handle::select([first, second]));
796
797            let metrics = context.encode();
798            assert_eq!(running_tasks_for_label(&metrics, FIRST_LABEL), Some(0));
799            assert_eq!(running_tasks_for_label(&metrics, SECOND_LABEL), Some(0));
800        });
801    }
802
803    #[test]
804    fn completion_handle_abort_stops_waiting() {
805        deterministic::Runner::default().start(|_| async move {
806            let (sender, receiver) = oneshot::channel();
807            let handle = Handle::from_receiver(receiver);
808
809            handle.abort();
810
811            assert!(sender.send(Ok(())).is_ok());
812            assert!(matches!(handle.await, Err(Error::Aborted)));
813        });
814    }
815
816    #[test]
817    fn abort_on_drop_cancels_pending_join() {
818        for poll_join in [false, true] {
819            deterministic::Runner::default().start(|context| async move {
820                let (held, released) = oneshot::channel::<()>();
821                let guard = context
822                    .child("guarded")
823                    .spawn(move |_| async move {
824                        let _held = held;
825                        future::pending::<()>().await;
826                    })
827                    .abort_on_drop();
828                let mut join = Box::pin(guard.join());
829                if poll_join {
830                    assert!(poll!(&mut join).is_pending());
831                }
832                drop(join);
833                assert!(released.await.is_err());
834            });
835        }
836    }
837
838    #[test]
839    fn abort_on_drop_join_all_drains_pending_task() {
840        deterministic::Runner::default().start(|context| async move {
841            let (held, released) = oneshot::channel::<()>();
842            let pending = context
843                .child("pending")
844                .spawn(move |_| async move {
845                    let _held = held;
846                    future::pending::<Result<(), Error>>().await
847                })
848                .abort_on_drop();
849            let failed = Handle::ready(Ok(Err(Error::Closed))).abort_on_drop();
850            let result = AbortOnDrop::join_all::<Error>(vec![failed, pending]).await;
851            assert!(matches!(result, Err(Error::Closed)));
852            assert!(
853                released
854                    .now_or_never()
855                    .expect("task still owns sender")
856                    .is_err()
857            );
858        });
859    }
860
861    struct AbortObservation {
862        abort_handle: AbortHandle,
863        observed: Arc<Mutex<Option<bool>>>,
864    }
865
866    impl Drop for AbortObservation {
867        fn drop(&mut self) {
868            *self.observed.lock() = Some(self.abort_handle.is_aborted());
869        }
870    }
871
872    impl From<AbortObservation> for Error {
873        fn from(_error: AbortObservation) -> Self {
874            Self::Closed
875        }
876    }
877
878    #[derive(Debug)]
879    enum JoinCleanup {
880        Unpolled,
881        CollectedOutput,
882        RemainingOutput,
883        PendingCompletion,
884        ErrorConversion,
885    }
886
887    #[rstest]
888    #[case::unpolled(JoinCleanup::Unpolled)]
889    #[case::collected_output(JoinCleanup::CollectedOutput)]
890    #[case::remaining_output(JoinCleanup::RemainingOutput)]
891    #[case::pending_completion(JoinCleanup::PendingCompletion)]
892    #[case::error_conversion(JoinCleanup::ErrorConversion)]
893    fn join_all_aborts_before_destruction(#[case] cleanup: JoinCleanup) {
894        deterministic::Runner::default().start(|context| async move {
895            let pending = context
896                .child("pending")
897                .spawn(|_| future::pending::<Result<Option<AbortObservation>, AbortObservation>>());
898            let abort_handle = pending.aborter().unwrap().inner;
899            assert!(!abort_handle.is_aborted());
900            let observed = Arc::new(Mutex::new(None));
901            let resource = AbortObservation {
902                abort_handle,
903                observed: observed.clone(),
904            };
905            let first = match cleanup {
906                JoinCleanup::PendingCompletion => Handle::from_future(async move {
907                    let _resource = resource;
908                    future::pending().await
909                }),
910                JoinCleanup::ErrorConversion => Handle::ready(Ok(Err(resource))),
911                _ => Handle::ready(Ok(Ok(Some(resource)))),
912            };
913            let mut handles = vec![first, pending];
914            if matches!(cleanup, JoinCleanup::RemainingOutput) {
915                handles.insert(0, Handle::from_future(future::pending()));
916                handles.insert(0, Handle::ready(Ok(Ok(None))));
917            }
918            let mut joining = Box::pin(AbortOnDrop::join_all::<Error>(
919                handles.into_iter().map(Handle::abort_on_drop).collect(),
920            ));
921            if matches!(cleanup, JoinCleanup::ErrorConversion) {
922                assert!(matches!(joining.await, Err(Error::Closed)));
923            } else {
924                if !matches!(cleanup, JoinCleanup::Unpolled) {
925                    assert!(poll!(&mut joining).is_pending());
926                }
927                drop(joining);
928            }
929            assert_eq!(*observed.lock(), Some(true));
930        });
931    }
932
933    #[test]
934    fn abort_on_drop_completion_leaves_producer_running() {
935        deterministic::Runner::default().start(|context| async move {
936            let (release, wait) = oneshot::channel::<()>();
937            let (sender, receiver) = oneshot::channel();
938            let mut producer = context.child("producer").spawn(move |_| async move {
939                wait.await.unwrap();
940                let _ = sender.send(Ok(()));
941            });
942
943            Handle::from_receiver(receiver)
944                .abort_on_drop()
945                .abort()
946                .await;
947            assert!(poll!(&mut producer).is_pending());
948            release.send(()).unwrap();
949            producer.await.unwrap();
950        });
951    }
952
953    #[test]
954    fn tasks_running_decreased_after_blocking_completion() {
955        const LABEL: &str = "tasks_running_after_blocking_completion";
956
957        let runner = deterministic::Runner::default();
958        runner.start(|context| async move {
959            let blocking_handle = context.child(LABEL).shared(true).spawn(|_| async move {
960                // Simulate some blocking work
961                42
962            });
963
964            let metrics = context.encode();
965            assert_eq!(
966                running_tasks_for_label(&metrics, LABEL),
967                Some(1),
968                "expected tasks_running gauge to be 1 while blocking task runs: {metrics}",
969            );
970
971            let result = blocking_handle.await.expect("blocking task failed");
972            assert_eq!(result, 42);
973
974            let metrics = context.encode();
975            assert_eq!(
976                running_tasks_for_label(&metrics, LABEL),
977                Some(0),
978                "expected tasks_running gauge to return to 0 after blocking task completes: {metrics}",
979            );
980        });
981    }
982
983    #[test]
984    fn tasks_running_decreased_immediately_on_abort_via_aborter() {
985        const LABEL: &str = "tasks_running_abort_via_aborter";
986
987        let runner = deterministic::Runner::default();
988        runner.start(|context| async move {
989            let handle = context.child(LABEL).spawn(|_| async move {
990                future::pending::<()>().await;
991            });
992
993            let metrics = context.encode();
994            assert_eq!(
995                running_tasks_for_label(&metrics, LABEL),
996                Some(1),
997                "expected tasks_running gauge to be 1 before abort: {metrics}",
998            );
999
1000            let aborter = handle.aborter().unwrap();
1001            aborter.abort();
1002
1003            let metrics = context.encode();
1004            assert_eq!(
1005                running_tasks_for_label(&metrics, LABEL),
1006                Some(0),
1007                "expected tasks_running gauge to return to 0 after abort: {metrics}",
1008            );
1009        });
1010    }
1011
1012    /// A panic published during the task's completing poll wins over its output.
1013    #[test]
1014    fn interrupt_observes_panic_sent_while_completing() {
1015        let (panicker, panicked) = Panicker::new(false);
1016        let result = catch_unwind(AssertUnwindSafe(|| {
1017            panicked
1018                .interrupt(async move {
1019                    panicker.notify(Box::new("late"));
1020                    7
1021                })
1022                .now_or_never()
1023        }));
1024        let panic = result.expect_err("panic sent while completing was dropped");
1025        assert_eq!(extract_panic_message(&*panic), "late");
1026    }
1027
1028    /// A task whose supervisor closed before initialization is cancelled before its future can
1029    /// be polled.
1030    #[test]
1031    fn init_registers_before_publication() {
1032        // Close the supervisor, as when it aborts while the task factory runs.
1033        let tree = Tree::root();
1034        tree.abort();
1035
1036        // Initialization registers the task, which the closed supervisor cancels at once.
1037        let (panicker, _panicked) = Panicker::new(false);
1038        let gauge = Gauge::default();
1039        let polled = Arc::new(AtomicBool::new(false));
1040        let observed = polled.clone();
1041        let (task, handle) = Handle::init(
1042            async move { observed.store(true, Ordering::SeqCst) },
1043            MetricHandle::new(gauge.clone()),
1044            panicker,
1045            tree,
1046        );
1047        assert_eq!(gauge.get(), 0);
1048
1049        // The first poll completes without polling the user future.
1050        assert_eq!(task.now_or_never(), Some(()));
1051        assert!(!polled.load(Ordering::SeqCst));
1052        assert!(matches!(handle.now_or_never(), Some(Err(Error::Closed))));
1053    }
1054
1055    /// A task can write through a borrow of its own state after suspending (checked by Miri).
1056    #[commonware_macros::test_group("miri")]
1057    #[test]
1058    fn task_keeps_self_borrow_across_suspension() {
1059        deterministic::Runner::default().start(|context| async move {
1060            let handle = context.child("task").spawn(|_| async move {
1061                let mut value = 0u8;
1062                let borrow = &mut value;
1063                *borrow += 1;
1064                reschedule().await;
1065                *borrow += 1;
1066                value
1067            });
1068            assert_eq!(handle.await.unwrap(), 2);
1069        });
1070    }
1071}