Skip to main content

async_rt/rt/
lite.rs

1use crate::{
2    CompletionGuard, Executor, ExecutorBlockOn, ExecutorBlocking, ExecutorTimeout, InnerJoinHandle,
3    JoinHandle, abortable_result,
4};
5use futures::future::{AbortHandle, BoxFuture};
6use futures::task::AtomicWaker;
7use parking_lot::Mutex;
8use pollable_map::optional::Optional;
9use std::fmt::{Debug, Formatter};
10use std::future::Future;
11use std::sync::atomic::{AtomicBool, Ordering};
12use std::sync::{Arc, LazyLock};
13use std::task::{Context, Poll, Wake, Waker};
14
15thread_local! {
16    static WAKER_LOCAL_THREAD: Waker = Waker::from(Arc::new(LocalWaker(std::thread::current())));
17}
18
19static LITE_EXECUTOR: LazyLock<LiteRuntimeExecutor> = LazyLock::new(LiteRuntimeExecutor::default);
20
21struct LocalWaker(std::thread::Thread);
22
23impl Wake for LocalWaker {
24    fn wake(self: Arc<Self>) {
25        self.0.unpark();
26    }
27
28    fn wake_by_ref(self: &Arc<Self>) {
29        self.0.unpark();
30    }
31}
32
33struct DriveGuard<'a>(&'a AtomicBool);
34
35impl Drop for DriveGuard<'_> {
36    fn drop(&mut self) {
37        self.0.store(false, Ordering::Release);
38    }
39}
40
41struct ActiveTasks<'a> {
42    queue: &'a Mutex<Vec<BoxFuture<'static, ()>>>,
43    tasks: Vec<BoxFuture<'static, ()>>,
44}
45
46impl Drop for ActiveTasks<'_> {
47    fn drop(&mut self) {
48        self.queue.lock().append(&mut self.tasks);
49    }
50}
51
52/// A light single-threaded executor backed by a shared runtime.
53///
54/// Tasks only make progress while [`LiteExecutor::block_on`] is running. Only one
55/// thread may drive the shared runtime at a time.
56#[derive(Default, Clone, Copy, Debug, PartialOrd, PartialEq, Eq)]
57pub struct LiteExecutor;
58
59impl Executor for LiteExecutor {
60    fn runtime_type(&self) -> Option<&'static str> {
61        Some("lite")
62    }
63
64    fn spawn<F>(&self, future: F) -> JoinHandle<F::Output>
65    where
66        F: Future + Send + 'static,
67        F::Output: Send + 'static,
68    {
69        LITE_EXECUTOR.spawn(future)
70    }
71}
72
73impl ExecutorBlocking for LiteExecutor {
74    fn spawn_blocking<F, R>(&self, f: F) -> JoinHandle<R>
75    where
76        F: FnOnce() -> R + Send + 'static,
77        R: Send + 'static,
78    {
79        LITE_EXECUTOR.spawn_blocking(f)
80    }
81}
82
83impl ExecutorTimeout for LiteExecutor {}
84
85impl ExecutorBlockOn for LiteExecutor {
86    fn block_on<F: Future>(&self, future: F) -> F::Output {
87        LITE_EXECUTOR.block_on(future)
88    }
89}
90
91/// A light single-threaded executor with minimal dependencies.
92///
93/// Tasks only make progress while [`LiteRuntimeExecutor::block_on`] is running. Only one
94/// thread may drive an executor and its clones at a time.
95#[derive(Default, Clone)]
96pub struct LiteRuntimeExecutor {
97    queued_tasks: Arc<Mutex<Vec<BoxFuture<'static, ()>>>>,
98    waker: Arc<AtomicWaker>,
99    driving: Arc<AtomicBool>,
100}
101
102impl Debug for LiteRuntimeExecutor {
103    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
104        f.debug_struct("LiteRuntimeExecutor").finish()
105    }
106}
107
108impl PartialEq for LiteRuntimeExecutor {
109    fn eq(&self, other: &Self) -> bool {
110        Arc::ptr_eq(&self.queued_tasks, &other.queued_tasks)
111    }
112}
113
114impl Eq for LiteRuntimeExecutor {}
115
116impl LiteRuntimeExecutor {
117    /// Creates a new lightweight runtime.
118    pub fn new() -> Self {
119        Self::default()
120    }
121
122    fn take_queued_tasks(&self, tasks: &mut Vec<BoxFuture<'static, ()>>) -> bool {
123        let mut queued = self.queued_tasks.lock();
124        if queued.is_empty() {
125            return false;
126        }
127
128        tasks.append(&mut queued);
129        true
130    }
131}
132
133impl Executor for LiteRuntimeExecutor {
134    fn runtime_type(&self) -> Option<&'static str> {
135        Some("lite")
136    }
137
138    fn spawn<F>(&self, future: F) -> JoinHandle<F::Output>
139    where
140        F: Future + Send + 'static,
141        F::Output: Send + 'static,
142    {
143        let (abort_handle, abort_registration) = AbortHandle::new_pair();
144        let future = abortable_result(future, abort_registration);
145        let (tx, rx) = futures::channel::oneshot::channel();
146        let finished = Arc::new(AtomicBool::new(false));
147        let completion = CompletionGuard::new(finished.clone());
148        let task = async move {
149            let _completion = completion;
150            let result = future.await;
151            let _ = tx.send(result);
152        };
153
154        self.queued_tasks.lock().push(Box::pin(task));
155        self.waker.wake();
156
157        let inner = InnerJoinHandle::CustomHandle {
158            inner: Optional::new(rx),
159            handle: abort_handle,
160            finished,
161        };
162
163        JoinHandle { inner }
164    }
165}
166
167impl ExecutorBlocking for LiteRuntimeExecutor {
168    fn spawn_blocking<F, R>(&self, f: F) -> JoinHandle<R>
169    where
170        F: FnOnce() -> R + Send + 'static,
171        R: Send + 'static,
172    {
173        self.spawn(async move {
174            let (tx, rx) = futures::channel::oneshot::channel();
175            let _handle = std::thread::spawn(move || {
176                let result = f();
177                let _ = tx.send(result);
178            });
179            rx.await.expect("blocking task should not be dropped")
180        })
181    }
182}
183
184impl ExecutorTimeout for LiteRuntimeExecutor {}
185
186impl ExecutorBlockOn for LiteRuntimeExecutor {
187    fn block_on<F: Future>(&self, future: F) -> F::Output {
188        if self
189            .driving
190            .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
191            .is_err()
192        {
193            panic!("LiteRuntimeExecutor is already being driven");
194        }
195        let _drive_guard = DriveGuard(&self.driving);
196        let mut future = core::pin::pin!(future);
197        let mut active = ActiveTasks {
198            queue: &self.queued_tasks,
199            tasks: Vec::new(),
200        };
201
202        WAKER_LOCAL_THREAD.with(|waker| {
203            let mut context = Context::from_waker(waker);
204
205            loop {
206                self.waker.register(waker);
207                self.take_queued_tasks(&mut active.tasks);
208
209                if let Poll::Ready(output) = future.as_mut().poll(&mut context) {
210                    return output;
211                }
212
213                let mut made_progress = false;
214                let mut index = 0;
215                while index < active.tasks.len() {
216                    if active.tasks[index].as_mut().poll(&mut context).is_ready() {
217                        drop(active.tasks.swap_remove(index));
218                        made_progress = true;
219                    } else {
220                        index += 1;
221                    }
222                }
223
224                if self.take_queued_tasks(&mut active.tasks) || made_progress {
225                    continue;
226                }
227
228                self.waker.register(waker);
229                if self.take_queued_tasks(&mut active.tasks) {
230                    continue;
231                }
232
233                std::thread::park();
234            }
235        })
236    }
237}
238
239#[cfg(test)]
240mod tests {
241    use super::{LiteExecutor, LiteRuntimeExecutor};
242    use crate::error::JoinError;
243    use crate::{Executor, ExecutorBlockOn, ExecutorBlocking, ExecutorTimeout};
244    use std::panic::AssertUnwindSafe;
245    use std::time::Duration;
246
247    #[test]
248    fn ready_future_completes() {
249        assert_eq!(LiteRuntimeExecutor::new().block_on(async { 42 }), 42);
250    }
251
252    #[test]
253    fn shared_executor_drives_spawned_tasks() {
254        let handle = LiteExecutor.spawn(async { 42 });
255
256        assert_eq!(LiteExecutor.block_on(handle).unwrap(), 42);
257    }
258
259    #[test]
260    fn spawned_task_completes() {
261        let executor = LiteRuntimeExecutor::new();
262        let handle = executor.spawn(async { 42 });
263
264        assert_eq!(executor.block_on(handle).unwrap(), 42);
265    }
266
267    #[test]
268    fn nested_spawn_completes() {
269        let executor = LiteRuntimeExecutor::new();
270        let nested_executor = executor.clone();
271        let handle =
272            executor.spawn(async move { nested_executor.spawn(async { 41 }).await.unwrap() + 1 });
273
274        assert_eq!(executor.block_on(handle).unwrap(), 42);
275    }
276
277    #[test]
278    fn task_spawned_from_another_thread_wakes_driver() {
279        let executor = LiteRuntimeExecutor::new();
280        let other_executor = executor.clone();
281        let (start_tx, start_rx) = std::sync::mpsc::channel();
282        let (value_tx, value_rx) = futures::channel::oneshot::channel();
283
284        let worker = std::thread::spawn(move || {
285            start_rx.recv().unwrap();
286            std::thread::sleep(Duration::from_millis(10));
287            other_executor.dispatch(async move {
288                value_tx.send(42).unwrap();
289            });
290        });
291
292        let value = executor.block_on(async move {
293            start_tx.send(()).unwrap();
294            value_rx.await.unwrap()
295        });
296
297        worker.join().unwrap();
298        assert_eq!(value, 42);
299    }
300
301    #[test]
302    fn unfinished_tasks_are_preserved() {
303        let executor = LiteRuntimeExecutor::new();
304        let handle = executor.spawn(async { 42 });
305
306        executor.block_on(async {});
307
308        assert!(!handle.is_finished());
309        assert_eq!(executor.block_on(handle).unwrap(), 42);
310    }
311
312    #[cfg(panic = "unwind")]
313    #[test]
314    fn unfinished_tasks_are_preserved_after_block_on_panics() {
315        let executor = LiteRuntimeExecutor::new();
316        let handle = executor.spawn(async { 42 });
317
318        let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
319            executor.block_on(async { panic!("expected driver panic") });
320        }));
321
322        assert!(result.is_err());
323        assert!(!handle.is_finished());
324        assert_eq!(executor.block_on(handle).unwrap(), 42);
325    }
326
327    #[test]
328    fn spawn_blocking_completes() {
329        let executor = LiteRuntimeExecutor::new();
330        let handle = executor.spawn_blocking(|| 42);
331
332        assert_eq!(executor.block_on(handle).unwrap(), 42);
333    }
334
335    #[test]
336    fn timeout_completes() {
337        let executor = LiteRuntimeExecutor::new();
338        let handle = executor.spawn_timeout(Duration::from_millis(10), async {
339            futures::future::pending::<()>().await
340        });
341
342        assert!(executor.block_on(handle).unwrap().is_err());
343    }
344
345    #[cfg(panic = "unwind")]
346    #[test]
347    fn task_panic_is_classified() {
348        let executor = LiteRuntimeExecutor::new();
349        let handle = executor.spawn(async { panic!("expected task panic") });
350
351        assert!(matches!(
352            executor.block_on(handle),
353            Err(JoinError::Panicked)
354        ));
355    }
356
357    #[test]
358    fn explicit_abort_is_reported_as_aborted() {
359        let executor = LiteRuntimeExecutor::new();
360        let handle = executor.spawn(futures::future::pending::<()>());
361        handle.abort();
362
363        assert!(matches!(executor.block_on(handle), Err(JoinError::Aborted)));
364    }
365
366    #[cfg(panic = "unwind")]
367    #[test]
368    fn concurrent_block_on_panics() {
369        let executor = LiteRuntimeExecutor::new();
370        let other_executor = executor.clone();
371        let (started_tx, started_rx) = std::sync::mpsc::channel();
372        let (release_tx, release_rx) = futures::channel::oneshot::channel();
373
374        let driver = std::thread::spawn(move || {
375            other_executor.block_on(async move {
376                started_tx.send(()).unwrap();
377                release_rx.await.unwrap();
378            });
379        });
380        started_rx.recv().unwrap();
381
382        let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
383            executor.block_on(async {});
384        }));
385
386        release_tx.send(()).unwrap();
387        driver.join().unwrap();
388        assert!(result.is_err());
389    }
390}