use std::panic::{AssertUnwindSafe, catch_unwind};
use std::sync::{
Arc, Mutex, PoisonError,
atomic::{AtomicBool, AtomicUsize, Ordering},
};
use crossbeam_channel::Sender;
use crate::{collator::Collator, dataset::Dataset, error::Result};
pub(super) type WorkerMsg<B> = (usize, std::thread::Result<Result<B>>);
pub(super) fn process_batch<D, C>(
dataset: &D,
indices: &[usize],
collator: &C,
items_in_parallel: usize,
pool: Option<&rayon::ThreadPool>,
) -> Result<C::Batch>
where
D: Dataset,
C: Collator<D::Item>,
{
let items = match (pool, indices) {
(Some(_), &[index]) if items_in_parallel > 1 => vec![dataset.get(index)?],
(Some(pool), _) if items_in_parallel > 1 => {
fetch_in_lanes(dataset, indices, items_in_parallel, pool)?
}
_ => dataset.get_batch(indices)?,
};
collator.collate(items)
}
fn fetch_in_lanes<D: Dataset>(
dataset: &D,
indices: &[usize],
lanes: usize,
pool: &rayon::ThreadPool,
) -> Result<Vec<D::Item>> {
let next = AtomicUsize::new(0);
let failed = AtomicBool::new(false);
let slots: Vec<Mutex<Option<Result<D::Item>>>> =
indices.iter().map(|_| Mutex::new(None)).collect();
pool.scope(|scope| {
for _ in 0..lanes.min(indices.len()) {
scope.spawn(|_| {
while !failed.load(Ordering::Relaxed) {
let position = next.fetch_add(1, Ordering::Relaxed);
let Some(&index) = indices.get(position) else {
break;
};
let item = dataset.get(index);
if item.is_err() {
failed.store(true, Ordering::Relaxed);
}
*slots[position]
.lock()
.unwrap_or_else(PoisonError::into_inner) = Some(item);
}
});
}
});
let mut items = Vec::with_capacity(indices.len());
for slot in slots {
match slot.into_inner().unwrap_or_else(PoisonError::into_inner) {
Some(Ok(item)) => items.push(item),
Some(Err(e)) => return Err(e),
None => {}
}
}
Ok(items)
}
pub(super) struct Epoch<D, C>
where
D: Dataset,
C: Collator<D::Item>,
{
dataset: Arc<D>,
collator: Arc<C>,
items_in_parallel: usize,
pool: Arc<rayon::ThreadPool>,
chunks: Vec<Vec<usize>>,
next_claim: AtomicUsize,
next_out: AtomicUsize,
window: usize,
tasks: AtomicUsize,
max_tasks: usize,
cancelled: AtomicBool,
result_tx: Sender<WorkerMsg<C::Batch>>,
}
impl<D, C> Epoch<D, C>
where
D: Dataset,
C: Collator<D::Item>,
C::Batch: Send + 'static,
{
#[allow(clippy::too_many_arguments)]
pub(super) fn new(
dataset: Arc<D>,
collator: Arc<C>,
items_in_parallel: usize,
pool: Arc<rayon::ThreadPool>,
chunks: Vec<Vec<usize>>,
window: usize,
max_tasks: usize,
result_tx: Sender<WorkerMsg<C::Batch>>,
) -> Self {
Self {
dataset,
collator,
items_in_parallel,
pool,
chunks,
next_claim: AtomicUsize::new(0),
next_out: AtomicUsize::new(0),
window,
tasks: AtomicUsize::new(0),
max_tasks,
cancelled: AtomicBool::new(false),
result_tx,
}
}
pub(super) fn len(&self) -> usize {
self.chunks.len()
}
fn claimable(&self, claim: usize) -> bool {
!self.cancelled.load(Ordering::SeqCst)
&& claim < self.chunks.len()
&& claim < self.next_out.load(Ordering::SeqCst) + self.window
}
fn claim(&self) -> Option<usize> {
self.next_claim
.try_update(Ordering::SeqCst, Ordering::SeqCst, |claim| {
self.claimable(claim).then_some(claim + 1)
})
.ok()
}
fn reserve_task(&self) -> bool {
self.claimable(self.next_claim.load(Ordering::SeqCst))
&& self
.tasks
.try_update(Ordering::SeqCst, Ordering::SeqCst, |running| {
(running < self.max_tasks).then_some(running + 1)
})
.is_ok()
}
pub(super) fn spawn_tasks(self: &Arc<Self>) {
while self.reserve_task() {
let epoch = Arc::clone(self);
self.pool.spawn(move || epoch.run());
}
}
pub(super) fn advance(self: &Arc<Self>, next_out: usize) {
self.next_out.store(next_out, Ordering::SeqCst);
self.spawn_tasks();
}
pub(super) fn cancel(&self) {
self.cancelled.store(true, Ordering::SeqCst);
}
fn run(&self) {
loop {
while let Some(batch_idx) = self.claim() {
let result = catch_unwind(AssertUnwindSafe(|| {
process_batch(
&*self.dataset,
&self.chunks[batch_idx],
&*self.collator,
self.items_in_parallel,
Some(&self.pool),
)
}));
let panicked = result.is_err();
if self.result_tx.send((batch_idx, result)).is_err() || panicked {
self.cancel();
}
}
self.tasks.fetch_sub(1, Ordering::SeqCst);
if !self.reserve_task() {
return;
}
}
}
}