Skip to main content

reth_tasks/
for_each_ordered.rs

1use crossbeam_utils::CachePadded;
2use parking_lot::{Condvar, Mutex};
3use rayon::iter::{IndexedParallelIterator, ParallelIterator};
4use std::{
5    panic::{catch_unwind, AssertUnwindSafe},
6    sync::atomic::{AtomicBool, Ordering},
7};
8
9/// Extension trait for [`IndexedParallelIterator`]
10/// that streams results to a sequential consumer in index order.
11pub trait ForEachOrdered: IndexedParallelIterator {
12    /// Executes the parallel iterator, calling `f` on each result **sequentially in index
13    /// order**.
14    ///
15    /// Items are computed in parallel, but `f` is invoked as `f(item_0)`, `f(item_1)`, …,
16    /// `f(item_{n-1})` on the calling thread. The calling thread receives each item as soon
17    /// as it (and all preceding items) are ready.
18    ///
19    /// `f` does **not** need to be [`Send`] — it runs exclusively on the calling thread.
20    ///
21    /// # Blocking
22    ///
23    /// The calling thread blocks (via [`Condvar`]) while waiting for the next item to become
24    /// ready. It does **not** participate in rayon's work-stealing while blocked. Callers
25    /// should invoke this from a dedicated blocking thread (e.g. via
26    /// [`tokio::task::spawn_blocking`]) rather than from within the rayon thread pool.
27    fn for_each_ordered<F>(self, f: F)
28    where
29        Self::Item: Send,
30        F: FnMut(Self::Item);
31
32    /// Like [`for_each_ordered`](Self::for_each_ordered), but runs the parallel work on the
33    /// given `pool` instead of the global rayon thread pool.
34    fn for_each_ordered_in<F>(self, pool: &rayon::ThreadPool, f: F)
35    where
36        Self::Item: Send,
37        F: FnMut(Self::Item);
38}
39
40impl<I: IndexedParallelIterator> ForEachOrdered for I {
41    fn for_each_ordered<F>(self, f: F)
42    where
43        Self::Item: Send,
44        F: FnMut(Self::Item),
45    {
46        ordered_impl(self, None, f);
47    }
48
49    fn for_each_ordered_in<F>(self, pool: &rayon::ThreadPool, f: F)
50    where
51        Self::Item: Send,
52        F: FnMut(Self::Item),
53    {
54        ordered_impl(self, Some(pool), f);
55    }
56}
57
58/// A slot holding an optional value and a condvar for notification.
59struct Slot<T> {
60    value: Mutex<Option<T>>,
61    notify: Condvar,
62}
63
64impl<T> Slot<T> {
65    const fn new() -> Self {
66        Self { value: Mutex::new(None), notify: Condvar::new() }
67    }
68}
69
70struct Shared<T> {
71    slots: Box<[CachePadded<Slot<T>>]>,
72    panicked: AtomicBool,
73}
74
75impl<T> Shared<T> {
76    fn new(n: usize) -> Self {
77        let slots =
78            (0..n).map(|_| CachePadded::new(Slot::new())).collect::<Vec<_>>().into_boxed_slice();
79        Self { slots, panicked: AtomicBool::new(false) }
80    }
81
82    /// Writes a value into slot `i`. Must only be called once per index.
83    #[inline]
84    fn write(&self, i: usize, val: T) {
85        let slot = &self.slots[i];
86        *slot.value.lock() = Some(val);
87        slot.notify.notify_one();
88    }
89
90    /// Blocks until slot `i` is ready and takes the value.
91    /// Returns `None` if the producer panicked.
92    fn take(&self, i: usize) -> Option<T> {
93        let slot = &self.slots[i];
94        let mut guard = slot.value.lock();
95        loop {
96            if let Some(val) = guard.take() {
97                return Some(val);
98            }
99            if self.panicked.load(Ordering::Acquire) {
100                return None;
101            }
102            slot.notify.wait(&mut guard);
103        }
104    }
105}
106
107/// Executes a parallel iterator and delivers results to a sequential callback in index order.
108///
109/// Each slot has its own [`Condvar`], so the consumer blocks precisely on the slot it needs
110/// with zero spurious wakeups.
111fn ordered_impl<I, F>(iter: I, pool: Option<&rayon::ThreadPool>, mut f: F)
112where
113    I: IndexedParallelIterator,
114    I::Item: Send,
115    F: FnMut(I::Item),
116{
117    let n = iter.len();
118    if n == 0 {
119        return;
120    }
121
122    let shared = Shared::<I::Item>::new(n);
123
124    in_place_scope_in(pool, |s| {
125        // Producer: compute items in parallel and write them into their slots.
126        s.spawn(|_| {
127            let res = catch_unwind(AssertUnwindSafe(|| {
128                iter.enumerate().for_each(|(i, item)| {
129                    shared.write(i, item);
130                });
131            }));
132            if let Err(payload) = res {
133                shared.panicked.store(true, Ordering::Release);
134                // Wake all slots so the consumer doesn't hang. Lock each slot's mutex
135                // first to serialize with the consumer's panicked check → wait sequence.
136                for slot in &*shared.slots {
137                    let _guard = slot.value.lock();
138                    slot.notify.notify_one();
139                }
140                std::panic::resume_unwind(payload);
141            }
142        });
143
144        // Consumer: sequential, ordered, on the calling thread.
145        for i in 0..n {
146            let Some(value) = shared.take(i) else {
147                return;
148            };
149            f(value);
150        }
151    });
152}
153
154fn in_place_scope_in<'scope, F, R>(pool: Option<&rayon::ThreadPool>, f: F)
155where
156    F: FnOnce(&rayon::Scope<'scope>) -> R,
157{
158    if let Some(pool) = pool {
159        pool.in_place_scope(f);
160    } else {
161        rayon::in_place_scope(f);
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168    use rayon::prelude::*;
169    use std::{
170        rc::Rc,
171        sync::{
172            atomic::{AtomicUsize, Ordering},
173            Barrier,
174        },
175    };
176
177    #[test]
178    fn preserves_order() {
179        let input: Vec<u64> = (0..1000).collect();
180        let mut output = Vec::with_capacity(input.len());
181        input.par_iter().map(|x| x * 2).for_each_ordered(|x| output.push(x));
182        let expected: Vec<u64> = (0..1000).map(|x| x * 2).collect();
183        assert_eq!(output, expected);
184    }
185
186    #[test]
187    fn empty_iterator() {
188        let input: Vec<u64> = vec![];
189        let mut output = Vec::new();
190        input.par_iter().map(|x| *x).for_each_ordered(|x| output.push(x));
191        assert!(output.is_empty());
192    }
193
194    #[test]
195    fn single_element() {
196        let mut output = Vec::new();
197        vec![42u64].par_iter().map(|x| *x).for_each_ordered(|x| output.push(x));
198        assert_eq!(output, vec![42]);
199    }
200
201    #[test]
202    fn slow_early_items_still_delivered_in_order() {
203        let barrier = Barrier::new(2);
204        let n = 64usize;
205        let input: Vec<usize> = (0..n).collect();
206        let mut output = Vec::with_capacity(n);
207
208        input
209            .par_iter()
210            .map(|&i| {
211                if i == 0 || i == n - 1 {
212                    barrier.wait();
213                }
214                i
215            })
216            .for_each_ordered(|x| output.push(x));
217
218        assert_eq!(output, input);
219    }
220
221    #[test]
222    fn drops_unconsumed_slots_on_panic() {
223        static DROP_COUNT: AtomicUsize = AtomicUsize::new(0);
224
225        #[derive(Clone)]
226        struct Tracked(#[allow(dead_code)] u64);
227        impl Drop for Tracked {
228            fn drop(&mut self) {
229                DROP_COUNT.fetch_add(1, Ordering::Relaxed);
230            }
231        }
232
233        DROP_COUNT.store(0, Ordering::Relaxed);
234
235        let input: Vec<u64> = (0..100).collect();
236        let result = std::panic::catch_unwind(|| {
237            input
238                .par_iter()
239                .map(|&i| {
240                    assert!(i != 50, "intentional");
241                    Tracked(i)
242                })
243                .for_each_ordered(|_item| {});
244        });
245
246        assert!(result.is_err());
247        let drops = DROP_COUNT.load(Ordering::Relaxed);
248        assert!(drops > 0, "some items should have been dropped");
249    }
250
251    #[test]
252    fn no_double_drop() {
253        static DROP_COUNT: AtomicUsize = AtomicUsize::new(0);
254
255        struct Counted(#[allow(dead_code)] u64);
256        impl Drop for Counted {
257            fn drop(&mut self) {
258                DROP_COUNT.fetch_add(1, Ordering::Relaxed);
259            }
260        }
261
262        DROP_COUNT.store(0, Ordering::Relaxed);
263        let n = 200u64;
264        let input: Vec<u64> = (0..n).collect();
265        input.par_iter().map(|&i| Counted(i)).for_each_ordered(|_item| {});
266
267        assert_eq!(DROP_COUNT.load(Ordering::Relaxed), n as usize);
268    }
269
270    #[test]
271    fn callback_is_not_send() {
272        let counter = Rc::new(std::cell::Cell::new(0u64));
273        let input: Vec<u64> = (0..100).collect();
274        input.par_iter().map(|&x| x).for_each_ordered(|x| {
275            counter.set(counter.get() + x);
276        });
277        assert_eq!(counter.get(), (0..100u64).sum::<u64>());
278    }
279
280    #[test]
281    fn panic_does_not_deadlock_consumer() {
282        // Regression test: producer panics while consumer is waiting on a condvar.
283        // Without the lock-before-notify fix, the consumer could miss the wakeup
284        // and deadlock.
285        for _ in 0..100 {
286            let result = std::panic::catch_unwind(|| {
287                let input: Vec<usize> = (0..256).collect();
288                input
289                    .par_iter()
290                    .map(|&i| {
291                        if i == 128 {
292                            // Yield to increase the chance of the consumer being
293                            // between the panicked check and condvar wait.
294                            std::thread::yield_now();
295                            panic!("intentional");
296                        }
297                        i
298                    })
299                    .for_each_ordered(|_| {});
300            });
301            assert!(result.is_err());
302        }
303    }
304
305    #[test]
306    fn early_panic_at_item_zero() {
307        let result = std::panic::catch_unwind(|| {
308            let input: Vec<u64> = (0..10).collect();
309            input
310                .par_iter()
311                .map(|&i| {
312                    assert!(i != 0, "boom at zero");
313                    i
314                })
315                .for_each_ordered(|_| {});
316        });
317        assert!(result.is_err());
318    }
319
320    #[test]
321    fn late_panic_at_last_item() {
322        let n = 100usize;
323        let result = std::panic::catch_unwind(|| {
324            let input: Vec<usize> = (0..n).collect();
325            input
326                .par_iter()
327                .map(|&i| {
328                    assert!(i != n - 1, "boom at last");
329                    i
330                })
331                .for_each_ordered(|_| {});
332        });
333        assert!(result.is_err());
334    }
335
336    #[test]
337    fn large_items() {
338        let n = 500usize;
339        let input: Vec<usize> = (0..n).collect();
340        let mut output = Vec::with_capacity(n);
341        input
342            .par_iter()
343            .map(|&i| {
344                // Return a heap-allocated value to stress drop semantics.
345                vec![i; 64]
346            })
347            .for_each_ordered(|v| output.push(v[0]));
348        assert_eq!(output, input);
349    }
350
351    #[test]
352    fn consumer_slower_than_producer() {
353        // Producer is fast, consumer is slow. All items should still arrive in order.
354        let n = 64usize;
355        let input: Vec<usize> = (0..n).collect();
356        let mut output = Vec::with_capacity(n);
357        input.par_iter().map(|&i| i).for_each_ordered(|x| {
358            if x % 8 == 0 {
359                std::thread::yield_now();
360            }
361            output.push(x);
362        });
363        assert_eq!(output, input);
364    }
365
366    #[test]
367    fn concurrent_panic_and_drop_no_leak() {
368        // Ensure items produced before a panic are all dropped.
369        static DROP_COUNT: AtomicUsize = AtomicUsize::new(0);
370        static PRODUCED: AtomicUsize = AtomicUsize::new(0);
371
372        struct Tracked;
373        impl Drop for Tracked {
374            fn drop(&mut self) {
375                DROP_COUNT.fetch_add(1, Ordering::Relaxed);
376            }
377        }
378
379        DROP_COUNT.store(0, Ordering::Relaxed);
380        PRODUCED.store(0, Ordering::Relaxed);
381
382        let barrier = Barrier::new(2);
383        let result = std::panic::catch_unwind(|| {
384            let input: Vec<usize> = (0..64).collect();
385            input
386                .par_iter()
387                .map(|&i| {
388                    if i == 32 {
389                        barrier.wait();
390                        panic!("intentional");
391                    }
392                    if i == 0 {
393                        barrier.wait();
394                    }
395                    PRODUCED.fetch_add(1, Ordering::Relaxed);
396                    Tracked
397                })
398                .for_each_ordered(|_| {});
399        });
400
401        assert!(result.is_err());
402        let produced = PRODUCED.load(Ordering::Relaxed);
403        let dropped = DROP_COUNT.load(Ordering::Relaxed);
404        assert_eq!(dropped, produced, "all produced items must be dropped");
405    }
406}