Skip to main content

uqa_planner/
parallel.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Rayon-backed parallel split + recombine, plus a branch-level
8//! [`ParallelExecutor`] for independent plan fragments.
9//!
10//! Two parallelism shapes live here:
11//!
12//! * [`run_parallel`] — chunked Vec-in / Vec-out. Used by parallel-
13//!   aware operators (large hash joins, blocking sorts, hash
14//!   aggregates) to fan out per-partition work.
15//! * [`ParallelExecutor`] — runs N independent worker closures
16//!   concurrently and returns their results in input order, allowing the
17//!   operator-tree driver
18//!   can fork independent branches (`Intersect` / `Union` /
19//!   `BayesianEvidenceFusion` / `RobustPositiveEvidencePool` /
20//!   `ProbBoolFusion` children, deep-fusion
21//!   `SignalLayer` signals) without serialising them.
22
23// The browser (emscripten) target runs single-threaded, so the rayon
24// pool is native-only and every parallel site keeps a sequential twin.
25#[cfg(not(target_os = "emscripten"))]
26use rayon::prelude::*;
27use std::sync::atomic::{AtomicBool, Ordering};
28use std::sync::Arc;
29
30/// Split `input` into `num_partitions` chunks, run `worker` over each
31/// chunk in parallel, and concatenate the results in the same order
32/// the chunks were emitted in.
33pub fn run_parallel<T, R, F>(input: Vec<T>, num_partitions: usize, worker: F) -> Vec<R>
34where
35    T: Send,
36    R: Send,
37    F: Fn(Vec<T>) -> Vec<R> + Sync + Send,
38{
39    let parts = if num_partitions == 0 {
40        1
41    } else {
42        num_partitions
43    };
44    let chunk_size = input.len().div_ceil(parts).max(1);
45    let chunks: Vec<Vec<T>> = input
46        .into_iter()
47        .fold(Vec::with_capacity(parts), |mut acc, x| {
48            if acc
49                .last()
50                .map(|c: &Vec<T>| c.len() >= chunk_size)
51                .unwrap_or(true)
52            {
53                acc.push(Vec::with_capacity(chunk_size));
54            }
55            if let Some(chunk) = acc.last_mut() {
56                chunk.push(x);
57            }
58            acc
59        });
60    #[cfg(not(target_os = "emscripten"))]
61    {
62        chunks
63            .into_par_iter()
64            .map(worker)
65            .reduce(Vec::new, |mut a, b| {
66                a.extend(b);
67                a
68            })
69    }
70    #[cfg(target_os = "emscripten")]
71    {
72        chunks.into_iter().map(worker).fold(Vec::new(), |mut a, b| {
73            a.extend(b);
74            a
75        })
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82
83    #[test]
84    fn run_parallel_preserves_total_elements() {
85        let v: Vec<i32> = (0..100).collect();
86        let out = run_parallel(v.clone(), 4, |chunk| {
87            chunk.into_iter().map(|x| x * 2).collect()
88        });
89        assert_eq!(out.len(), v.len());
90        assert_eq!(
91            out.iter().sum::<i32>(),
92            v.iter().map(|x| x * 2).sum::<i32>()
93        );
94    }
95
96    #[test]
97    fn run_parallel_with_zero_partitions_is_safe() {
98        let out = run_parallel(vec![1, 2, 3], 0, |c| c);
99        assert_eq!(out, vec![1, 2, 3]);
100    }
101
102    #[test]
103    fn parallel_executor_returns_results_in_branch_order() {
104        let par = ParallelExecutor::new(4);
105        let workers: Vec<Box<dyn Fn() -> i32 + Send + Sync>> =
106            vec![Box::new(|| 1), Box::new(|| 2), Box::new(|| 3)];
107        let out = par.execute_branches(&workers);
108        assert_eq!(out, vec![1, 2, 3]);
109    }
110
111    #[test]
112    fn parallel_executor_disabled_falls_back_to_sequential() {
113        let par = ParallelExecutor::new(0);
114        assert!(!par.enabled());
115        let workers: Vec<Box<dyn Fn() -> i32 + Send + Sync>> =
116            vec![Box::new(|| 10), Box::new(|| 20)];
117        let out = par.execute_branches(&workers);
118        assert_eq!(out, vec![10, 20]);
119    }
120
121    #[test]
122    fn parallel_executor_below_threshold_skips_pool() {
123        let par = ParallelExecutor::new(4);
124        let workers: Vec<Box<dyn Fn() -> i32 + Send + Sync>> = vec![Box::new(|| 99)];
125        let out = par.execute_branches(&workers);
126        assert_eq!(out, vec![99]);
127    }
128}
129
130// ---------------------------------------------------------------
131// Branch-level parallel executor
132// ---------------------------------------------------------------
133
134/// Default thread-pool size. Setting the runtime value to `0` disables
135/// parallel execution.
136pub const DEFAULT_PARALLEL_WORKERS: usize = 4;
137
138/// Minimum number of branches before parallel dispatch kicks in. Below this,
139/// sequential execution wins on overhead.
140pub const MIN_PARALLEL_BRANCHES: usize = 2;
141
142/// Branch-level parallel executor.
143///
144/// Holds the configured worker count and a "shutdown" flag. Each call
145/// to [`Self::execute_branches`] runs the supplied workers in parallel
146/// (when enabled and above the branching threshold) and collects
147/// their results in input order.
148#[derive(Debug, Clone)]
149pub struct ParallelExecutor {
150    max_workers: usize,
151    shutdown: Arc<AtomicBool>,
152}
153
154impl ParallelExecutor {
155    /// Build an executor with at most `max_workers` concurrent
156    /// branches. `0` disables parallel dispatch (every branch runs
157    /// sequentially).
158    #[must_use]
159    pub fn new(max_workers: usize) -> Self {
160        Self {
161            max_workers,
162            shutdown: Arc::new(AtomicBool::new(false)),
163        }
164    }
165
166    /// Whether the executor will dispatch concurrently.
167    #[must_use]
168    pub fn enabled(&self) -> bool {
169        self.max_workers > 0 && !self.shutdown.load(Ordering::Acquire)
170    }
171
172    /// Mark the executor as shut down. Subsequent
173    /// [`Self::execute_branches`] calls fall back to sequential
174    /// execution.
175    pub fn shutdown(&self) {
176        self.shutdown.store(true, Ordering::Release);
177    }
178
179    /// Run each `worker` and collect results in the same order. Uses
180    /// rayon's work-stealing pool when [`Self::enabled`] is `true` and
181    /// the branch count is at least [`MIN_PARALLEL_BRANCHES`];
182    /// otherwise falls back to sequential execution.
183    pub fn execute_branches<R, F>(&self, workers: &[F]) -> Vec<R>
184    where
185        R: Send,
186        F: Fn() -> R + Sync + Send,
187    {
188        if !self.enabled() || workers.len() < MIN_PARALLEL_BRANCHES {
189            return workers.iter().map(|w| w()).collect();
190        }
191        #[cfg(not(target_os = "emscripten"))]
192        {
193            workers.par_iter().map(|w| w()).collect()
194        }
195        #[cfg(target_os = "emscripten")]
196        {
197            workers.iter().map(|w| w()).collect()
198        }
199    }
200}
201
202impl Default for ParallelExecutor {
203    fn default() -> Self {
204        Self::new(DEFAULT_PARALLEL_WORKERS)
205    }
206}