Skip to main content

shuttle_std/
future.rs

1//! Shuttle's implementation of an async executor, roughly equivalent to [`futures::executor`].
2//!
3//! The [spawn] method spawns a new asynchronous task that the executor will run to completion. The
4//! [block_on] method blocks the current thread on the completion of a future.
5//!
6//! [`futures::executor`]: https://docs.rs/futures/0.3.13/futures/executor/index.html
7
8use shuttle_engine::backtrace_enabled;
9use shuttle_engine::runtime::execution::ExecutionState;
10use shuttle_engine::runtime::task::TaskId;
11use shuttle_engine::runtime::thread;
12use std::error::Error;
13use std::fmt::{Display, Formatter};
14use std::future::Future;
15use std::panic::Location;
16use std::pin::Pin;
17use std::result::Result;
18use std::sync::atomic::{AtomicBool, Ordering};
19use std::sync::Arc;
20use std::task::{Context, Poll, Waker};
21
22pub use shuttle_engine::future::batch_semaphore;
23
24fn spawn_inner<F>(fut: F, caller: &'static Location<'static>) -> JoinHandle<F::Output>
25where
26    F: Future + 'static,
27    F::Output: 'static,
28{
29    let stack_size = ExecutionState::with(|s| s.config.stack_size);
30    let inner = Arc::new(std::sync::Mutex::new(JoinHandleInner::default()));
31    let aborted = Arc::new(AtomicBool::new(false));
32    let task_id = ExecutionState::spawn_future(
33        Wrapper::new(fut, inner.clone(), aborted.clone()),
34        stack_size,
35        None,
36        caller,
37    );
38
39    JoinHandle {
40        task_id,
41        inner,
42        aborted,
43    }
44}
45
46/// Spawn a new async task that the executor will run to completion.
47#[track_caller]
48pub fn spawn<F>(fut: F) -> JoinHandle<F::Output>
49where
50    F: Future + Send + 'static,
51    F::Output: Send + 'static,
52{
53    spawn_inner(fut, Location::caller())
54}
55
56/// Spawn a new async task that the executor will run to completion.
57/// This is just `spawn` without the `Send` bound, and it mirrors `spawn_local` from Tokio.
58#[track_caller]
59pub fn spawn_local<F>(fut: F) -> JoinHandle<F::Output>
60where
61    F: Future + 'static,
62    F::Output: 'static,
63{
64    spawn_inner(fut, Location::caller())
65}
66
67/// An owned permission to abort a spawned task, without awaiting its completion.
68#[derive(Debug, Clone)]
69pub struct AbortHandle {
70    task_id: TaskId,
71    aborted: Arc<AtomicBool>,
72}
73
74impl AbortHandle {
75    /// Abort the task associated with the handle.
76    ///
77    /// The task will be cancelled at the next await point. If the task has already
78    /// completed, this is a no-op.
79    pub fn abort(&self) {
80        // Scheduling point: the scheduler may run other tasks (including the target)
81        // before the abort flag is set, creating interleavings where the task completes
82        // normally despite an abort() call.
83        thread::switch();
84
85        // Signal the Wrapper to skip the inner future on the next poll.
86        // If already aborted, skip the redundant wake.
87        if self.aborted.swap(true, Ordering::Relaxed) {
88            return;
89        }
90        // Wake the task so Wrapper::poll() runs and performs cleanup.
91        let res = ExecutionState::try_with(|state| {
92            if !state.is_finished() {
93                state.get_mut(self.task_id).abort();
94            }
95        });
96        if let Err(e) = res {
97            tracing::error!("`AbortHandle::abort` failed with error: {e:?}");
98        }
99    }
100
101    /// Returns `true` if this task is finished, otherwise returns `false`.
102    ///
103    /// ## Panics
104    /// Panics if called outside of shuttle context, i.e. if there is no execution context.
105    pub fn is_finished(&self) -> bool {
106        ExecutionState::with(|state| {
107            let task = state.get(self.task_id);
108            task.finished()
109        })
110    }
111}
112
113unsafe impl Send for AbortHandle {}
114unsafe impl Sync for AbortHandle {}
115
116/// An owned permission to join on an async task (await its termination).
117#[derive(Debug)]
118pub struct JoinHandle<T> {
119    task_id: TaskId,
120    inner: Arc<std::sync::Mutex<JoinHandleInner<T>>>,
121    aborted: Arc<AtomicBool>,
122}
123
124#[derive(Debug)]
125struct JoinHandleInner<T> {
126    result: Option<Result<T, JoinError>>,
127    waker: Option<Waker>,
128}
129
130impl<T> Default for JoinHandleInner<T> {
131    fn default() -> Self {
132        JoinHandleInner {
133            result: None,
134            waker: None,
135        }
136    }
137}
138
139impl<T> JoinHandle<T> {
140    /// Abort the task associated with the handle.
141    ///
142    /// The task will be cancelled at the next await point. Awaiting the `JoinHandle` after
143    /// calling `abort` will return `Err(JoinError::Cancelled)`, unless the task completed
144    /// before the abort took effect.
145    pub fn abort(&self) {
146        // Scheduling point: the scheduler may run other tasks (including the target)
147        // before the abort flag is set, creating interleavings where the task completes
148        // normally despite an abort() call.
149        thread::switch();
150
151        // Signal the Wrapper to skip the inner future on the next poll.
152        // If already aborted, skip the redundant wake.
153        if self.aborted.swap(true, Ordering::Relaxed) {
154            return;
155        }
156        // Wake the task so Wrapper::poll() runs and performs cleanup.
157        let res = ExecutionState::try_with(|state| {
158            if !state.is_finished() {
159                state.get_mut(self.task_id).abort();
160            }
161        });
162        if let Err(e) = res {
163            tracing::error!("`JoinHandle::abort` failed with error: {e:?}");
164        }
165    }
166
167    /// Returns `true` if this task is finished, otherwise returns `false`.
168    ///
169    /// ## Panics
170    /// Panics if called outside of shuttle context, i.e. if there is no execution context.
171    pub fn is_finished(&self) -> bool {
172        ExecutionState::with(|state| {
173            let task = state.get(self.task_id);
174            task.finished()
175        })
176    }
177
178    /// Returns a new `AbortHandle` that can be used to remotely abort this task.
179    pub fn abort_handle(&self) -> AbortHandle {
180        AbortHandle {
181            task_id: self.task_id,
182            aborted: self.aborted.clone(),
183        }
184    }
185}
186
187// TODO: need to work out all the error cases here
188/// Task failed to execute to completion.
189#[derive(Debug)]
190pub enum JoinError {
191    /// Task was aborted
192    Cancelled,
193}
194
195impl Display for JoinError {
196    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
197        match self {
198            JoinError::Cancelled => write!(f, "task was cancelled"),
199        }
200    }
201}
202
203impl Error for JoinError {}
204
205impl<T> Drop for JoinHandle<T> {
206    fn drop(&mut self) {
207        // Detach the task so it keeps running but we don't wait for it.
208        // Dropping a JoinHandle does NOT cancel the task (unlike abort()).
209        let res = ExecutionState::try_with(|state| {
210            if !state.is_finished() {
211                state.detach(self.task_id);
212            }
213        });
214        if let Err(e) = res {
215            tracing::error!("`JoinHandle::drop` failed with error: {e:?}");
216        }
217    }
218}
219
220impl<T> Future for JoinHandle<T> {
221    type Output = Result<T, JoinError>;
222
223    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
224        let mut lock = self.inner.lock().unwrap();
225        if let Some(result) = lock.result.take() {
226            Poll::Ready(result)
227        } else {
228            lock.waker = Some(cx.waker().clone());
229
230            ExecutionState::with(|state| {
231                state.current_mut().backtrace = if backtrace_enabled() {
232                    Some(std::backtrace::Backtrace::force_capture())
233                } else {
234                    None
235                }
236            });
237
238            Poll::Pending
239        }
240    }
241}
242
243// We wrap a task returning a value inside a wrapper task that returns (). The wrapper
244// contains a mutex-wrapped field that stores the value and the waker for the task
245// waiting on the join handle. When `poll` returns `Poll::Ready`, the `Wrapper` stores
246// the result in the `result` field and wakes the `waker`.
247//
248// The `aborted` flag is set by `JoinHandle::abort()` / `AbortHandle::abort()`. On the
249// next poll, `Wrapper` detects the flag, drops the inner future (running its destructors),
250// cleans up thread-local storage, publishes `Err(JoinError::Cancelled)` to any awaiting
251// join handle, and returns `Poll::Ready(())`.
252struct Wrapper<F: Future> {
253    /// The inner future. Wrapped in `Option` so we can drop it explicitly in the
254    /// abort path (before running thread-local destructors).
255    future: Option<Pin<Box<F>>>,
256    /// Where the result goes, until it has been published.
257    inner: Option<Arc<std::sync::Mutex<JoinHandleInner<F::Output>>>>,
258    aborted: Arc<AtomicBool>,
259}
260
261impl<F> Wrapper<F>
262where
263    F: Future + 'static,
264    F::Output: 'static,
265{
266    fn new(future: F, inner: Arc<std::sync::Mutex<JoinHandleInner<F::Output>>>, aborted: Arc<AtomicBool>) -> Self {
267        Self {
268            future: Some(Box::pin(future)),
269            inner: Some(inner),
270            aborted,
271        }
272    }
273}
274
275impl<F> Wrapper<F>
276where
277    F: Future + 'static,
278    F::Output: 'static,
279{
280    /// Drop the future, clean up thread-local storage, publish the result to the JoinHandle, and
281    /// wake any task waiting on the result.
282    fn finish(&mut self, result: Result<F::Output, JoinError>) {
283        // Drop the inner future first, so that its destructors can still use the task's
284        // thread-locals: a future that is ready can still own futures that are not, such as the one
285        // inside a `timeout` that fired.
286        self.future.take();
287        let inner = self.inner.take().expect("a task's result is published once");
288        // Nobody can read the result of a detached task, so drop it now too, as std drops what a
289        // thread returns once its `JoinHandle` is gone.
290        let result = if Arc::strong_count(&inner) > 1 {
291            Some(result)
292        } else {
293            drop(result);
294            None
295        };
296
297        // Run thread-local destructors.
298        ExecutionState::drop_task_locals();
299
300        let mut lock = inner.lock().unwrap();
301        if let Some(result) = result {
302            lock.result = Some(result);
303        }
304        if let Some(waker) = lock.waker.take() {
305            waker.wake();
306        }
307    }
308}
309
310impl<F: Future> Drop for Wrapper<F> {
311    fn drop(&mut self) {
312        // A task whose future is dropped before it finished was torn down at the end of the
313        // execution, which cancels it, as if it was aborted (see `ExecutionState::tear_down`, which
314        // drops the task's thread-local storage). Unless the execution is abandoned: then Shuttle's
315        // own destructors skip their bookkeeping.
316        if let Some(inner) = self.inner.take() {
317            // Drop the inner future first, like `finish`.
318            self.future.take();
319            if !ExecutionState::should_stop() {
320                let mut lock = inner.lock().unwrap();
321                lock.result = Some(Err(JoinError::Cancelled));
322                if let Some(waker) = lock.waker.take() {
323                    waker.wake();
324                }
325            }
326        }
327    }
328}
329
330impl<F> Future for Wrapper<F>
331where
332    F: Future + 'static,
333    F::Output: 'static,
334{
335    type Output = ();
336
337    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
338        let this = self.get_mut();
339
340        // If abort() was called, skip polling the inner future.
341        if this.aborted.load(Ordering::Relaxed) {
342            // If the execution is already finished (e.g., the runtime is shutting down),
343            // we can't access task state for cleanup. Just return Ready to let the task
344            // be cleaned up by the runtime.
345            if ExecutionState::try_with(|state| state.is_finished()).unwrap_or(true) {
346                return Poll::Ready(());
347            }
348
349            this.finish(Err(JoinError::Cancelled));
350            return Poll::Ready(());
351        }
352
353        match this.future.as_mut().unwrap().as_mut().poll(cx) {
354            Poll::Ready(result) => {
355                // If we've finished execution already (this task was detached), don't clean up. We
356                // can't access the state any more to destroy thread locals, and don't want to run
357                // any more wakers (which will be no-ops anyway).
358                if ExecutionState::try_with(|state| state.is_finished()).unwrap_or(true) {
359                    return Poll::Ready(());
360                }
361
362                this.finish(Ok(result));
363                Poll::Ready(())
364            }
365            Poll::Pending => Poll::Pending,
366        }
367    }
368}
369
370/// Run a future to completion on the current thread.
371pub fn block_on<F: Future>(future: F) -> F::Output {
372    let mut future = Box::pin(future);
373    let waker = ExecutionState::with(|state| state.current_mut().waker());
374    let cx = &mut Context::from_waker(&waker);
375
376    // Note: we only switch on poll pending, since this blocks the current task. This means that *internal*
377    // Shuttle futures which do not use other Shuttle primitives such as `batch_semaphore::Acquire` must
378    // have a scheduling point prior to first poll if that poll will be successful and can affect other tasks.
379    // For example, an uncontested acquire makes other threads block or fail try-acquires, so there must be
380    // a scheduling point for scheduling completeness. For *external* futures, this is a non-issue because they
381    // should use other Shuttle primitives inside of `poll` if polling can affect other threads.
382    loop {
383        match future.as_mut().poll(cx) {
384            Poll::Ready(result) => break result,
385            Poll::Pending => {
386                ExecutionState::with(|state| state.current_mut().sleep_unless_woken());
387                thread::switch();
388            }
389        }
390    }
391}
392
393/// Yields execution back to the scheduler.
394///
395/// Borrowed from the Tokio implementation.
396pub async fn yield_now() {
397    /// Yield implementation
398    struct YieldNow {
399        yielded: bool,
400    }
401
402    impl Future for YieldNow {
403        type Output = ();
404
405        fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
406            if self.yielded {
407                return Poll::Ready(());
408            }
409
410            self.yielded = true;
411            cx.waker().wake_by_ref();
412            ExecutionState::request_yield();
413            Poll::Pending
414        }
415    }
416
417    YieldNow { yielded: false }.await
418}