Skip to main content

hataori/
local.rs

1use crate::domain::{Domain, DomainBusy, LocalMode};
2use crate::map::{map, MapError};
3use rayon::iter::{IntoParallelIterator, ParallelIterator};
4use std::fmt;
5
6/// Failure from [`map_in`].
7#[derive(Debug)]
8pub enum MapInError {
9    /// The domain has no explicit Rayon pool.
10    MissingPool,
11    /// The call originated on a different Rayon pool.
12    ForeignPool,
13    /// Another operation already owns the domain admission slot.
14    DomainBusy(DomainBusy),
15    /// A callback returned an error.
16    Callback(MapError),
17}
18
19impl fmt::Display for MapInError {
20    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
21        match self {
22            Self::MissingPool => formatter.write_str("domain has no Rayon pool"),
23            Self::ForeignPool => formatter.write_str("map_in called from a foreign Rayon pool"),
24            Self::DomainBusy(error) => error.fmt(formatter),
25            Self::Callback(error) => error.fmt(formatter),
26        }
27    }
28}
29
30impl std::error::Error for MapInError {
31    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
32        match self {
33            Self::DomainBusy(error) => Some(error),
34            Self::Callback(error) => Some(error),
35            Self::MissingPool | Self::ForeignPool => None,
36        }
37    }
38}
39
40impl From<DomainBusy> for MapInError {
41    fn from(error: DomainBusy) -> Self {
42        Self::DomainBusy(error)
43    }
44}
45
46impl From<MapError> for MapInError {
47    fn from(error: MapError) -> Self {
48        Self::Callback(error)
49    }
50}
51
52pub(crate) fn in_rayon_worker_context() -> bool {
53    rayon::current_thread_index().is_some()
54}
55
56pub(crate) fn run_in_current_pool<T, U, E, F>(
57    mode: LocalMode,
58    items: Vec<T>,
59    f: &F,
60) -> Result<Vec<U>, MapError>
61where
62    T: Send,
63    U: Send,
64    E: fmt::Display + Send,
65    F: Fn(T) -> Result<U, E> + Sync,
66{
67    match mode {
68        LocalMode::Sequential | LocalMode::Inner => map(items, f),
69        LocalMode::Outer => {
70            let results: Vec<Result<U, E>> = items.into_par_iter().map(f).collect();
71            map(results, |result| result)
72        }
73    }
74}
75
76pub(crate) fn run_in_pool<T, U, E, F>(
77    pool: &rayon::ThreadPool,
78    mode: LocalMode,
79    items: Vec<T>,
80    f: F,
81) -> Result<Vec<U>, MapError>
82where
83    T: Send,
84    U: Send,
85    E: fmt::Display + Send,
86    F: Fn(T) -> Result<U, E> + Send + Sync,
87{
88    pool.install(|| run_in_current_pool(mode, items, &f))
89}
90
91/// Maps `items` through an explicit Rayon domain.
92///
93/// The target domain must have a pool, the call must not originate in another
94/// Rayon pool, and the domain must not already be admitted. These preconditions
95/// are checked in that order, before `f` is called. The callback and input,
96/// output, and error types must satisfy `Send`/`Sync` as required by Rayon, but
97/// none needs to be `'static`.
98///
99/// [`LocalMode::Sequential`] and [`LocalMode::Inner`] run callbacks one at a
100/// time in the target pool and stop at the first callback error. `Inner` leaves
101/// nested same-pool Rayon work available. [`LocalMode::Outer`] evaluates every
102/// input exactly once in parallel, preserves result order, and reports the
103/// lowest callback-error index only after all callbacks have finished.
104///
105/// # Example
106///
107/// ```
108/// # #[cfg(feature = "rayon")]
109/// # {
110/// use hataori::{map_in, Domain, LocalMode};
111/// use std::sync::Arc;
112/// use rayon::ThreadPoolBuilder;
113///
114/// let pool = Arc::new(ThreadPoolBuilder::new().num_threads(2).build().unwrap());
115/// let domain = Domain::external(Arc::clone(&pool), vec![0, 1], 2).unwrap();
116/// let result = map_in(&domain, LocalMode::Outer, vec![1, 2, 3], |item| {
117///     Ok::<_, std::convert::Infallible>(item * 2)
118/// }).unwrap();
119/// assert_eq!(result, vec![2, 4, 6]);
120/// # }
121/// ```
122pub fn map_in<T, U, E, F>(
123    domain: &Domain,
124    mode: LocalMode,
125    items: Vec<T>,
126    f: F,
127) -> Result<Vec<U>, MapInError>
128where
129    T: Send,
130    U: Send,
131    E: fmt::Display + Send,
132    F: Fn(T) -> Result<U, E> + Send + Sync,
133{
134    let pool = domain.rayon_pool().ok_or(MapInError::MissingPool)?;
135    if in_rayon_worker_context() && pool.current_thread_index().is_none() {
136        return Err(MapInError::ForeignPool);
137    }
138    let _admission = domain.try_admit().map_err(MapInError::DomainBusy)?;
139
140    run_in_pool(pool, mode, items, f).map_err(MapInError::Callback)
141}
142
143#[cfg(test)]
144mod tests {
145    use super::{in_rayon_worker_context, map_in, MapInError};
146    use crate::{Domain, DomainBusy, LocalMode};
147    use rayon::ThreadPoolBuilder;
148    use std::error::Error;
149    use std::panic::{catch_unwind, AssertUnwindSafe};
150    use std::sync::atomic::{AtomicUsize, Ordering};
151    use std::sync::{mpsc, Arc, Once};
152
153    fn domain(worker_count: usize) -> (Domain, Arc<rayon::ThreadPool>) {
154        let pool = Arc::new(
155            ThreadPoolBuilder::new()
156                .num_threads(worker_count)
157                .build()
158                .unwrap(),
159        );
160        let cpus = (0..worker_count).collect();
161        let domain = Domain::external(Arc::clone(&pool), cpus, worker_count).unwrap();
162        (domain, pool)
163    }
164
165    #[test]
166    fn rayon_worker_guard_matches_entry_contexts() {
167        assert!(!in_rayon_worker_context());
168        assert!(!std::thread::spawn(in_rayon_worker_context).join().unwrap());
169
170        let pool = ThreadPoolBuilder::new().num_threads(1).build().unwrap();
171        assert!(pool.install(in_rayon_worker_context));
172        assert!(pool.scope(|_| in_rayon_worker_context()));
173        assert!(!pool.in_place_scope(|_| in_rayon_worker_context()));
174
175        let (sender, receiver) = mpsc::channel();
176        rayon::scope(|scope| {
177            assert!(in_rayon_worker_context());
178            scope.spawn(|_| sender.send(in_rayon_worker_context()).unwrap());
179        });
180        assert!(receiver.recv().unwrap());
181    }
182
183    #[test]
184    fn current_pool_runner_accepts_borrowed_callback() {
185        let (domain, pool) = domain(1);
186        let bias = 10;
187        let values = [1, 2, 3];
188        let items = values.iter().collect::<Vec<_>>();
189        let callback = |value: &i32| Ok::<_, &'static str>(*value + bias);
190        let result =
191            pool.install(|| super::run_in_current_pool(LocalMode::Sequential, items, &callback));
192        assert_eq!(result.unwrap(), vec![11, 12, 13]);
193        drop(domain);
194    }
195
196    #[test]
197    fn all_modes_succeed_in_order() {
198        let (domain, _) = domain(2);
199        for mode in [LocalMode::Sequential, LocalMode::Inner, LocalMode::Outer] {
200            let result = map_in(&domain, mode, vec![1, 2, 3], |item| {
201                Ok::<_, &'static str>(item * 2)
202            })
203            .unwrap();
204            assert_eq!(result, vec![2, 4, 6]);
205        }
206    }
207
208    #[test]
209    fn sequential_and_inner_stop_at_first_error() {
210        for mode in [LocalMode::Sequential, LocalMode::Inner] {
211            let (domain, _) = domain(2);
212            let calls = Arc::new(AtomicUsize::new(0));
213            let seen = Arc::clone(&calls);
214            let error = map_in(&domain, mode, vec![0, 1, 2, 3], move |item| {
215                seen.fetch_add(1, Ordering::Relaxed);
216                if item == 2 {
217                    Err::<usize, _>("callback failed")
218                } else {
219                    Ok(item)
220                }
221            })
222            .unwrap_err();
223            assert!(matches!(error, MapInError::Callback(ref error) if error.index() == 2));
224            assert_eq!(calls.load(Ordering::Relaxed), 3);
225        }
226    }
227
228    #[test]
229    fn outer_evaluates_every_input_and_reports_lowest_error() {
230        let (domain, _) = domain(2);
231        let calls: Arc<Vec<AtomicUsize>> = Arc::new((0..6).map(|_| AtomicUsize::new(0)).collect());
232        let seen = Arc::clone(&calls);
233        let error = map_in(&domain, LocalMode::Outer, (0..6).collect(), move |item| {
234            seen[item].fetch_add(1, Ordering::Relaxed);
235            match item {
236                1 => Err::<usize, _>("first"),
237                4 => Err::<usize, _>("later"),
238                _ => Ok(item),
239            }
240        })
241        .unwrap_err();
242        assert!(matches!(error, MapInError::Callback(ref error) if error.index() == 1));
243        assert_eq!(error.to_string(), "map failed at index 1: first");
244        assert!(calls.iter().all(|count| count.load(Ordering::Relaxed) == 1));
245    }
246
247    #[test]
248    fn preconditions_have_fixed_order_and_never_call_back() {
249        let calls = Arc::new(AtomicUsize::new(0));
250        let sequential = Domain::sequential();
251        let admission = sequential.try_admit().unwrap();
252        let seen = Arc::clone(&calls);
253        assert!(matches!(
254            map_in(&sequential, LocalMode::Sequential, vec![()], move |_| {
255                seen.fetch_add(1, Ordering::Relaxed);
256                Ok::<_, &'static str>(())
257            }),
258            Err(MapInError::MissingPool)
259        ));
260        drop(admission);
261
262        let (domain, pool) = domain(1);
263        let foreign_pool = ThreadPoolBuilder::new().num_threads(1).build().unwrap();
264        let admission = domain.try_admit().unwrap();
265        let seen = Arc::clone(&calls);
266        let foreign_and_busy = foreign_pool.install(|| {
267            map_in(&domain, LocalMode::Sequential, vec![()], move |_| {
268                seen.fetch_add(1, Ordering::Relaxed);
269                Ok::<_, &'static str>(())
270            })
271        });
272        assert!(matches!(foreign_and_busy, Err(MapInError::ForeignPool)));
273        drop(admission);
274
275        let admission = domain.try_admit().unwrap();
276        let seen = Arc::clone(&calls);
277        let busy = pool.install(|| {
278            map_in(&domain, LocalMode::Sequential, vec![()], move |_| {
279                seen.fetch_add(1, Ordering::Relaxed);
280                Ok::<_, &'static str>(())
281            })
282        });
283        assert!(matches!(busy, Err(MapInError::DomainBusy(_))));
284        drop(admission);
285        assert_eq!(calls.load(Ordering::Relaxed), 0);
286    }
287
288    #[test]
289    fn foreign_custom_and_global_pools_are_rejected_but_same_pool_is_accepted() {
290        let (domain, pool) = domain(1);
291        let foreign_pool = ThreadPoolBuilder::new().num_threads(1).build().unwrap();
292        let custom_result = foreign_pool.install(|| {
293            map_in(&domain, LocalMode::Sequential, vec![1], |_| {
294                Ok::<_, &'static str>(1)
295            })
296        });
297        assert!(matches!(custom_result, Err(MapInError::ForeignPool)));
298
299        let same_pool_result = pool.install(|| {
300            map_in(&domain, LocalMode::Sequential, vec![1], |_| {
301                Ok::<_, &'static str>(1)
302            })
303        });
304        assert_eq!(same_pool_result.unwrap(), vec![1]);
305
306        static GLOBAL: Once = Once::new();
307        GLOBAL.call_once(|| {
308            let _ = ThreadPoolBuilder::new().num_threads(2).build_global();
309        });
310        let (sender, receiver) = mpsc::channel();
311        rayon::scope(|scope| {
312            scope.spawn(|_| {
313                sender
314                    .send(map_in(&domain, LocalMode::Sequential, vec![1], |_| {
315                        Ok::<_, &'static str>(1)
316                    }))
317                    .unwrap();
318            });
319        });
320        assert!(matches!(
321            receiver.recv().unwrap(),
322            Err(MapInError::ForeignPool)
323        ));
324    }
325
326    #[test]
327    fn inner_callback_can_use_the_target_pool() {
328        let (domain, _) = domain(2);
329        let result = map_in(&domain, LocalMode::Inner, vec![()], |_| {
330            let (thread_count, in_pool) = rayon::join(rayon::current_num_threads, || {
331                rayon::current_thread_index().is_some()
332            });
333            Ok::<_, &'static str>((thread_count, in_pool))
334        })
335        .unwrap();
336        assert_eq!(result, vec![(2, true)]);
337    }
338
339    #[test]
340    fn admission_is_released_after_success_error_and_unwind() {
341        let (domain, _) = domain(1);
342        map_in(&domain, LocalMode::Sequential, vec![()], |_| {
343            Ok::<_, &'static str>(())
344        })
345        .unwrap();
346        assert!(domain.try_admit().is_ok());
347
348        let error = map_in(&domain, LocalMode::Sequential, vec![()], |_| {
349            Err::<(), _>("callback failed")
350        });
351        assert!(error.is_err());
352        assert!(domain.try_admit().is_ok());
353
354        let result = catch_unwind(AssertUnwindSafe(|| {
355            map_in(
356                &domain,
357                LocalMode::Sequential,
358                vec![()],
359                |_| -> Result<(), &'static str> {
360                    panic!("callback panic");
361                },
362            )
363            .unwrap();
364        }));
365        assert!(result.is_err());
366        assert!(domain.try_admit().is_ok());
367    }
368
369    #[test]
370    fn borrowed_inputs_and_sync_stack_capture_need_no_static_bound() {
371        let (domain, _) = domain(2);
372        let values = [1, 2, 3];
373        let bias = 10;
374        let captured = &bias;
375        let items = values.iter().collect::<Vec<_>>();
376        let result = map_in(&domain, LocalMode::Outer, items, |value| {
377            Ok::<_, &'static str>(*value + *captured)
378        })
379        .unwrap();
380        assert_eq!(result, vec![11, 12, 13]);
381    }
382
383    #[test]
384    fn error_sources_and_conversions_are_typed() {
385        let busy: MapInError = DomainBusy.into();
386        assert!(busy.source().is_some());
387        let map_error = crate::map::map(vec![()], |_| Err::<(), _>("bad")).unwrap_err();
388        let callback: MapInError = map_error.into();
389        assert!(callback.source().is_some());
390    }
391}