Skip to main content

objstore_r/
join.rs

1// Author: spicyzhug@gmail.com
2// Copyright: lituus-io, all rights reserved
3//
4//! Executor-free concurrency: [`join_all`] drives many futures on the
5//! caller's task, and [`spin_on`] runs a future that never waits on real I/O
6//! (tests over in-memory stores and scripted transports). Runtime-driven
7//! I/O from a synchronous `main` is `http::block_on` (feature `blocking`).
8//!
9//! [`join_all`] in detail:
10//!
11//! No spawning, no `Arc`, no runtime dependency: the futures may borrow from
12//! the caller (a session, its buffers) because they never leave its task. Each
13//! future is polled only when its own waker fired, so with k model streams a
14//! chunk arriving on one stream does not re-poll the other k-1.
15
16use std::future::Future;
17use std::pin::Pin;
18use std::sync::atomic::{AtomicBool, Ordering};
19use std::sync::Arc;
20use std::task::{Context, Poll, Wake, Waker};
21
22/// Per-slot wake flag that forwards to the task's waker. (`Arc` here is the
23/// std `Wake` contract for building a `Waker`, not shared ownership of data.)
24struct SlotWaker {
25    ready: AtomicBool,
26    parent: std::sync::Mutex<Option<Waker>>,
27}
28
29impl Wake for SlotWaker {
30    fn wake(self: Arc<Self>) {
31        self.wake_by_ref();
32    }
33    fn wake_by_ref(self: &Arc<Self>) {
34        self.ready.store(true, Ordering::Release);
35        if let Some(w) =
36            self.parent.lock().unwrap_or_else(std::sync::PoisonError::into_inner).as_ref()
37        {
38            w.wake_by_ref();
39        }
40    }
41}
42
43struct Slot<F: Future> {
44    fut: Option<Pin<Box<F>>>,
45    out: Option<F::Output>,
46    flag: Arc<SlotWaker>,
47    waker: Waker,
48}
49
50/// Future returned by [`join_all`].
51pub struct JoinAll<F: Future> {
52    slots: Vec<Slot<F>>,
53    pending: usize,
54}
55
56impl<F: Future> std::fmt::Debug for JoinAll<F> {
57    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58        f.debug_struct("JoinAll")
59            .field("len", &self.slots.len())
60            .field("pending", &self.pending)
61            .finish()
62    }
63}
64
65/// Run every future to completion concurrently; outputs keep input order.
66/// Dropping the returned future drops (cancels) every unfinished one.
67pub fn join_all<F: Future>(futures: impl IntoIterator<Item = F>) -> JoinAll<F> {
68    let slots: Vec<Slot<F>> = futures
69        .into_iter()
70        .map(|f| {
71            let flag = Arc::new(SlotWaker {
72                ready: AtomicBool::new(true),
73                parent: std::sync::Mutex::new(None),
74            });
75            let waker = Waker::from(Arc::clone(&flag));
76            Slot { fut: Some(Box::pin(f)), out: None, flag, waker }
77        })
78        .collect();
79    let pending = slots.len();
80    JoinAll { slots, pending }
81}
82
83impl<F: Future> Unpin for JoinAll<F> {}
84
85impl<F: Future> Future for JoinAll<F> {
86    type Output = Vec<F::Output>;
87
88    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
89        let this = &mut *self;
90        for slot in &mut this.slots {
91            let Some(fut) = slot.fut.as_mut() else { continue };
92            {
93                let mut parent =
94                    slot.flag.parent.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
95                if parent.as_ref().is_none_or(|w| !w.will_wake(cx.waker())) {
96                    *parent = Some(cx.waker().clone());
97                }
98            }
99            if !slot.flag.ready.swap(false, Ordering::AcqRel) {
100                continue;
101            }
102            if let Poll::Ready(v) = fut.as_mut().poll(&mut Context::from_waker(&slot.waker)) {
103                slot.out = Some(v);
104                slot.fut = None;
105                this.pending -= 1;
106            }
107        }
108        if this.pending == 0 {
109            Poll::Ready(this.slots.iter_mut().filter_map(|s| s.out.take()).collect())
110        } else {
111            Poll::Pending
112        }
113    }
114}
115
116/// Drive `f` to completion on the calling thread by polling it with a no-op
117/// waker. For futures that make progress on every poll — test doubles,
118/// in-memory stores, scripted transports; a future that waits on real I/O
119/// would spin. Production callers bring their own executor.
120pub fn spin_on<F: Future>(f: F) -> F::Output {
121    let mut cx = Context::from_waker(Waker::noop());
122    let mut f = std::pin::pin!(f);
123    loop {
124        if let Poll::Ready(v) = f.as_mut().poll(&mut cx) {
125            return v;
126        }
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133    use std::cell::Cell;
134
135    /// Minimal single-threaded executor for tests.
136    /// Yields `n` times (waking itself) before completing with `v`.
137    struct YieldN(u32, u32);
138    impl Future for YieldN {
139        type Output = u32;
140        fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<u32> {
141            if self.0 == 0 {
142                Poll::Ready(self.1)
143            } else {
144                self.0 -= 1;
145                cx.waker().wake_by_ref();
146                Poll::Pending
147            }
148        }
149    }
150
151    #[test]
152    fn outputs_keep_input_order() {
153        let out = spin_on(join_all([YieldN(5, 1), YieldN(0, 2), YieldN(2, 3)]));
154        assert_eq!(out, vec![1, 2, 3]);
155    }
156
157    #[test]
158    fn empty_input_is_ready() {
159        let out: Vec<u32> = spin_on(join_all(Vec::<YieldN>::new()));
160        assert!(out.is_empty());
161    }
162
163    #[test]
164    fn futures_may_borrow_caller_state() {
165        let counter = Cell::new(0);
166        let futs = (0..4).map(|i| {
167            let c = &counter;
168            async move {
169                c.set(c.get() + i);
170                i * 2
171            }
172        });
173        let out = spin_on(join_all(futs));
174        assert_eq!(out, vec![0, 2, 4, 6]);
175        assert_eq!(counter.get(), 6);
176    }
177
178    #[test]
179    fn only_woken_futures_are_polled() {
180        struct Counted<'a>(&'a Cell<u32>, bool);
181        impl Future for Counted<'_> {
182            type Output = ();
183            fn poll(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<()> {
184                self.0.set(self.0.get() + 1);
185                if self.1 {
186                    Poll::Ready(())
187                } else {
188                    self.1 = true; // never wakes itself: must not be re-polled
189                    Poll::Pending
190                }
191            }
192        }
193        let polls = Cell::new(0);
194        let mut j = join_all([Counted(&polls, false)]);
195        let waker = Waker::from(Arc::new(SlotWaker {
196            ready: AtomicBool::new(false),
197            parent: std::sync::Mutex::new(None),
198        }));
199        let mut cx = Context::from_waker(&waker);
200        assert!(Pin::new(&mut j).poll(&mut cx).is_pending());
201        assert!(Pin::new(&mut j).poll(&mut cx).is_pending());
202        assert_eq!(polls.get(), 1, "a future that did not wake is not polled again");
203    }
204}