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}