mod map;
mod policy;
mod pool;
pub(crate) use pool::run_ordered_pool;
pub use map::{
par_map, parallel_worthwhile, try_par_map, try_par_map_force, try_par_map_with_local,
};
pub use policy::{ParallelOp, ParallelOptions, ThreadPolicy};
use std::num::NonZeroUsize;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ParallelHashError<E> {
Task(E),
}
impl<E: std::fmt::Display> std::fmt::Display for ParallelHashError<E> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Task(e) => write!(f, "{e}"),
}
}
}
impl<E: std::fmt::Debug + std::fmt::Display> std::error::Error for ParallelHashError<E> {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Parallelism {
threads: NonZeroUsize,
}
impl Parallelism {
#[must_use]
pub fn new(threads: NonZeroUsize) -> Self {
Self { threads }
}
#[must_use]
pub fn resolve(requested: Option<usize>) -> Self {
let avail = std::thread::available_parallelism()
.map(NonZeroUsize::get)
.unwrap_or(1);
let n = match requested {
None | Some(0) => avail,
Some(n) => n.min(avail),
};
Self {
threads: NonZeroUsize::new(n.max(1)).unwrap_or(NonZeroUsize::MIN),
}
}
#[must_use]
pub fn threads(self) -> NonZeroUsize {
self.threads
}
}
#[must_use]
pub fn index_parallelism_from_config(config: &crate::config::ConfigSet) -> Parallelism {
ThreadPolicy::default()
.resolve(ParallelOp::IndexRefresh, config)
.parallelism()
}
pub fn index_pack_threads_for_ingest(
policy: ThreadPolicy,
config: &crate::config::ConfigSet,
explicit: Option<usize>,
) -> Result<Option<usize>, crate::error::ConfigError> {
if let Some(n) = explicit {
return Ok(Some(n));
}
if policy.threads_override.is_some() {
return Ok(Some(
policy
.try_resolve(ParallelOp::IndexPack, config)?
.threads
.get(),
));
}
Ok(None)
}
#[must_use]
pub fn index_load_lazy_thread_count(
policy: &ThreadPolicy,
config: &crate::config::ConfigSet,
cache_nr: usize,
try_threaded: bool,
) -> usize {
if !try_threaded {
return 0;
}
let opts = policy.resolve(ParallelOp::IndexLoad, config);
if opts.threads.get() <= 1 {
return 0;
}
if cache_nr < 2 * crate::index_name_hash_lazy::LAZY_THREAD_COST {
return 0;
}
let mut nr = opts.threads.get();
if cache_nr < nr * crate::index_name_hash_lazy::LAZY_THREAD_COST {
nr = cache_nr / crate::index_name_hash_lazy::LAZY_THREAD_COST;
}
nr
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
fn threads(n: usize) -> NonZeroUsize {
NonZeroUsize::new(n).expect("non-zero threads")
}
#[test]
fn try_par_map_preserves_order() {
let items: Vec<usize> = (0..256).collect();
let opts = ParallelOptions {
threads: threads(4),
min_items: 0,
min_total_bytes: None,
signed_entry_threshold: None,
};
let got = par_map(&items, opts, items.len(), |&x| x * 2);
let expect: Vec<_> = items.iter().map(|x| x * 2).collect();
assert_eq!(got, expect);
}
#[test]
fn try_par_map_surfaces_task_error() {
let items: Vec<i32> = (0..128).collect();
let opts = ParallelOptions {
threads: threads(4),
min_items: 0,
min_total_bytes: None,
signed_entry_threshold: None,
};
let err = try_par_map(&items, opts, items.len(), |&x| {
if x == 64 {
Err("boom")
} else {
Ok(x)
}
})
.unwrap_err();
assert_eq!(err, ParallelHashError::Task("boom"));
}
#[test]
fn index_pack_threads_for_ingest_honors_override_only() {
let cfg = config_from("[pack]\n\tthreads = 2\n");
assert_eq!(
index_pack_threads_for_ingest(ThreadPolicy::default(), &cfg, None).expect("ingest"),
None
);
assert_eq!(
index_pack_threads_for_ingest(ThreadPolicy::default(), &cfg, Some(3)).expect("ingest"),
Some(3)
);
let policy = ThreadPolicy::with_threads_override(Some(6));
assert_eq!(
index_pack_threads_for_ingest(policy, &cfg, None).expect("ingest"),
Some(policy.resolve(ParallelOp::IndexPack, &cfg).threads.get())
);
}
fn config_from(text: &str) -> crate::config::ConfigSet {
use crate::config::{ConfigFile, ConfigScope, ConfigSet};
use std::path::Path;
let file = ConfigFile::parse(Path::new("test"), text, ConfigScope::Local).expect("parse");
let mut set = ConfigSet::new();
set.merge(&file);
set
}
#[test]
fn try_par_map_with_local_reuses_per_worker_state() {
let items: Vec<i32> = (0..256).collect();
let opts = ParallelOptions {
threads: threads(4),
min_items: 0,
min_total_bytes: None,
signed_entry_threshold: None,
};
let init_count = Arc::new(AtomicUsize::new(0));
let init_count_for_init = Arc::clone(&init_count);
let got = try_par_map_with_local(
&items,
opts,
items.len(),
move || {
init_count_for_init.fetch_add(1, Ordering::SeqCst);
0_i32
},
|state, &x| -> Result<i32, ()> {
*state += 1;
Ok(x + *state)
},
)
.expect("map");
assert_eq!(got.len(), items.len());
let inits = init_count.load(Ordering::SeqCst);
assert!(
inits >= 2,
"expected multiple workers to init state, got {inits}"
);
assert!(
inits <= 4,
"init count should not exceed worker count, got {inits}"
);
}
}