use std::sync::Arc;
use parking_lot::Mutex;
use crate::error::{Error, Result};
pub const RECOMMENDED_MAX_THREADS: usize = 12;
pub fn resolve_parallel(parallel: i64) -> usize {
if parallel > 0 {
return parallel as usize;
}
std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1)
.min(RECOMMENDED_MAX_THREADS)
}
pub struct Executor {
pool: Option<rayon::ThreadPool>,
handles: Arc<Mutex<Vec<std::thread::JoinHandle<()>>>>,
parallel: usize,
}
impl Drop for Executor {
fn drop(&mut self) {
drop(self.pool.take());
for handle in self.handles.lock().drain(..) {
let _ = handle.join();
}
}
}
impl std::fmt::Debug for Executor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Executor")
.field("parallel", &self.parallel)
.finish()
}
}
impl Executor {
pub fn new(parallel: i64) -> Result<Self> {
let parallel = resolve_parallel(parallel);
let handles: Arc<Mutex<Vec<std::thread::JoinHandle<()>>>> =
Arc::new(Mutex::new(Vec::with_capacity(parallel)));
let sink = handles.clone();
let pool = rayon::ThreadPoolBuilder::new()
.num_threads(parallel)
.spawn_handler(move |thread| {
let name = thread
.name()
.map(str::to_string)
.unwrap_or_else(|| format!("gwseq-io-{}", thread.index()));
let handle = std::thread::Builder::new()
.name(name)
.spawn(move || thread.run())?;
sink.lock().push(handle);
Ok(())
})
.thread_name(|i| format!("gwseq-io-{i}"))
.panic_handler(|_| {})
.build()
.map_err(|e| {
Error::io(
format!("could not start {parallel} threads"),
std::io::Error::other(e.to_string()),
)
})?;
Ok(Self {
pool: Some(pool),
handles,
parallel,
})
}
#[cfg(test)]
fn handle_count(&self) -> usize {
self.handles.lock().len()
}
fn pool(&self) -> &rayon::ThreadPool {
self.pool
.as_ref()
.expect("the pool is only taken while the executor is being dropped")
}
pub fn parallel(&self) -> usize {
self.parallel
}
pub fn install<R: Send>(&self, f: impl FnOnce() -> R + Send) -> R {
self.pool().install(f)
}
pub fn spawn(&self, f: impl FnOnce() + Send + 'static) {
self.pool().spawn(f);
}
pub fn for_each_batch<T: Send + Sync>(
&self,
batches: &[T],
f: impl Fn(usize, &T) -> Result<()> + Send + Sync,
) -> Result<()> {
use rayon::prelude::*;
self.install(|| {
batches
.par_iter()
.enumerate()
.try_for_each(|(index, batch)| f(index, batch))
})
}
pub fn map_batches<T: Send, B: Send + Sync>(
&self,
batches: &[B],
f: impl Fn(usize, &B) -> Result<T> + Send + Sync,
) -> Result<Vec<T>> {
use rayon::prelude::*;
self.install(|| {
batches
.par_iter()
.enumerate()
.map(|(index, batch)| f(index, batch))
.collect::<Result<Vec<T>>>()
})
}
}
#[derive(Debug)]
pub struct Promise<T> {
inner: Arc<(Mutex<Option<T>>, parking_lot::Condvar)>,
}
impl<T> Clone for Promise<T> {
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
}
}
}
impl<T> Default for Promise<T> {
fn default() -> Self {
Self::new()
}
}
impl<T> Promise<T> {
pub fn new() -> Self {
Self {
inner: Arc::new((Mutex::new(None), parking_lot::Condvar::new())),
}
}
pub fn set(&self, value: T) {
let mut slot = self.inner.0.lock();
if slot.is_none() {
*slot = Some(value);
self.inner.1.notify_all();
}
}
pub fn wait(self) -> Option<T> {
let mut slot = self.inner.0.lock();
loop {
if let Some(value) = slot.take() {
return Some(value);
}
if Arc::strong_count(&self.inner) <= 1 {
return None;
}
self.inner
.1
.wait_for(&mut slot, std::time::Duration::from_millis(50));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
#[test]
fn resolve_parallel_takes_a_positive_count_as_given() {
assert_eq!(resolve_parallel(1), 1);
assert_eq!(resolve_parallel(7), 7);
assert_eq!(resolve_parallel(100), 100);
}
#[test]
fn zero_or_less_means_one_per_core_capped() {
for asked in [0, -1, -12] {
let n = resolve_parallel(asked);
assert!(
(1..=RECOMMENDED_MAX_THREADS).contains(&n),
"{asked} gave {n}"
);
}
assert_eq!(resolve_parallel(0), resolve_parallel(-1));
}
#[test]
fn every_batch_runs_exactly_once() {
let executor = Executor::new(4).unwrap();
assert_eq!(executor.parallel(), 4);
let batches: Vec<usize> = (0..64).collect();
let seen: Vec<AtomicUsize> = (0..64).map(|_| AtomicUsize::new(0)).collect();
executor
.for_each_batch(&batches, |index, batch| {
assert_eq!(index, *batch);
seen[index].fetch_add(1, Ordering::SeqCst);
Ok(())
})
.unwrap();
assert!(seen.iter().all(|c| c.load(Ordering::SeqCst) == 1));
}
#[test]
fn a_failing_batch_surfaces_as_the_result() {
let executor = Executor::new(4).unwrap();
let batches: Vec<usize> = (0..32).collect();
let err = executor
.for_each_batch(&batches, |_, batch| {
if *batch == 17 {
Err(Error::invalid("batch 17"))
} else {
Ok(())
}
})
.unwrap_err();
assert!(err.to_string().contains("batch 17"));
}
#[test]
fn every_worker_is_held_for_joining_and_the_join_happens_on_drop() {
let executor = Executor::new(4).unwrap();
let batches: Vec<usize> = (0..64).collect();
executor.for_each_batch(&batches, |_, _| Ok(())).unwrap();
assert_eq!(
executor.handle_count(),
4,
"rayon spawned workers this executor did not keep a handle for"
);
drop(executor);
}
#[test]
fn map_batches_keeps_submission_order() {
let executor = Executor::new(4).unwrap();
let batches: Vec<usize> = (0..50).collect();
let out = executor
.map_batches(&batches, |index, batch| Ok(index * 10 + batch))
.unwrap();
assert_eq!(out, (0..50).map(|i| i * 11).collect::<Vec<_>>());
}
#[test]
fn map_batches_surfaces_a_failure() {
let executor = Executor::new(4).unwrap();
let batches: Vec<usize> = (0..32).collect();
let err = executor
.map_batches(&batches, |_, batch| {
if *batch == 5 {
Err(Error::invalid("batch 5"))
} else {
Ok(*batch)
}
})
.unwrap_err();
assert!(err.to_string().contains("batch 5"));
}
#[test]
fn an_empty_request_is_not_an_error() {
let executor = Executor::new(2).unwrap();
let batches: Vec<usize> = Vec::new();
executor.for_each_batch(&batches, |_, _| Ok(())).unwrap();
}
}