Skip to main content

moonpool_sim/executor/
join.rs

1//! Join handles and yield primitives for the deterministic executor.
2
3use std::any::Any;
4use std::future::Future;
5use std::pin::Pin;
6use std::task::{Context, Poll};
7
8use async_task::FallibleTask;
9use moonpool_core::JoinError;
10use parking_lot::Mutex;
11
12use super::TaskMeta;
13
14/// A task's raw output: the value, or the payload of the panic that killed it
15/// (captured by the `catch_unwind` wrapper installed at spawn).
16pub(crate) type TaskResult<T> = Result<T, Box<dyn Any + Send>>;
17
18/// Owned handle to a spawned task, mirroring `tokio::task::JoinHandle`
19/// semantics.
20///
21/// - `.await` resolves to `Ok(output)`, or `Err(JoinError::Cancelled)` after
22///   [`abort`](Self::abort), or `Err(JoinError::Panicked)` if the task
23///   panicked (siblings and the executor are unaffected).
24/// - Dropping the handle **detaches** the task: it keeps running to
25///   completion. Only [`abort`](Self::abort) or dropping the whole
26///   [`Executor`](super::Executor) cancels it.
27/// - [`is_finished`](Self::is_finished) is a non-blocking completion probe.
28#[derive(Debug)]
29pub struct JoinHandle<T> {
30    /// `None` after `abort()` consumed the inner task (async-task cancels a
31    /// task when its `Task`/`FallibleTask` handle is dropped). The `Mutex`
32    /// (uncontended, single-threaded) exists so `abort(&self)` can take the
33    /// inner value while the handle satisfies the `Sync` bound that
34    /// `TaskProvider::JoinHandle` requires.
35    inner: Mutex<Option<FallibleTask<TaskResult<T>, TaskMeta>>>,
36}
37
38impl<T> JoinHandle<T> {
39    pub(crate) fn new(task: FallibleTask<TaskResult<T>, TaskMeta>) -> Self {
40        Self {
41            inner: Mutex::new(Some(task)),
42        }
43    }
44
45    /// Report whether the task has finished running (completed, panicked, or
46    /// was aborted). Non-blocking; mirrors `tokio::task::JoinHandle::is_finished`.
47    #[must_use]
48    pub fn is_finished(&self) -> bool {
49        match self.inner.lock().as_ref() {
50            Some(task) => task.is_finished(),
51            None => true,
52        }
53    }
54
55    /// Abort the task: it will never be polled again and its future is
56    /// dropped. Awaiting the handle afterwards yields
57    /// `Err(JoinError::Cancelled)`.
58    ///
59    /// Aborting a task that already finished is a no-op and awaiting the
60    /// handle still yields the task's output (tokio parity: abort racing
61    /// completion loses).
62    pub fn abort(&self) {
63        let mut inner = self.inner.lock();
64        // A finished task keeps its output harvestable; only a live task is
65        // cancelled (dropping the FallibleTask cancels, async-task semantics).
66        if inner.as_ref().is_some_and(|task| !task.is_finished()) {
67            drop(inner.take());
68        }
69    }
70}
71
72impl<T> Future for JoinHandle<T> {
73    type Output = Result<T, JoinError>;
74
75    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
76        let mut inner = self.inner.lock();
77        match inner.as_mut() {
78            // Aborted through this handle.
79            None => Poll::Ready(Err(JoinError::Cancelled)),
80            Some(task) => match Pin::new(task).poll(cx) {
81                Poll::Pending => Poll::Pending,
82                // Cancelled elsewhere (executor drop) before completing.
83                Poll::Ready(None) => Poll::Ready(Err(JoinError::Cancelled)),
84                Poll::Ready(Some(Ok(output))) => Poll::Ready(Ok(output)),
85                Poll::Ready(Some(Err(_panic_payload))) => Poll::Ready(Err(JoinError::Panicked)),
86            },
87        }
88    }
89}
90
91impl<T> Drop for JoinHandle<T> {
92    fn drop(&mut self) {
93        // Detach-on-drop (tokio parity): the task keeps running.
94        if let Some(task) = self.inner.lock().take() {
95            task.detach();
96        }
97    }
98}
99
100/// Future returned by [`yield_now`] and [`until_stalled`]: pends once,
101/// waking its own waker, then completes on the next poll.
102#[derive(Debug)]
103pub struct YieldNow {
104    yielded: bool,
105}
106
107impl Future for YieldNow {
108    type Output = ();
109
110    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
111        if self.yielded {
112            Poll::Ready(())
113        } else {
114            self.yielded = true;
115            cx.waker().wake_by_ref();
116            Poll::Pending
117        }
118    }
119}
120
121/// Yield control from inside a task.
122///
123/// The task is rescheduled into the ready queue and resumes at a
124/// seeded-random position among the runnable tasks (there is no "back of
125/// the queue" under randomized scheduling).
126#[must_use = "futures do nothing unless awaited"]
127pub fn yield_now() -> YieldNow {
128    YieldNow { yielded: false }
129}
130
131/// Driver-only: resume after every currently-runnable task has been polled
132/// to `Pending` or completion.
133///
134/// Awaited by the orchestrator's step loops between `sim.step()` calls.
135/// Mechanically identical to [`yield_now`], but because
136/// [`Executor::block_on`](super::Executor::block_on) only re-polls the
137/// driver after a full `run_until_stalled` drain, awaiting this from the
138/// driver guarantees the "all runnable tasks ran" contract that the old
139/// tokio-FIFO `yield_now` dance only approximated.
140///
141/// # Panics
142///
143/// Panics when called from inside a task (all builds): a task awaiting
144/// "drain until nothing is runnable" would silently degrade to plain
145/// [`yield_now`] semantics, resuming mid-drain while believing every
146/// runnable task quiesced. That contract violation must not survive only
147/// in release. Tasks yield with [`yield_now`].
148#[must_use = "futures do nothing unless awaited"]
149pub fn until_stalled() -> YieldNow {
150    assert!(
151        !super::in_task(),
152        "until_stalled() is driver-only; tasks must use executor::yield_now()"
153    );
154    YieldNow { yielded: false }
155}