1mod map;
17mod policy;
18mod pool;
19
20pub(crate) use map::pool_worthwhile;
21pub(crate) use pool::{run_ordered_batches, run_ordered_pool};
22
23pub use map::{
24 par_map, parallel_worthwhile, try_par_map, try_par_map_force, try_par_map_with_local,
25};
26pub use policy::{ParallelOp, ParallelOptions, ThreadPolicy};
27
28use std::num::NonZeroUsize;
29
30#[derive(Debug, Clone, PartialEq, Eq)]
35pub enum ParallelHashError<E> {
36 Task(E),
38}
39
40impl<E: std::fmt::Display> std::fmt::Display for ParallelHashError<E> {
41 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42 match self {
43 Self::Task(e) => write!(f, "{e}"),
44 }
45 }
46}
47
48impl<E: std::fmt::Debug + std::fmt::Display> std::error::Error for ParallelHashError<E> {}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub struct Parallelism {
56 threads: NonZeroUsize,
57}
58
59impl Parallelism {
60 #[must_use]
62 pub fn new(threads: NonZeroUsize) -> Self {
63 Self { threads }
64 }
65
66 #[must_use]
68 pub fn resolve(requested: Option<usize>) -> Self {
69 let avail = std::thread::available_parallelism()
70 .map(NonZeroUsize::get)
71 .unwrap_or(1);
72 let n = match requested {
73 None | Some(0) => avail,
74 Some(n) => n.min(avail),
75 };
76 Self {
77 threads: NonZeroUsize::new(n.max(1)).unwrap_or(NonZeroUsize::MIN),
78 }
79 }
80
81 #[must_use]
83 pub fn threads(self) -> NonZeroUsize {
84 self.threads
85 }
86}
87
88#[must_use]
90pub fn index_parallelism_from_config(config: &crate::config::ConfigSet) -> Parallelism {
91 ThreadPolicy::default()
92 .resolve(ParallelOp::IndexRefresh, config)
93 .parallelism()
94}
95
96pub fn index_pack_threads_for_ingest(
101 policy: ThreadPolicy,
102 config: &crate::config::ConfigSet,
103 explicit: Option<usize>,
104) -> Result<Option<usize>, crate::error::ConfigError> {
105 if let Some(n) = explicit {
106 return Ok(Some(n));
107 }
108 if policy.threads_override.is_some() {
109 return Ok(Some(
110 policy
111 .try_resolve(ParallelOp::IndexPack, config)?
112 .threads
113 .get(),
114 ));
115 }
116 Ok(None)
117}
118
119#[must_use]
121pub fn index_load_lazy_thread_count(
122 policy: &ThreadPolicy,
123 config: &crate::config::ConfigSet,
124 cache_nr: usize,
125 try_threaded: bool,
126) -> usize {
127 if !try_threaded {
128 return 0;
129 }
130 let opts = policy.resolve(ParallelOp::IndexLoad, config);
131 if opts.threads.get() <= 1 {
132 return 0;
133 }
134 if cache_nr < 2 * crate::index_name_hash_lazy::LAZY_THREAD_COST {
135 return 0;
136 }
137 let mut nr = opts.threads.get();
138 if cache_nr < nr * crate::index_name_hash_lazy::LAZY_THREAD_COST {
139 nr = cache_nr / crate::index_name_hash_lazy::LAZY_THREAD_COST;
140 }
141 nr
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147 use std::sync::atomic::{AtomicUsize, Ordering};
148 use std::sync::Arc;
149
150 fn threads(n: usize) -> NonZeroUsize {
151 NonZeroUsize::new(n).expect("non-zero threads")
152 }
153
154 #[test]
155 fn try_par_map_preserves_order() {
156 let items: Vec<usize> = (0..256).collect();
157 let opts = ParallelOptions {
158 threads: threads(4),
159 min_items: 0,
160 min_total_bytes: None,
161 signed_entry_threshold: None,
162 };
163 let got = par_map(&items, opts, items.len(), |&x| x * 2);
164 let expect: Vec<_> = items.iter().map(|x| x * 2).collect();
165 assert_eq!(got, expect);
166 }
167
168 #[test]
169 fn try_par_map_surfaces_task_error() {
170 let items: Vec<i32> = (0..128).collect();
171 let opts = ParallelOptions {
172 threads: threads(4),
173 min_items: 0,
174 min_total_bytes: None,
175 signed_entry_threshold: None,
176 };
177 let err = try_par_map(&items, opts, items.len(), |&x| {
178 if x == 64 {
179 Err("boom")
180 } else {
181 Ok(x)
182 }
183 })
184 .unwrap_err();
185 assert_eq!(err, ParallelHashError::Task("boom"));
186 }
187
188 #[test]
189 fn ordered_batches_consume_in_item_order_with_worker_state() {
190 let items: Vec<usize> = (0..1000).collect();
191 let inits = AtomicUsize::new(0);
192 let mut seen = Vec::new();
193 let mut batch_lens = Vec::new();
194 run_ordered_batches(
195 &items,
196 7,
197 4,
198 || {
199 inits.fetch_add(1, Ordering::Relaxed);
200 0usize
201 },
202 |mapped: &mut usize, &x| -> Result<usize, ()> {
203 *mapped += 1;
204 Ok(x * 3)
205 },
206 |batch, results| {
207 assert_eq!(batch.len(), results.len());
208 batch_lens.push(batch.len());
209 seen.extend(results);
210 Ok(())
211 },
212 )
213 .expect("batches");
214 assert_eq!(seen, items.iter().map(|x| x * 3).collect::<Vec<_>>());
215 assert_eq!(batch_lens.iter().sum::<usize>(), items.len());
216 assert!(batch_lens[..batch_lens.len() - 1].iter().all(|&n| n == 7));
217 assert_eq!(
218 inits.load(Ordering::Relaxed),
219 4,
220 "init runs once per worker"
221 );
222 }
223
224 #[test]
225 fn ordered_batches_surface_map_and_consume_errors() {
226 let items: Vec<i32> = (0..500).collect();
227 let err = run_ordered_batches(
228 &items,
229 10,
230 3,
231 || (),
232 |_, &x| if x == 250 { Err("map") } else { Ok(x) },
233 |_, _| Ok(()),
234 )
235 .unwrap_err();
236 assert_eq!(err, "map");
237
238 let mut consumed = 0;
239 let err = run_ordered_batches(
240 &items,
241 10,
242 3,
243 || (),
244 |_, &x| Ok::<_, &str>(x),
245 |_, _| {
246 consumed += 1;
247 if consumed == 2 {
248 Err("consume")
249 } else {
250 Ok(())
251 }
252 },
253 )
254 .unwrap_err();
255 assert_eq!(err, "consume");
256 assert_eq!(consumed, 2, "no batch is consumed after an error");
257
258 run_ordered_batches(
259 &[] as &[i32],
260 10,
261 3,
262 || (),
263 |_, &x| Ok::<_, ()>(x),
264 |_, _| panic!("nothing to consume"),
265 )
266 .expect("empty input");
267 }
268
269 #[test]
270 fn ordered_batches_resume_worker_panics() {
271 let items: Vec<i32> = (0..200).collect();
272 let outcome = std::panic::catch_unwind(|| {
273 run_ordered_batches(
274 &items,
275 5,
276 4,
277 || (),
278 |_, &x| -> Result<i32, ()> {
279 assert!(x != 120, "worker boom");
280 Ok(x)
281 },
282 |_, _| Ok(()),
283 )
284 });
285 assert!(outcome.is_err());
286 }
287
288 #[test]
289 fn index_pack_threads_for_ingest_honors_override_only() {
290 let cfg = config_from("[pack]\n\tthreads = 2\n");
291 assert_eq!(
292 index_pack_threads_for_ingest(ThreadPolicy::default(), &cfg, None).expect("ingest"),
293 None
294 );
295 assert_eq!(
296 index_pack_threads_for_ingest(ThreadPolicy::default(), &cfg, Some(3)).expect("ingest"),
297 Some(3)
298 );
299 let policy = ThreadPolicy::with_threads_override(Some(6));
300 assert_eq!(
301 index_pack_threads_for_ingest(policy, &cfg, None).expect("ingest"),
302 Some(policy.resolve(ParallelOp::IndexPack, &cfg).threads.get())
303 );
304 }
305
306 fn config_from(text: &str) -> crate::config::ConfigSet {
307 use crate::config::{ConfigFile, ConfigScope, ConfigSet};
308 use std::path::Path;
309 let file = ConfigFile::parse(Path::new("test"), text, ConfigScope::Local).expect("parse");
310 let mut set = ConfigSet::new();
311 set.merge(&file);
312 set
313 }
314
315 #[test]
316 fn try_par_map_with_local_reuses_per_worker_state() {
317 let items: Vec<i32> = (0..256).collect();
318 let opts = ParallelOptions {
319 threads: threads(4),
320 min_items: 0,
321 min_total_bytes: None,
322 signed_entry_threshold: None,
323 };
324 let init_count = Arc::new(AtomicUsize::new(0));
325 let init_count_for_init = Arc::clone(&init_count);
326 let got = try_par_map_with_local(
327 &items,
328 opts,
329 items.len(),
330 move || {
331 init_count_for_init.fetch_add(1, Ordering::SeqCst);
332 0_i32
333 },
334 |state, &x| -> Result<i32, ()> {
335 *state += 1;
336 Ok(x + *state)
337 },
338 )
339 .expect("map");
340 assert_eq!(got.len(), items.len());
341 let inits = init_count.load(Ordering::SeqCst);
342 assert!(
343 inits >= 2,
344 "expected multiple workers to init state, got {inits}"
345 );
346 assert!(
347 inits <= 4,
348 "init count should not exceed worker count, got {inits}"
349 );
350 }
351}