Skip to main content

vortex_io/runtime/
current.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use std::future::Future;
5use std::sync::Arc;
6
7use futures::Stream;
8use futures::StreamExt;
9use futures::stream::BoxStream;
10use parking_lot::Mutex;
11use smol::block_on;
12
13use crate::runtime::BlockingRuntime;
14use crate::runtime::Executor;
15use crate::runtime::Handle;
16pub use crate::runtime::pool::CurrentThreadWorkerPool;
17
18/// A current thread runtime allows callers to much more explicitly drive Vortex futures than with
19/// a Tokio runtime.
20///
21/// The current thread runtime will do no work unless `block_on` is called. In other words, the
22/// default behavior is single-threaded with code running on the thread that called `block_on`.
23///
24/// It's also possible to clone the runtime onto other threads, each of which can call `block_on`
25/// to drive work on that thread. Each thread shares the same underlying executor with the same
26/// set of tasks, allowing work to be driven in parallel.
27///
28/// For automatic driving of work, a [`CurrentThreadWorkerPool`] can be created from the runtime
29/// by calling [`new_pool`](CurrentThreadRuntime::new_pool). The returned pool can be configured
30/// with the desired number of worker threads that will drive work on behalf of the runtime.
31#[derive(Clone, Default)]
32pub struct CurrentThreadRuntime {
33    executor: Arc<smol::Executor<'static>>,
34}
35
36impl CurrentThreadRuntime {
37    /// Create a new current thread runtime.
38    pub fn new() -> Self {
39        Self::default()
40    }
41
42    /// Create a new worker pool for driving the runtime in the background.
43    ///
44    /// This pool can be used to offload work from the current thread to a set of worker threads
45    /// that will drive the runtime's executor.
46    ///
47    /// By default, the pool has no worker threads; the caller must set the desired number of
48    /// worker threads using the `set_workers` method on the returned pool.
49    pub fn new_pool(&self) -> CurrentThreadWorkerPool {
50        CurrentThreadWorkerPool::new(Arc::clone(&self.executor))
51    }
52
53    /// Returns an iterator wrapper around a stream, blocking the current thread for each item.
54    ///
55    /// ## Multi-threaded Usage
56    ///
57    /// To drive the iterator from multiple threads, simply clone it and call `next()` on each
58    /// clone. Results on each thread are ordered with respect to the stream, but there is no
59    /// ordering guarantee between threads.
60    pub fn block_on_stream_thread_safe<F, S, R>(&self, f: F) -> ThreadSafeIterator<R>
61    where
62        F: FnOnce(Handle) -> S,
63        S: Stream<Item = R> + Send + 'static,
64        R: Send + 'static,
65    {
66        let stream = f(self.handle());
67
68        // We create an MPMC result channel and spawn a task to drive the stream and send results.
69        // This allows multiple worker threads to drive the execution while all waiting for results
70        // on the channel.
71        let (result_tx, result_rx) = kanal::bounded_async(1);
72        let driver = self.executor.spawn(async move {
73            futures::pin_mut!(stream);
74            while let Some(item) = stream.next().await {
75                // If all receivers are dropped, we stop driving the stream.
76                if let Err(e) = result_tx.send(item).await {
77                    tracing::trace!("all receivers dropped, stopping stream: {}", e);
78                    break;
79                }
80            }
81        });
82
83        ThreadSafeIterator {
84            executor: Arc::clone(&self.executor),
85            results: result_rx,
86            driver: Arc::new(Mutex::new(Some(driver))),
87        }
88    }
89}
90
91impl BlockingRuntime for CurrentThreadRuntime {
92    type BlockingIterator<'a, R: 'a> = CurrentThreadIterator<'a, R>;
93
94    fn handle(&self) -> Handle {
95        let executor: Arc<dyn Executor> = Arc::clone(&self.executor) as Arc<dyn Executor>;
96        Handle::new(Arc::downgrade(&executor))
97    }
98
99    fn block_on<Fut, R>(&self, fut: Fut) -> R
100    where
101        Fut: Future<Output = R>,
102    {
103        block_on(self.executor.run(fut))
104    }
105
106    fn block_on_stream<'a, S, R>(&self, stream: S) -> Self::BlockingIterator<'a, R>
107    where
108        S: Stream<Item = R> + Send + 'a,
109        R: Send + 'a,
110    {
111        CurrentThreadIterator {
112            executor: Arc::clone(&self.executor),
113            stream: stream.boxed(),
114        }
115    }
116}
117
118/// An iterator that wraps up a stream to drive it using the current thread execution.
119pub struct CurrentThreadIterator<'a, T> {
120    executor: Arc<smol::Executor<'static>>,
121    stream: BoxStream<'a, T>,
122}
123
124impl<T> Iterator for CurrentThreadIterator<'_, T> {
125    type Item = T;
126
127    fn next(&mut self) -> Option<Self::Item> {
128        block_on(self.executor.run(self.stream.next()))
129    }
130}
131
132/// An iterator that drives a stream from multiple threads.
133pub struct ThreadSafeIterator<T> {
134    executor: Arc<smol::Executor<'static>>,
135    results: kanal::AsyncReceiver<T>,
136    /// Handle to the task driving the stream. Once the stream ends, the first consumer to
137    /// observe it joins the task so a panic raised while driving the stream is re-raised rather
138    /// than silently ending the iterator.
139    driver: Arc<Mutex<Option<smol::Task<()>>>>,
140}
141
142// Manual clone implementation since `T` does not need to be `Clone`.
143impl<T> Clone for ThreadSafeIterator<T> {
144    fn clone(&self) -> Self {
145        Self {
146            executor: Arc::clone(&self.executor),
147            results: self.results.clone(),
148            driver: Arc::clone(&self.driver),
149        }
150    }
151}
152
153impl<T> Iterator for ThreadSafeIterator<T> {
154    type Item = T;
155
156    fn next(&mut self) -> Option<Self::Item> {
157        match block_on(self.executor.run(self.results.recv())) {
158            Ok(item) => Some(item),
159            // The result channel closes when the driver task finishes. Join the task so a panic
160            // raised while driving the stream is re-raised here instead of being lost. The first
161            // consumer to observe closure joins it; any later consumer just sees the stream end.
162            Err(_) => {
163                let task = self.driver.lock().take();
164                if let Some(task) = task {
165                    block_on(self.executor.run(task));
166                }
167                None
168            }
169        }
170    }
171}
172
173#[expect(clippy::if_then_some_else_none)] // Clippy is wrong when if/else has await.
174#[cfg(test)]
175mod tests {
176    use std::any::Any;
177    use std::panic::AssertUnwindSafe;
178    use std::sync::Arc;
179    use std::sync::Barrier;
180    use std::sync::atomic::AtomicUsize;
181    use std::sync::atomic::Ordering;
182    use std::task::Poll;
183    use std::thread;
184    use std::time::Duration;
185
186    use futures::StreamExt;
187    use futures::stream;
188    use parking_lot::Mutex;
189
190    use super::*;
191
192    #[test]
193    fn test_worker_thread() {
194        let runtime = CurrentThreadRuntime::new();
195
196        // We spawn a future that sets a value on a separate thread.
197        let value = Arc::new(AtomicUsize::new(0));
198        let value2 = Arc::clone(&value);
199        runtime
200            .handle()
201            .spawn(async move {
202                value2.store(42, Ordering::SeqCst);
203            })
204            .detach();
205
206        // By default, nothing has driven the executor, so the value should still be 0.
207        assert_eq!(value.load(Ordering::SeqCst), 0);
208
209        // An empty pool still does nothing.
210        let pool = runtime.new_pool();
211        assert_eq!(value.load(Ordering::SeqCst), 0);
212
213        // Adding a worker thread should drive the executor.
214        pool.set_workers(1);
215        for _ in 0..10 {
216            if value.load(Ordering::SeqCst) == 42 {
217                break;
218            }
219            thread::sleep(Duration::from_millis(10));
220        }
221        assert_eq!(value.load(Ordering::SeqCst), 42);
222    }
223
224    #[test]
225    fn test_block_on_stream_single_thread() {
226        let mut iter =
227            CurrentThreadRuntime::new().block_on_stream(stream::iter(vec![1, 2, 3, 4, 5]).boxed());
228
229        assert_eq!(iter.next(), Some(1));
230        assert_eq!(iter.next(), Some(2));
231        assert_eq!(iter.next(), Some(3));
232        assert_eq!(iter.next(), Some(4));
233        assert_eq!(iter.next(), Some(5));
234        assert_eq!(iter.next(), None);
235    }
236
237    #[test]
238    fn test_block_on_stream_multiple_threads() {
239        let counter = Arc::new(AtomicUsize::new(0));
240        let num_threads = 4;
241        let items_per_thread = 25;
242        let total_items = 100;
243
244        let iter = CurrentThreadRuntime::new()
245            .block_on_stream_thread_safe(|_h| stream::iter(0..total_items).boxed());
246
247        let barrier = Arc::new(Barrier::new(num_threads));
248        let results = Arc::new(Mutex::new(Vec::new()));
249
250        let threads: Vec<_> = (0..num_threads)
251            .map(|_| {
252                let mut iter = iter.clone();
253                let counter = Arc::clone(&counter);
254                let barrier = Arc::clone(&barrier);
255                let results = Arc::clone(&results);
256
257                thread::spawn(move || {
258                    barrier.wait();
259                    let mut local_results = Vec::new();
260
261                    for _ in 0..items_per_thread {
262                        if let Some(item) = iter.next() {
263                            counter.fetch_add(1, Ordering::SeqCst);
264                            local_results.push(item);
265                        }
266                    }
267
268                    results.lock().push(local_results);
269                })
270            })
271            .collect();
272
273        for thread in threads {
274            thread.join().unwrap();
275        }
276
277        assert_eq!(counter.load(Ordering::SeqCst), total_items);
278
279        let all_results = results.lock();
280        let mut collected: Vec<_> = all_results.iter().flatten().copied().collect();
281        collected.sort();
282        assert_eq!(collected, (0..total_items).collect::<Vec<_>>());
283    }
284
285    #[test]
286    fn test_block_on_stream_thread_safe_propagates_driver_panic() {
287        let runtime = CurrentThreadRuntime::new();
288        let mut iter = runtime.block_on_stream_thread_safe(|_h| {
289            stream::poll_fn(|_| -> Poll<Option<usize>> {
290                panic!("stream driver panic");
291            })
292            .boxed()
293        });
294
295        let panic = std::panic::catch_unwind(AssertUnwindSafe(|| iter.next()))
296            .expect_err("stream panic must propagate through iterator");
297        let message = panic
298            .downcast_ref::<&'static str>()
299            .copied()
300            .or_else(|| panic.downcast_ref::<String>().map(String::as_str))
301            .unwrap_or("<unknown panic>");
302        assert!(message.contains("stream driver panic"));
303    }
304
305    fn panic_message(panic: &(dyn Any + Send)) -> &str {
306        panic
307            .downcast_ref::<&'static str>()
308            .copied()
309            .or_else(|| panic.downcast_ref::<String>().map(String::as_str))
310            .unwrap_or("<unknown panic>")
311    }
312
313    // A driver panic must propagate on *every* run, regardless of executor scheduling. Running
314    // the scenario many times guards against a return to timing-dependent propagation.
315    #[test]
316    fn test_block_on_stream_thread_safe_panic_propagation_is_deterministic() {
317        for i in 0..2000 {
318            let mut iter = CurrentThreadRuntime::new().block_on_stream_thread_safe(|_h| {
319                stream::poll_fn(|_| -> Poll<Option<usize>> {
320                    panic!("deterministic driver panic");
321                })
322                .boxed()
323            });
324
325            let outcome = std::panic::catch_unwind(AssertUnwindSafe(|| iter.next()));
326            assert!(
327                outcome.is_err(),
328                "driver panic was swallowed on iteration {i}: next() returned {:?}",
329                outcome.ok().flatten(),
330            );
331        }
332    }
333
334    // A panic after some items were already produced must still surface, not be seen as a clean
335    // end of stream.
336    #[test]
337    fn test_block_on_stream_thread_safe_panic_after_items() {
338        let mut emitted = 0usize;
339        let iter = CurrentThreadRuntime::new().block_on_stream_thread_safe(move |_h| {
340            stream::poll_fn(move |_| -> Poll<Option<usize>> {
341                if emitted < 3 {
342                    emitted += 1;
343                    Poll::Ready(Some(emitted))
344                } else {
345                    panic!("driver panic after items");
346                }
347            })
348            .boxed()
349        });
350
351        // Drain the iterator. The terminal event must be a propagated panic, never a clean
352        // `None`. This avoids depending on exactly how many buffered items survive channel close.
353        let outcome = std::panic::catch_unwind(AssertUnwindSafe(move || iter.collect::<Vec<_>>()));
354        match outcome {
355            Ok(items) => panic!("driver panic was swallowed; stream ended cleanly with {items:?}"),
356            Err(panic) => assert!(panic_message(&*panic).contains("driver panic after items")),
357        }
358    }
359
360    // With multiple consumers, a driver panic must reach *every* consumer that observes the end
361    // of the stream; it must never be swallowed by all of them. Exactly one consumer joins the
362    // driver and observes the panic; the rest see the stream end.
363    #[test]
364    fn test_block_on_stream_thread_safe_multi_consumer_panic_surfaced() {
365        let iter = CurrentThreadRuntime::new().block_on_stream_thread_safe(|_h| {
366            stream::poll_fn(|_| -> Poll<Option<usize>> {
367                panic!("multi consumer driver panic");
368            })
369            .boxed()
370        });
371
372        let num_threads = 4;
373        let barrier = Arc::new(Barrier::new(num_threads));
374        let panics = Arc::new(AtomicUsize::new(0));
375
376        let handles: Vec<_> = (0..num_threads)
377            .map(|_| {
378                let mut iter = iter.clone();
379                let barrier = Arc::clone(&barrier);
380                let panics = Arc::clone(&panics);
381                thread::spawn(move || {
382                    barrier.wait();
383                    match std::panic::catch_unwind(AssertUnwindSafe(|| iter.next())) {
384                        // The driver panicked before producing anything, so a clean end is the
385                        // only non-panic outcome a consumer may observe.
386                        Ok(None) => {}
387                        Ok(Some(_)) => panic!("no item was produced before the driver panicked"),
388                        Err(panic) => {
389                            assert!(panic_message(&*panic).contains("multi consumer driver panic"));
390                            panics.fetch_add(1, Ordering::SeqCst);
391                        }
392                    }
393                })
394            })
395            .collect();
396
397        for handle in handles {
398            handle.join().expect("consumer thread panicked uncaught");
399        }
400
401        // The panic surfaces to exactly one consumer and is never swallowed by all of them.
402        assert_eq!(panics.load(Ordering::SeqCst), 1);
403    }
404
405    // Clean completion must return `None` with no spurious panic.
406    #[test]
407    fn test_block_on_stream_thread_safe_clean_completion_returns_none() {
408        let mut iter = CurrentThreadRuntime::new()
409            .block_on_stream_thread_safe(|_h| stream::iter(vec![1usize, 2, 3]).boxed());
410
411        assert_eq!(iter.next(), Some(1));
412        assert_eq!(iter.next(), Some(2));
413        assert_eq!(iter.next(), Some(3));
414        assert_eq!(iter.next(), None);
415        assert_eq!(iter.next(), None);
416    }
417
418    #[test]
419    fn test_block_on_stream_concurrent_clone_and_drive() {
420        let num_items = 50;
421        let num_threads = 3;
422
423        let iter = CurrentThreadRuntime::new().block_on_stream_thread_safe(|h| {
424            stream::unfold(0, move |state| {
425                let h = h.clone();
426                async move {
427                    if state < num_items {
428                        h.spawn_cpu(move || {
429                            thread::sleep(Duration::from_micros(10));
430                            state
431                        })
432                        .await;
433                        Some((state, state + 1))
434                    } else {
435                        None
436                    }
437                }
438            })
439        });
440
441        let collected = Arc::new(Mutex::new(Vec::new()));
442        let barrier = Arc::new(Barrier::new(num_threads));
443
444        let threads: Vec<_> = (0..num_threads)
445            .map(|thread_id| {
446                let iter = iter.clone();
447                let collected = Arc::clone(&collected);
448                let barrier = Arc::clone(&barrier);
449
450                thread::spawn(move || {
451                    barrier.wait();
452                    let mut local_items = Vec::new();
453
454                    for item in iter {
455                        local_items.push((thread_id, item));
456                        if local_items.len() >= 5 {
457                            break;
458                        }
459                    }
460
461                    collected.lock().extend(local_items);
462                })
463            })
464            .collect();
465
466        for thread in threads {
467            thread.join().unwrap();
468        }
469
470        let results = collected.lock();
471        let mut values: Vec<_> = results.iter().map(|(_, v)| *v).collect();
472        values.sort();
473        values.dedup();
474
475        assert!(values.len() >= 5);
476        assert!(values.iter().all(|&v| v < num_items));
477    }
478
479    #[test]
480    fn test_block_on_stream_async_work() {
481        let runtime = CurrentThreadRuntime::new();
482        let handle = runtime.handle();
483        let iter = runtime.block_on_stream({
484            stream::unfold((handle, 0), |(h, state)| async move {
485                if state < 10 {
486                    let value = h
487                        .spawn(async move { futures::future::ready(state * 2).await })
488                        .await;
489                    Some((value, (h, state + 1)))
490                } else {
491                    None
492                }
493            })
494        });
495
496        let results: Vec<_> = iter.collect();
497        assert_eq!(results, vec![0, 2, 4, 6, 8, 10, 12, 14, 16, 18]);
498    }
499
500    #[test]
501    fn test_block_on_stream_drop_receivers_early() {
502        let counter = Arc::new(AtomicUsize::new(0));
503        let c = Arc::clone(&counter);
504
505        let mut iter = CurrentThreadRuntime::new().block_on_stream({
506            stream::unfold(0, move |state| {
507                let c = Arc::clone(&c);
508                async move {
509                    (state < 100).then(|| {
510                        c.fetch_add(1, Ordering::SeqCst);
511                        (state, state + 1)
512                    })
513                }
514            })
515            .boxed()
516        });
517
518        assert_eq!(iter.next(), Some(0));
519        assert_eq!(iter.next(), Some(1));
520        assert_eq!(iter.next(), Some(2));
521
522        drop(iter);
523
524        let final_count = counter.load(Ordering::SeqCst);
525        assert!(
526            final_count < 100,
527            "Stream should stop when all receivers are dropped"
528        );
529    }
530
531    #[test]
532    fn test_block_on_stream_interleaved_access() {
533        let barrier = Arc::new(Barrier::new(2));
534        let iter = CurrentThreadRuntime::new()
535            .block_on_stream_thread_safe(|_h| stream::iter(0..20).boxed());
536
537        let iter1 = iter.clone();
538        let iter2 = iter;
539        let barrier1 = Arc::clone(&barrier);
540        let barrier2 = barrier;
541
542        let thread1 = thread::spawn(move || {
543            let mut iter = iter1;
544            let mut results = Vec::new();
545            barrier1.wait();
546
547            for _ in 0..5 {
548                if let Some(val) = iter.next() {
549                    results.push(val);
550                    thread::sleep(Duration::from_micros(50));
551                }
552            }
553            results
554        });
555
556        let thread2 = thread::spawn(move || {
557            let mut iter = iter2;
558            let mut results = Vec::new();
559            barrier2.wait();
560
561            for _ in 0..5 {
562                if let Some(val) = iter.next() {
563                    results.push(val);
564                    thread::sleep(Duration::from_micros(50));
565                }
566            }
567            results
568        });
569
570        let results1 = thread1.join().unwrap();
571        let results2 = thread2.join().unwrap();
572
573        let mut all_results = results1;
574        all_results.extend(results2);
575        all_results.sort();
576
577        assert_eq!(all_results, (0..10).collect::<Vec<_>>());
578
579        for i in 0..10 {
580            assert_eq!(all_results.iter().filter(|&&x| x == i).count(), 1);
581        }
582    }
583
584    #[test]
585    fn test_block_on_stream_stress_test() {
586        let num_threads = 10;
587        let num_items = 1000;
588
589        let iter = CurrentThreadRuntime::new()
590            .block_on_stream_thread_safe(|_h| stream::iter(0..num_items).boxed());
591
592        let received = Arc::new(Mutex::new(Vec::new()));
593        let barrier = Arc::new(Barrier::new(num_threads));
594
595        let threads: Vec<_> = (0..num_threads)
596            .map(|_| {
597                let iter = iter.clone();
598                let received = Arc::clone(&received);
599                let barrier = Arc::clone(&barrier);
600
601                thread::spawn(move || {
602                    barrier.wait();
603                    for val in iter {
604                        received.lock().push(val);
605                    }
606                })
607            })
608            .collect();
609
610        for thread in threads {
611            thread.join().unwrap();
612        }
613
614        let mut results = received.lock().clone();
615        results.sort();
616
617        assert_eq!(results.len(), num_items);
618        assert_eq!(results, (0..num_items).collect::<Vec<_>>());
619    }
620}