use std::convert::Infallible;
use std::sync::Arc;
#[cfg(feature = "python")]
use std::time::{Duration, Instant};
#[cfg(feature = "python")]
use crossbeam_channel::RecvTimeoutError;
use crossbeam_channel::{Receiver, Sender, TryRecvError, bounded, unbounded};
use super::worker::{Epoch, WorkerMsg};
use crate::{
collator::Collator, dataset::Dataset, error::Result, loader::iter::forked, transform::Transform,
};
pub struct ParallelCore<D, T, C>
where
D: Dataset,
T: Transform<D::Item>,
C: Collator<T::Output>,
{
epoch: Arc<Epoch<D, T, C>>,
result_rx: Receiver<WorkerMsg<C::Batch>>,
reorder: Vec<Option<Result<C::Batch>>>,
next_out: usize,
pid: u32,
alive: Option<Sender<Infallible>>,
#[cfg_attr(not(feature = "python"), allow(dead_code))]
exited: Receiver<Infallible>,
}
impl<D, T, C> ParallelCore<D, T, C>
where
D: Dataset,
T: Transform<D::Item>,
C: Collator<T::Output>,
C::Batch: Send + 'static,
{
#[allow(clippy::too_many_arguments)]
pub(super) fn new(
dataset: &Arc<D>,
transform: &Arc<T>,
collator: &Arc<C>,
pool: &Arc<rayon::ThreadPool>,
item_concurrency: usize,
num_workers: usize,
prefetch_depth: usize,
chunks: Vec<Vec<usize>>,
) -> Self {
let (result_tx, result_rx) = unbounded();
let window = num_workers
.saturating_add(prefetch_depth)
.min(chunks.len())
.max(1);
let epoch = Arc::new(Epoch::new(
Arc::clone(dataset),
Arc::clone(transform),
Arc::clone(collator),
item_concurrency,
Arc::clone(pool),
chunks,
window,
num_workers,
result_tx,
));
let (alive, exited) = bounded(0);
epoch.spawn_tasks(&alive);
Self {
epoch,
result_rx,
reorder: std::iter::repeat_with(|| None).take(window).collect(),
next_out: 0,
pid: std::process::id(),
alive: Some(alive),
exited,
}
}
#[cfg(feature = "python")]
pub(super) fn stop(&mut self, timeout: Duration) -> bool {
self.epoch.cancel();
self.next_out = self.epoch.len();
self.alive = None;
self.pid == std::process::id()
&& matches!(
self.exited.recv_timeout(timeout),
Err(RecvTimeoutError::Disconnected)
)
}
fn check_forked(&mut self) -> Result<()> {
if self.pid == std::process::id() {
return Ok(());
}
self.next_out = self.epoch.len();
Err(forked())
}
pub(super) fn next(&mut self) -> Option<Result<C::Batch>> {
if self.next_out == self.epoch.len() {
return None;
}
let window = self.reorder.len();
loop {
if let Some(batch) = self.reorder[self.next_out % window].take() {
self.next_out += 1;
if let Some(alive) = &self.alive {
self.epoch.advance(self.next_out, alive);
}
return Some(batch);
}
let msg = match self.result_rx.try_recv() {
Ok(msg) => msg,
Err(TryRecvError::Empty) => {
if let Err(e) = self.check_forked() {
return Some(Err(e));
}
match self.result_rx.recv() {
Ok(msg) => msg,
Err(_) => return None,
}
}
Err(TryRecvError::Disconnected) => return None,
};
self.store(msg);
}
}
#[cfg(feature = "python")]
pub(super) fn next_until(
&mut self,
deadline: Option<Instant>,
) -> Option<Option<Result<C::Batch>>> {
if self.next_out == self.epoch.len() {
return Some(None);
}
let window = self.reorder.len();
while self.reorder[self.next_out % window].is_none() {
let msg = match deadline {
None => self.result_rx.try_recv().ok()?,
Some(deadline) => match self.check_forked() {
Err(e) => return Some(Some(Err(e))),
Ok(()) => match self.result_rx.recv_deadline(deadline) {
Ok(msg) => msg,
Err(RecvTimeoutError::Timeout) => return None,
Err(RecvTimeoutError::Disconnected) => return Some(None),
},
},
};
self.store(msg);
}
Some(self.next())
}
fn store(&mut self, (idx, result): WorkerMsg<C::Batch>) {
match result {
Ok(batch) => {
let window = self.reorder.len();
self.reorder[idx % window] = Some(batch);
}
Err(payload) => {
self.next_out = self.epoch.len();
std::panic::resume_unwind(payload);
}
}
}
pub(super) fn len(&self) -> usize {
self.epoch.len() - self.next_out
}
}
impl<D, T, C> Drop for ParallelCore<D, T, C>
where
D: Dataset,
T: Transform<D::Item>,
C: Collator<T::Output>,
{
fn drop(&mut self) {
self.epoch.cancel();
}
}