Skip to main content

vortex_io/runtime/
single.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use std::rc::Rc;
5use std::rc::Weak as RcWeak;
6use std::sync::Arc;
7
8use async_executor::LocalExecutor;
9use futures::Stream;
10use futures::StreamExt;
11use futures::channel::oneshot;
12use futures::future::BoxFuture;
13use futures::stream::LocalBoxStream;
14use parking_lot::Mutex;
15use vortex_error::vortex_panic;
16
17use crate::runtime::AbortHandle;
18use crate::runtime::AbortHandleRef;
19use crate::runtime::BlockingRuntime;
20use crate::runtime::Executor;
21use crate::runtime::Handle;
22use crate::runtime::abort::TaskAbortHandle;
23use crate::runtime::platform;
24
25/// A runtime that drives all work on the current thread.
26///
27/// This is subtly different from using a current-thread runtime to drive a future since it is
28/// capable of running `!Send` I/O futures.
29///
30/// This is also the runtime to use on WebAssembly targets without a JavaScript event loop: since
31/// spawned work is queued onto the local executor rather than driven inline, tasks that
32/// communicate with one another can make progress. Note that `block_on` cannot park a
33/// single-threaded WebAssembly thread, so every future it drives must be woken from this same
34/// thread; futures waiting on an external event loop will never complete.
35pub struct SingleThreadRuntime {
36    sender: Arc<Sender>,
37    executor: Rc<LocalExecutor<'static>>,
38}
39
40impl Default for SingleThreadRuntime {
41    fn default() -> Self {
42        let executor = Rc::new(LocalExecutor::new());
43        let sender = Arc::new(Sender::new(&executor));
44        Self { sender, executor }
45    }
46}
47
48struct Sender {
49    scheduling: kanal::Sender<SpawnAsync<'static>>,
50    cpu: kanal::Sender<SpawnSync<'static>>,
51    blocking: kanal::Sender<SpawnSync<'static>>,
52}
53
54impl Sender {
55    fn new(local: &Rc<LocalExecutor<'static>>) -> Self {
56        let (scheduling_send, scheduling_recv) = kanal::unbounded::<SpawnAsync>();
57        let (cpu_send, cpu_recv) = kanal::unbounded::<SpawnSync>();
58        let (blocking_send, blocking_recv) = kanal::unbounded::<SpawnSync>();
59
60        // We pass weak references to the local execution into the async tasks such that the task's
61        // reference doesn't keep the execution alive after the runtime is dropped.
62        let weak_local = Rc::downgrade(local);
63
64        // Drive scheduling tasks.
65        let weak_local2 = RcWeak::clone(&weak_local);
66        local
67            .spawn(async move {
68                while let Ok(spawn) = scheduling_recv.as_async().recv().await {
69                    if let Some(local) = weak_local2.upgrade() {
70                        // Ignore send errors since it means the caller immediately detached.
71                        drop(
72                            spawn
73                                .task_callback
74                                .send(TaskAbortHandle::new_handle(local.spawn(spawn.future))),
75                        );
76                    }
77                }
78            })
79            .detach();
80
81        // Drive CPU tasks.
82        let weak_local2 = RcWeak::clone(&weak_local);
83        local
84            .spawn(async move {
85                while let Ok(spawn) = cpu_recv.as_async().recv().await {
86                    if let Some(local) = weak_local2.upgrade() {
87                        let work = spawn.sync;
88                        // Ignore send errors since it means the caller immediately detached.
89                        drop(spawn.task_callback.send(TaskAbortHandle::new_handle(
90                            local.spawn(async move { work() }),
91                        )));
92                    }
93                }
94            })
95            .detach();
96
97        // Drive blocking tasks.
98        let weak_local2 = RcWeak::clone(&weak_local);
99        local
100            .spawn(async move {
101                while let Ok(spawn) = blocking_recv.as_async().recv().await {
102                    if let Some(local) = weak_local2.upgrade() {
103                        let work = spawn.sync;
104                        // Ignore send errors since it means the caller immediately detached.
105                        drop(spawn.task_callback.send(TaskAbortHandle::new_handle(
106                            local.spawn(async move { work() }),
107                        )));
108                    }
109                }
110            })
111            .detach();
112
113        Self {
114            scheduling: scheduling_send,
115            cpu: cpu_send,
116            blocking: blocking_send,
117        }
118    }
119}
120
121/// Since the [`Handle`], and therefore runtime implementation needs to be `Send` and `Sync`,
122/// we cannot just `impl Runtime for LocalExecutor`. Instead, we create channels that the handle
123/// can forward its work into, and we drive the resulting tasks on a [`LocalExecutor`] on the
124/// calling thread.
125impl Executor for Sender {
126    fn spawn(&self, future: BoxFuture<'static, ()>) -> AbortHandleRef {
127        let (send, recv) = oneshot::channel();
128        if let Err(e) = self.scheduling.send(SpawnAsync {
129            future,
130            task_callback: send,
131        }) {
132            vortex_panic!("Executor missing: {}", e);
133        }
134        Box::new(LazyAbortHandle {
135            task: Mutex::new(recv),
136        })
137    }
138
139    fn spawn_cpu(&self, cpu: Box<dyn FnOnce() + Send + 'static>) -> AbortHandleRef {
140        let (send, recv) = oneshot::channel();
141        if let Err(e) = self.cpu.send(SpawnSync {
142            sync: cpu,
143            task_callback: send,
144        }) {
145            vortex_panic!("Executor missing: {}", e);
146        }
147        Box::new(LazyAbortHandle {
148            task: Mutex::new(recv),
149        })
150    }
151
152    fn spawn_blocking_io(&self, work: Box<dyn FnOnce() + Send + 'static>) -> AbortHandleRef {
153        let (send, recv) = oneshot::channel();
154        if let Err(e) = self.blocking.send(SpawnSync {
155            sync: work,
156            task_callback: send,
157        }) {
158            vortex_panic!("Executor missing: {}", e);
159        }
160        Box::new(LazyAbortHandle {
161            task: Mutex::new(recv),
162        })
163    }
164}
165
166impl BlockingRuntime for SingleThreadRuntime {
167    type BlockingIterator<'a, R: 'a> = SingleThreadIterator<'a, R>;
168
169    fn handle(&self) -> Handle {
170        let executor: Arc<dyn Executor> = Arc::clone(&self.sender) as Arc<dyn Executor>;
171        Handle::new(Arc::downgrade(&executor))
172    }
173
174    fn block_on<Fut, R>(&self, fut: Fut) -> R
175    where
176        Fut: Future<Output = R>,
177    {
178        platform::block_on(self.executor.run(fut))
179    }
180
181    fn block_on_stream<'a, S, R>(&self, stream: S) -> Self::BlockingIterator<'a, R>
182    where
183        S: Stream<Item = R> + Send + 'a,
184        R: Send + 'a,
185    {
186        SingleThreadIterator {
187            executor: Rc::clone(&self.executor),
188            stream: stream.boxed_local(),
189        }
190    }
191}
192
193/// Runs a future to completion on the current thread until it completes.
194///
195/// The future is provided a [`Handle`] to the runtime so that it may spawn additional tasks
196/// to be executed concurrently.
197pub fn block_on<F, Fut, R>(f: F) -> R
198where
199    F: FnOnce(Handle) -> Fut,
200    Fut: Future<Output = R>,
201{
202    let runtime = SingleThreadRuntime::default();
203    let handle = runtime.handle();
204    runtime.block_on(f(handle))
205}
206
207/// Returns an iterator wrapper around a stream, blocking the current thread for each item.
208pub fn block_on_stream<'a, F, S, R>(f: F) -> SingleThreadIterator<'a, R>
209where
210    F: FnOnce(Handle) -> S,
211    S: Stream<Item = R> + Send + Unpin + 'a,
212    R: Send + 'a,
213{
214    let runtime = SingleThreadRuntime::default();
215    let handle = runtime.handle();
216    runtime.block_on_stream(f(handle))
217}
218
219/// A spawn request for a future.
220///
221/// We pass back the abort handle via oneshot channel because this is a single-threaded runtime,
222/// meaning we need the spawning channel consumer to do some work before the caller can actually
223/// get ahold of their task handle.
224///
225/// The reason we don't pass back the executor's own task handle, and instead pass back a
226/// [`TaskAbortHandle`], is because we invert the behaviour of abort and drop. Dropping the abort
227/// handle results in the task being detached, whereas dropping the task handle results in the task
228/// being canceled. This helps avoid a race where the caller detaches the LazyAbortHandle before
229/// the task has been launched.
230struct SpawnAsync<'rt> {
231    future: BoxFuture<'rt, ()>,
232    task_callback: oneshot::Sender<AbortHandleRef>,
233}
234
235// A spawn request for a synchronous job.
236struct SpawnSync<'rt> {
237    sync: Box<dyn FnOnce() + Send + 'rt>,
238    task_callback: oneshot::Sender<AbortHandleRef>,
239}
240
241struct LazyAbortHandle {
242    task: Mutex<oneshot::Receiver<AbortHandleRef>>,
243}
244
245impl AbortHandle for LazyAbortHandle {
246    fn abort(self: Box<Self>) {
247        // Aborting a task is done by dropping its handle.
248        if let Ok(Some(task)) = self.task.lock().try_recv() {
249            task.abort()
250        }
251    }
252}
253
254/// A stream that wraps up the stream with the execution that drives it.
255pub struct SingleThreadIterator<'a, T> {
256    executor: Rc<LocalExecutor<'static>>,
257    stream: LocalBoxStream<'a, T>,
258}
259
260impl<T> Iterator for SingleThreadIterator<'_, T> {
261    type Item = T;
262
263    fn next(&mut self) -> Option<Self::Item> {
264        let fut = self.stream.next();
265        platform::block_on(self.executor.run(fut))
266    }
267}
268
269#[cfg(test)]
270mod tests {
271    use std::sync::Arc;
272    use std::sync::atomic::AtomicUsize;
273    use std::sync::atomic::Ordering;
274
275    use futures::FutureExt;
276
277    use crate::runtime::BlockingRuntime;
278    use crate::runtime::single::SingleThreadRuntime;
279    use crate::runtime::single::block_on;
280
281    #[test]
282    fn test_drive_simple_future() {
283        let result = SingleThreadRuntime::default().block_on(async { 123 }.boxed_local());
284        assert_eq!(result, 123);
285    }
286
287    #[test]
288    fn test_spawn_cpu_task() {
289        let counter = Arc::new(AtomicUsize::new(0));
290        let c = Arc::clone(&counter);
291
292        block_on(|handle| async move {
293            handle
294                .spawn_cpu(move || {
295                    c.fetch_add(1, Ordering::SeqCst);
296                })
297                .await
298        });
299
300        assert_eq!(counter.load(Ordering::SeqCst), 1);
301    }
302}