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