use crossbeam_channel::{Receiver, RecvTimeoutError, Sender, TryRecvError, bounded, unbounded};
use std::any::Any;
use std::collections::HashSet;
use std::panic::{self, AssertUnwindSafe};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Condvar, Mutex, MutexGuard, Weak};
use std::thread::{self, JoinHandle};
use std::time::{Duration, Instant};
use thiserror::Error;
const DEFAULT_BUFFER: usize = 4;
const MAX_CHUNK: usize = 64;
const SLOW_PULL: Duration = Duration::from_micros(64);
fn chunk_limit(buffer_size: usize) -> usize {
if buffer_size <= 2 { 1 } else { MAX_CHUNK }
}
struct FeederGate {
limit: Mutex<Option<usize>>,
cv: Condvar,
}
impl FeederGate {
fn new() -> Arc<Self> {
Arc::new(Self {
limit: Mutex::new(None),
cv: Condvar::new(),
})
}
fn open(&self, limit: usize) {
let mut guard = lock(&self.limit);
if guard.is_none() {
*guard = Some(limit);
self.cv.notify_one();
}
}
fn wait(&self) -> usize {
let mut guard = lock(&self.limit);
while guard.is_none() {
guard = self.cv.wait(guard).unwrap_or_else(|err| err.into_inner());
}
(*guard).expect("packet size is published before wait returns")
}
}
struct FeederRelease {
gate: Arc<FeederGate>,
limit: usize,
}
impl Drop for FeederRelease {
fn drop(&mut self) {
self.gate.open(self.limit);
}
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
#[non_exhaustive]
pub enum PipelineError {
#[error("worker panicked in stage {stage}: {message}")]
WorkerPanicked {
stage: &'static str,
message: String,
},
#[error("task failed in stage {stage}: {message}")]
TaskFailed {
stage: &'static str,
message: String,
},
#[error("timeout after {0:?}")]
Timeout(Duration),
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum ErrorMode<U> {
#[default]
FailFast,
Skip,
SkipAndLog,
Replace(U),
}
#[derive(Default)]
struct FinishState {
started: bool,
done: bool,
error: Option<PipelineError>,
}
struct Shared {
handles: Mutex<Vec<JoinHandle<()>>>,
error: Mutex<Option<PipelineError>>,
cancelled: AtomicBool,
peers: Mutex<Vec<Weak<Shared>>>,
retained: Mutex<Vec<Arc<Shared>>>,
finish: Mutex<FinishState>,
finish_cv: Condvar,
}
fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
mutex.lock().unwrap_or_else(|err| err.into_inner())
}
fn panic_message(payload: &(dyn Any + Send)) -> String {
if let Some(msg) = payload.downcast_ref::<&str>() {
return (*msg).to_owned();
}
if let Some(msg) = payload.downcast_ref::<String>() {
return msg.clone();
}
"(unknown panic)".to_owned()
}
pub(crate) enum Packet<T> {
One(T),
Many(Vec<T>),
}
#[derive(PartialEq, Eq)]
enum Step {
Yield,
Halted,
Closed,
Cancelled,
}
struct BatchTx<T> {
tx: Sender<Packet<T>>,
buf: Vec<T>,
limit: usize,
}
impl<T> BatchTx<T> {
fn new(tx: Sender<Packet<T>>, limit: usize) -> Self {
let limit = limit.max(1);
Self {
tx,
buf: Vec::with_capacity(limit.min(MAX_CHUNK)),
limit,
}
}
fn is_empty(&self) -> bool {
self.buf.is_empty()
}
#[inline]
fn push(&mut self, item: T) -> bool {
self.buf.push(item);
if self.buf.len() >= self.limit {
self.flush()
} else {
true
}
}
#[inline]
fn flush(&mut self) -> bool {
if self.buf.is_empty() {
return true;
}
let packet = if self.buf.len() == 1 {
Packet::One(self.buf.pop().expect("len checked"))
} else {
Packet::Many(std::mem::replace(
&mut self.buf,
Vec::with_capacity(self.limit.min(MAX_CHUNK)),
))
};
self.tx.send(packet).is_ok()
}
}
#[inline]
fn feed_packet<T>(packet: Packet<T>, shared: &Shared, on_item: &mut impl FnMut(T) -> bool) -> bool {
match packet {
Packet::One(item) => !shared.is_cancelled() && on_item(item),
Packet::Many(items) => {
for item in items {
if shared.is_cancelled() || !on_item(item) {
return false;
}
}
true
}
}
}
fn pull<T>(
rx: &Receiver<Packet<T>>,
shared: &Shared,
drain: bool,
mut on_item: impl FnMut(T) -> bool,
) -> Step {
if shared.is_cancelled() {
return Step::Cancelled;
}
let packet = match rx.recv() {
Ok(packet) => packet,
Err(_) => return Step::Closed,
};
if !feed_packet(packet, shared, &mut on_item) {
return if shared.is_cancelled() {
Step::Cancelled
} else {
Step::Halted
};
}
if drain {
loop {
match rx.try_recv() {
Ok(packet) => {
if !feed_packet(packet, shared, &mut on_item) {
return if shared.is_cancelled() {
Step::Cancelled
} else {
Step::Halted
};
}
}
Err(TryRecvError::Empty) => break,
Err(TryRecvError::Disconnected) => return Step::Closed,
}
}
}
if shared.is_cancelled() {
Step::Cancelled
} else {
Step::Yield
}
}
fn map_out<T, U>(
rx: Receiver<Packet<T>>,
tx: Sender<Packet<U>>,
shared: &Shared,
limit: usize,
drain: bool,
mut on_item: impl FnMut(T, &mut BatchTx<U>) -> bool,
) {
let mut out = BatchTx::new(tx, limit);
let mut hold = false;
let mut prev_fast = false;
loop {
let step = pull(&rx, shared, drain, |item| {
let started = if hold { None } else { Some(Instant::now()) };
if !on_item(item, &mut out) {
return false;
}
let Some(started) = started else {
return true;
};
let fast = started.elapsed() < SLOW_PULL;
if !fast {
prev_fast = false;
hold = false;
return out.flush();
}
if prev_fast {
hold = true;
}
prev_fast = true;
hold || out.flush()
});
match step {
Step::Yield => {
hold = false;
prev_fast = false;
if !out.flush() {
break;
}
}
Step::Closed | Step::Halted => {
if !shared.is_cancelled() {
let _ = out.flush();
}
break;
}
Step::Cancelled => break,
}
}
}
fn feed<I, T>(iter: I, tx: Sender<Packet<T>>, shared: &Shared, limit: usize)
where
I: IntoIterator<Item = T>,
{
let mut iter = iter.into_iter();
if limit <= 1 {
for item in iter {
if shared.is_cancelled() || tx.send(Packet::One(item)).is_err() {
break;
}
}
return;
}
let mut out = BatchTx::new(tx, limit);
let mut prev_fast = false;
let mut hold = false;
loop {
if shared.is_cancelled() {
break;
}
let time_this = out.is_empty();
let started = time_this.then(Instant::now);
let Some(item) = iter.next() else {
break;
};
if let Some(started) = started {
let fast = started.elapsed() < SLOW_PULL;
if !fast {
prev_fast = false;
hold = false;
if !out.push(item) || !out.flush() {
break;
}
continue;
}
if prev_fast {
hold = true;
}
prev_fast = true;
if !out.push(item) {
break;
}
if !hold && !out.flush() {
break;
}
} else if !out.push(item) {
break;
}
}
let _ = out.flush();
}
impl Shared {
fn new() -> Arc<Self> {
Arc::new(Self {
handles: Mutex::new(Vec::new()),
error: Mutex::new(None),
cancelled: AtomicBool::new(false),
peers: Mutex::new(Vec::new()),
retained: Mutex::new(Vec::new()),
finish: Mutex::new(FinishState::default()),
finish_cv: Condvar::new(),
})
}
#[inline]
fn is_cancelled(&self) -> bool {
self.cancelled.load(Ordering::Relaxed)
}
fn spawn(self: &Arc<Self>, stage: &'static str, body: impl FnOnce() + Send + 'static) {
let shared = Arc::clone(self);
let handle = thread::Builder::new()
.name(format!("olympipe-{stage}"))
.spawn(move || {
if let Err(payload) = panic::catch_unwind(AssertUnwindSafe(body)) {
shared.fail(PipelineError::WorkerPanicked {
stage,
message: panic_message(&*payload),
});
}
})
.unwrap_or_else(|_| panic!("olympipe-rs failed to spawn {stage} worker"));
lock(&self.handles).push(handle);
}
fn fail(self: &Arc<Self>, err: PipelineError) {
self.distribute(&err);
self.cancel_all();
}
fn distribute(self: &Arc<Self>, err: &PipelineError) {
let mut seen = HashSet::<*const Self>::new();
let mut stack = vec![Arc::clone(self)];
while let Some(node) = stack.pop() {
if !seen.insert(Arc::as_ptr(&node)) {
continue;
}
{
let mut slot = lock(&node.error);
if slot.is_none() {
*slot = Some(err.clone());
}
}
stack.extend(live_peers(&node));
}
}
fn cancel_all(self: &Arc<Self>) {
let mut seen = HashSet::<*const Self>::new();
let mut stack = vec![Arc::clone(self)];
while let Some(node) = stack.pop() {
if !seen.insert(Arc::as_ptr(&node)) {
continue;
}
node.cancelled.store(true, Ordering::Relaxed);
stack.extend(live_peers(&node));
}
}
fn link(parent: &Arc<Self>, child: &Arc<Self>) {
if Arc::ptr_eq(parent, child) {
return;
}
lock(&parent.retained).push(Arc::clone(child));
lock(&parent.peers).push(Arc::downgrade(child));
lock(&child.peers).push(Arc::downgrade(parent));
if let Some(err) = stored_error(child) {
parent.distribute(&err);
}
if let Some(err) = stored_error(parent) {
child.distribute(&err);
}
if parent.is_cancelled() || child.is_cancelled() {
parent.cancel_all();
}
}
fn join_handles(&self) {
let handles = {
let mut guard = lock(&self.handles);
std::mem::take(&mut *guard)
};
for handle in handles {
if let Err(payload) = handle.join() {
let mut slot = lock(&self.error);
if slot.is_none() {
*slot = Some(PipelineError::WorkerPanicked {
stage: "worker",
message: panic_message(&*payload),
});
}
self.cancelled.store(true, Ordering::Relaxed);
}
}
}
fn shutdown(self: &Arc<Self>) -> Option<PipelineError> {
{
let mut guard = lock(&self.finish);
if guard.done {
return guard.error.clone();
}
if guard.started {
while !guard.done {
guard = self
.finish_cv
.wait(guard)
.unwrap_or_else(|err| err.into_inner());
}
return guard.error.clone();
}
guard.started = true;
}
struct Publish<'a> {
shared: &'a Shared,
error: Option<PipelineError>,
}
impl Drop for Publish<'_> {
fn drop(&mut self) {
let mut guard = lock(&self.shared.finish);
if !guard.done {
guard.error = self.error.take();
guard.done = true;
self.shared.finish_cv.notify_all();
}
}
}
let mut publish = Publish {
shared: self,
error: None,
};
publish.error = self.join_graph();
let error = publish.error.clone();
drop(publish);
error
}
fn join_graph(self: &Arc<Self>) -> Option<PipelineError> {
self.join_handles();
let peers = live_peers(self);
for peer in &peers {
peer.join_handles();
}
let error = first_error(self);
lock(&self.retained).clear();
lock(&self.peers).clear();
error
}
}
fn live_peers(shared: &Shared) -> Vec<Arc<Shared>> {
lock(&shared.peers)
.iter()
.filter_map(Weak::upgrade)
.collect()
}
fn stored_error(shared: &Shared) -> Option<PipelineError> {
lock(&shared.error).clone()
}
fn first_error(root: &Shared) -> Option<PipelineError> {
if let Some(err) = stored_error(root) {
return Some(err);
}
let mut seen = HashSet::<*const Shared>::from([root as *const Shared]);
let mut stack = live_peers(root);
while let Some(node) = stack.pop() {
if !seen.insert(Arc::as_ptr(&node)) {
continue;
}
if let Some(err) = stored_error(&node) {
return Some(err);
}
stack.extend(live_peers(&node));
}
None
}
pub struct Pipeline<T: Send + 'static> {
pending_release: Option<FeederRelease>,
pub(crate) receiver: Receiver<Packet<T>>,
shared: Arc<Shared>,
pub(crate) buffer_size: usize,
chunk_limit: usize,
}
fn downstream<T: Send + 'static>(
receiver: Receiver<Packet<T>>,
shared: Arc<Shared>,
buffer_size: usize,
chunk_limit: usize,
) -> Pipeline<T> {
Pipeline {
pending_release: None,
receiver,
shared,
buffer_size,
chunk_limit,
}
}
impl<T: Send + 'static> Pipeline<T> {
pub fn new(iter: impl IntoIterator<Item = T> + Send + 'static) -> Self {
Self::new_with_buffer(iter, DEFAULT_BUFFER)
}
pub fn new_with_buffer(
iter: impl IntoIterator<Item = T> + Send + 'static,
buffer_size: usize,
) -> Self {
let buffer_size = buffer_size.max(1);
let limit = chunk_limit(buffer_size);
let (tx, rx) = bounded(buffer_size);
let shared = Shared::new();
let gate = FeederGate::new();
let gate_feeder = Arc::clone(&gate);
let shared_feeder = Arc::clone(&shared);
Shared::spawn(&shared, "feeder", move || {
let limit = gate_feeder.wait();
feed(iter, tx, &shared_feeder, limit);
});
Pipeline {
pending_release: Some(FeederRelease { gate, limit }),
receiver: rx,
shared,
buffer_size,
chunk_limit: limit,
}
}
pub fn with_buffer(mut self, size: usize) -> Self {
self.buffer_size = size.max(1);
self.chunk_limit = chunk_limit(self.buffer_size);
self
}
pub fn with_max_packet(mut self, n: usize) -> Self {
let n = n.clamp(1, MAX_CHUNK);
self.chunk_limit = n;
if let Some(release) = self.pending_release.as_mut() {
release.limit = n;
}
self
}
}
impl<T: Send + 'static> Pipeline<T> {
fn activate(&mut self) {
self.pending_release.take();
}
fn spawn_stage<U, W>(mut self, stage: &'static str, count: usize, worker: W) -> Pipeline<U>
where
U: Send + 'static,
W: Fn(Receiver<Packet<T>>, Sender<Packet<U>>, Arc<Shared>) + Send + Clone + 'static,
{
self.activate();
let (tx, rx) = bounded::<Packet<U>>(self.buffer_size);
let shared = Arc::clone(&self.shared);
for _ in 0..count {
let worker = worker.clone();
let in_rx = self.receiver.clone();
let out_tx = tx.clone();
let shared_worker = Arc::clone(&shared);
Shared::spawn(&shared, stage, move || {
worker(in_rx, out_tx, shared_worker);
});
}
drop(tx);
downstream(rx, shared, self.buffer_size, self.chunk_limit)
}
fn spawn_single<U, W>(mut self, stage: &'static str, worker: W) -> Pipeline<U>
where
U: Send + 'static,
W: FnOnce(Receiver<Packet<T>>, Sender<Packet<U>>, Arc<Shared>) + Send + 'static,
{
self.activate();
let (tx, rx) = bounded::<Packet<U>>(self.buffer_size);
let shared = Arc::clone(&self.shared);
let in_rx = self.receiver;
let shared_worker = Arc::clone(&shared);
let buffer_size = self.buffer_size;
let chunk_limit = self.chunk_limit;
Shared::spawn(&shared, stage, move || worker(in_rx, tx, shared_worker));
downstream(rx, shared, buffer_size, chunk_limit)
}
fn finish<R>(
mut self,
drain: impl FnOnce(&Receiver<Packet<T>>, &Shared) -> R,
) -> Result<R, PipelineError> {
self.activate();
let output = drain(&self.receiver, &self.shared);
let shared = Arc::clone(&self.shared);
drop(self);
match shared.shutdown() {
Some(err) => Err(err),
None => Ok(output),
}
}
}
impl<T: Send + 'static> Pipeline<T> {
pub fn task<U, F>(self, f: F, count: usize) -> Pipeline<U>
where
U: Send + 'static,
F: Fn(T) -> U + Send + Clone + 'static,
{
let count = count.max(1);
let drain = count == 1;
let limit = self.chunk_limit;
self.spawn_stage("task", count, move |rx, tx, shared| {
map_out(rx, tx, &shared, limit, drain, |item, out| {
if shared.is_cancelled() {
return false;
}
let mapped = f(item);
!shared.is_cancelled() && out.push(mapped)
});
})
}
pub fn class_task<S, U, I, M>(self, init: I, method: M, count: usize) -> Pipeline<U>
where
S: 'static,
U: Send + 'static,
I: Fn() -> S + Send + Clone + 'static,
M: Fn(&mut S, T) -> U + Send + Clone + 'static,
{
self.class_task_with_close(init, method, |_| {}, count)
}
pub fn class_task_with_close<S, U, I, M, C>(
self,
init: I,
method: M,
close: C,
count: usize,
) -> Pipeline<U>
where
S: 'static,
U: Send + 'static,
I: Fn() -> S + Send + Clone + 'static,
M: Fn(&mut S, T) -> U + Send + Clone + 'static,
C: Fn(S) + Send + Clone + 'static,
{
let count = count.max(1);
let drain = count == 1;
let limit = self.chunk_limit;
self.spawn_stage("class_task", count, move |rx, tx, shared| {
let mut state = init();
map_out(rx, tx, &shared, limit, drain, |item, out| {
if shared.is_cancelled() {
return false;
}
let mapped = method(&mut state, item);
!shared.is_cancelled() && out.push(mapped)
});
close(state);
})
}
pub fn task_or<U, E, F, H>(self, f: F, on_error: H) -> Pipeline<U>
where
T: Clone,
U: Send + 'static,
E: 'static,
F: Fn(T) -> Result<U, E> + Send + 'static,
H: Fn(T, E) -> Option<U> + Send + 'static,
{
let limit = self.chunk_limit;
self.spawn_single("task_or", move |rx, tx, shared| {
map_out(rx, tx, &shared, limit, true, |item, out| {
let cloned = item.clone();
match f(item) {
Ok(value) => out.push(value),
Err(err) => match on_error(cloned, err) {
Some(value) => out.push(value),
None => true,
},
}
});
})
}
pub fn try_task<U, E, F>(self, f: F, count: usize, mode: ErrorMode<U>) -> Pipeline<U>
where
U: Clone + Send + 'static,
E: std::fmt::Display + 'static,
F: Fn(T) -> Result<U, E> + Send + Clone + 'static,
{
let count = count.max(1);
let drain = count == 1;
let limit = self.chunk_limit;
if matches!(mode, ErrorMode::FailFast) {
return self.spawn_stage("try_task", count, move |rx, tx, shared| {
map_out(rx, tx, &shared, limit, drain, |item, out| {
if shared.is_cancelled() {
return false;
}
match f(item) {
Ok(value) => !shared.is_cancelled() && out.push(value),
Err(err) => {
shared.fail(PipelineError::TaskFailed {
stage: "try_task",
message: err.to_string(),
});
false
}
}
});
});
}
self.spawn_stage("try_task", count, move |rx, tx, shared| {
map_out(rx, tx, &shared, limit, drain, |item, out| {
if shared.is_cancelled() {
return false;
}
match panic::catch_unwind(AssertUnwindSafe(|| f(item))) {
Ok(Ok(value)) => !shared.is_cancelled() && out.push(value),
Ok(Err(err)) => {
!shared.is_cancelled() && note_skipped(&mode, "try_task", &err, out)
}
Err(payload) => {
!shared.is_cancelled()
&& note_skipped(&mode, "try_task", &panic_message(&payload), out)
}
}
});
})
}
pub fn task_with_mode<U, F>(self, f: F, count: usize, mode: ErrorMode<U>) -> Pipeline<U>
where
U: Clone + Send + 'static,
F: Fn(T) -> U + Send + Clone + 'static,
{
if matches!(mode, ErrorMode::FailFast) {
return self.task(f, count);
}
let count = count.max(1);
let drain = count == 1;
let limit = self.chunk_limit;
self.spawn_stage("task_with_mode", count, move |rx, tx, shared| {
map_out(rx, tx, &shared, limit, drain, |item, out| {
if shared.is_cancelled() {
return false;
}
match panic::catch_unwind(AssertUnwindSafe(|| f(item))) {
Ok(value) => !shared.is_cancelled() && out.push(value),
Err(payload) => {
!shared.is_cancelled()
&& note_skipped(&mode, "task_with_mode", &panic_message(&payload), out)
}
}
});
})
}
#[cfg(feature = "cache")]
#[cfg_attr(docsrs, doc(cfg(feature = "cache")))]
pub fn cached_task<U, F>(
self,
step: &str,
cache_dir: impl Into<std::path::PathBuf>,
f: F,
count: usize,
) -> Pipeline<U>
where
T: serde::Serialize,
U: serde::Serialize + serde::de::DeserializeOwned + Send + 'static,
F: Fn(T) -> U + Send + Clone + 'static,
{
let count = count.max(1);
let drain = count == 1;
let limit = self.chunk_limit;
let step = step.to_owned();
let cache_dir = cache_dir.into();
self.spawn_stage("cached_task", count, move |rx, tx, shared| {
let mut logged = false;
map_out(rx, tx, &shared, limit, drain, |item, out| {
if shared.is_cancelled() {
return false;
}
match crate::cache::resolve(&step, &cache_dir, item, &f, &mut logged) {
Ok(value) => !shared.is_cancelled() && out.push(value),
Err(err) => {
shared.fail(err);
false
}
}
});
})
}
pub fn filter<F>(self, f: F) -> Pipeline<T>
where
F: Fn(&T) -> bool + Send + 'static,
{
let limit = self.chunk_limit;
self.spawn_single("filter", move |rx, tx, shared| {
map_out(rx, tx, &shared, limit, true, |item, out| {
if f(&item) { out.push(item) } else { true }
});
})
}
pub fn batch(self, size: usize) -> Pipeline<Vec<T>> {
assert!(size >= 1, "batch size must be >= 1");
let limit = self.chunk_limit;
self.spawn_single("batch", move |rx, tx, shared| {
let mut buf: Vec<T> = Vec::with_capacity(size);
let mut out = BatchTx::new(tx, limit);
loop {
let step = pull(&rx, &shared, true, |item| {
buf.push(item);
if buf.len() >= size {
let full = std::mem::replace(&mut buf, Vec::with_capacity(size));
return out.push(full);
}
true
});
match step {
Step::Yield => {
if !out.flush() {
break;
}
}
Step::Closed => {
if !shared.is_cancelled() {
if !buf.is_empty() {
let _ = out.push(std::mem::take(&mut buf));
}
let _ = out.flush();
}
break;
}
Step::Halted | Step::Cancelled => break,
}
}
})
}
pub fn temporal_batch(self, window: Duration) -> Pipeline<Vec<T>> {
self.spawn_single("temporal_batch", move |rx, tx, shared| {
loop {
if shared.is_cancelled() {
return;
}
let first = match rx.recv() {
Ok(packet) => packet,
Err(_) => return,
};
if shared.is_cancelled() {
return;
}
let mut batch = Vec::new();
if !extend_batch(&mut batch, first, &shared) {
return;
}
loop {
if shared.is_cancelled() {
return;
}
match rx.recv_timeout(window) {
Ok(packet) => {
if !extend_batch(&mut batch, packet, &shared) {
return;
}
}
Err(RecvTimeoutError::Timeout) => break,
Err(RecvTimeoutError::Disconnected) => {
if !shared.is_cancelled() {
let _ = tx.send(Packet::One(batch));
}
return;
}
}
}
if shared.is_cancelled() || tx.send(Packet::One(batch)).is_err() {
return;
}
}
})
}
pub fn explode<U, I, F>(self, f: F) -> Pipeline<U>
where
U: Send + 'static,
I: IntoIterator<Item = U>,
F: Fn(T) -> I + Send + 'static,
{
let limit = self.chunk_limit;
self.spawn_single("explode", move |rx, tx, shared| {
map_out(rx, tx, &shared, limit, true, |item, out| {
for produced in f(item) {
if shared.is_cancelled() || !out.push(produced) {
return false;
}
}
true
});
})
}
pub fn split<A, B, F>(self, f: F) -> (Pipeline<A>, Pipeline<B>)
where
A: Send + 'static,
B: Send + 'static,
F: Fn(T) -> (Option<A>, Option<B>) + Send + 'static,
{
self.split_inner(false, f)
}
pub fn split_unbounded<A, B, F>(self, f: F) -> (Pipeline<A>, Pipeline<B>)
where
A: Send + 'static,
B: Send + 'static,
F: Fn(T) -> (Option<A>, Option<B>) + Send + 'static,
{
self.split_inner(true, f)
}
fn split_inner<A, B, F>(mut self, unbounded_out: bool, f: F) -> (Pipeline<A>, Pipeline<B>)
where
A: Send + 'static,
B: Send + 'static,
F: Fn(T) -> (Option<A>, Option<B>) + Send + 'static,
{
self.activate();
let buffer_size = self.buffer_size;
let chunk_limit = self.chunk_limit;
let shared = Arc::clone(&self.shared);
let (tx_a, rx_a) = if unbounded_out {
unbounded()
} else {
bounded(buffer_size)
};
let (tx_b, rx_b) = if unbounded_out {
unbounded()
} else {
bounded(buffer_size)
};
let input = self.receiver;
let shared_router = Arc::clone(&shared);
Shared::spawn(&shared, "split", move || {
route(input, tx_a, tx_b, chunk_limit, f, &shared_router);
});
(
downstream(rx_a, Arc::clone(&shared), buffer_size, chunk_limit),
downstream(rx_b, shared, buffer_size, chunk_limit),
)
}
pub fn split_by<F>(self, n: usize, f: F) -> Vec<Pipeline<T>>
where
F: Fn(&T) -> usize + Send + 'static,
{
self.split_by_inner(n, false, f)
}
pub fn split_by_unbounded<F>(self, n: usize, f: F) -> Vec<Pipeline<T>>
where
F: Fn(&T) -> usize + Send + 'static,
{
self.split_by_inner(n, true, f)
}
fn split_by_inner<F>(mut self, n: usize, unbounded_out: bool, f: F) -> Vec<Pipeline<T>>
where
F: Fn(&T) -> usize + Send + 'static,
{
assert!(n >= 1, "split_by requires at least one branch");
self.activate();
let buffer_size = self.buffer_size;
let chunk_limit = self.chunk_limit;
let shared = Arc::clone(&self.shared);
let cap = if unbounded_out {
None
} else {
Some(buffer_size)
};
let ends = open_channels::<T>(n, cap);
let txs = ends.txs;
let rxs = ends.rxs;
let input = self.receiver;
let shared_router = Arc::clone(&shared);
Shared::spawn(&shared, "split", move || {
route_by(input, txs, chunk_limit, f, &shared_router);
});
rxs.into_iter()
.map(|rx| downstream(rx, Arc::clone(&shared), buffer_size, chunk_limit))
.collect()
}
pub fn split_array<const N: usize, U, F>(self, f: F) -> [Pipeline<U>; N]
where
U: Send + 'static,
F: Fn(T) -> [Option<U>; N] + Send + 'static,
{
self.split_array_inner(false, f)
}
pub fn split_array_unbounded<const N: usize, U, F>(self, f: F) -> [Pipeline<U>; N]
where
U: Send + 'static,
F: Fn(T) -> [Option<U>; N] + Send + 'static,
{
self.split_array_inner(true, f)
}
fn split_array_inner<const N: usize, U, F>(
mut self,
unbounded_out: bool,
f: F,
) -> [Pipeline<U>; N]
where
U: Send + 'static,
F: Fn(T) -> [Option<U>; N] + Send + 'static,
{
assert!(N >= 1, "split_array requires at least one branch");
self.activate();
let buffer_size = self.buffer_size;
let chunk_limit = self.chunk_limit;
let shared = Arc::clone(&self.shared);
let cap = if unbounded_out {
None
} else {
Some(buffer_size)
};
let ends = open_channels::<U>(N, cap);
let txs = ends.txs;
let rxs = ends.rxs;
let input = self.receiver;
let shared_router = Arc::clone(&shared);
Shared::spawn(&shared, "split", move || {
route_fanout(input, txs, chunk_limit, f, &shared_router);
});
let mut rxs = rxs.into_iter();
std::array::from_fn(|_| {
downstream(
rxs.next().expect("one receiver per branch"),
Arc::clone(&shared),
buffer_size,
chunk_limit,
)
})
}
pub fn gather(mut self, others: Vec<Pipeline<T>>) -> Pipeline<T> {
self.activate();
let others: Vec<Pipeline<T>> = others
.into_iter()
.map(|mut other| {
other.activate();
other
})
.collect();
let buffer_size = self.buffer_size;
let chunk_limit = self.chunk_limit;
let (tx, rx) = bounded::<Packet<T>>(buffer_size);
let shared = Arc::clone(&self.shared);
let tx0 = tx.clone();
let self_rx = self.receiver;
let shared_fwd = Arc::clone(&shared);
Shared::spawn(&shared, "gather", move || forward(self_rx, tx0, shared_fwd));
for other in others {
Shared::link(&shared, &other.shared);
let drained = {
let mut guard = lock(&other.shared.handles);
std::mem::take(&mut *guard)
};
lock(&shared.handles).extend(drained);
let tx_n = tx.clone();
let other_rx = other.receiver;
let shared_fwd = Arc::clone(&shared);
Shared::spawn(&shared, "gather", move || {
forward(other_rx, tx_n, shared_fwd)
});
}
drop(tx);
downstream(rx, shared, buffer_size, chunk_limit)
}
pub fn gather_all(pipes: Vec<Pipeline<T>>) -> Pipeline<T> {
assert!(
!pipes.is_empty(),
"gather_all requires at least one pipeline"
);
let mut pipes = pipes.into_iter();
let first = pipes
.next()
.expect("gather_all requires at least one pipeline");
first.gather(pipes.collect())
}
pub fn reduce<U, F>(self, init: U, f: F) -> Pipeline<U>
where
U: Send + 'static,
F: Fn(U, T) -> U + Send + 'static,
{
self.spawn_single("reduce", move |rx, tx, shared| {
let mut acc = Some(init);
loop {
match pull(&rx, &shared, true, |item| {
let current = acc.take().expect("accumulator");
acc = Some(f(current, item));
true
}) {
Step::Yield => {}
Step::Closed => {
if !shared.is_cancelled() {
if let Some(value) = acc.take() {
let _ = tx.send(Packet::One(value));
}
}
break;
}
Step::Halted | Step::Cancelled => break,
}
}
})
}
pub fn checkpoint<S, U, I, R, E>(self, initial: S, reducer: R, emitter: E) -> Pipeline<U>
where
S: Send + 'static,
U: Send + 'static,
R: Fn(S, T) -> S + Send + 'static,
E: FnOnce(S) -> I + Send + 'static,
I: IntoIterator<Item = U>,
{
let limit = self.chunk_limit;
self.spawn_single("checkpoint", move |rx, tx, shared| {
let mut state = Some(initial);
loop {
match pull(&rx, &shared, true, |item| {
let current = state.take().expect("checkpoint state");
state = Some(reducer(current, item));
true
}) {
Step::Yield => {}
Step::Closed => {
if !shared.is_cancelled() {
if let Some(finished) = state.take() {
let mut out = BatchTx::new(tx, limit);
for produced in emitter(finished) {
if shared.is_cancelled() || !out.push(produced) {
return;
}
}
if !shared.is_cancelled() {
let _ = out.flush();
}
}
}
break;
}
Step::Halted | Step::Cancelled => break,
}
}
})
}
pub fn limit(self, n: usize) -> Pipeline<T> {
let limit = self.chunk_limit;
self.spawn_single("limit", move |rx, tx, shared| {
let mut sent = 0usize;
map_out(rx, tx, &shared, limit, true, |item, out| {
if !out.push(item) {
return false;
}
sent += 1;
sent < n
});
})
}
pub fn timeout(self, duration: Duration) -> Pipeline<T> {
let limit = self.chunk_limit;
self.spawn_single("timeout", move |rx, tx, shared| {
let mut out = BatchTx::new(tx, limit);
let first = match rx.recv() {
Ok(packet) => packet,
Err(_) => return,
};
if !forward_packet(&mut out, first, &shared) {
return;
}
loop {
if shared.is_cancelled() {
return;
}
match rx.recv_timeout(duration) {
Ok(packet) => {
if !forward_packet(&mut out, packet, &shared) {
break;
}
}
Err(RecvTimeoutError::Timeout) => {
shared.fail(PipelineError::Timeout(duration));
break;
}
Err(RecvTimeoutError::Disconnected) => break,
}
}
})
}
pub fn debug(self) -> Pipeline<T>
where
T: std::fmt::Debug,
{
let limit = self.chunk_limit;
self.spawn_single("debug", move |rx, tx, shared| {
map_out(rx, tx, &shared, limit, true, |item, out| {
println!("[olympipe-rs] {item:?}");
out.push(item)
});
})
}
}
fn extend_batch<T>(batch: &mut Vec<T>, packet: Packet<T>, shared: &Shared) -> bool {
feed_packet(packet, shared, &mut |item| {
batch.push(item);
true
})
}
fn forward_packet<T>(out: &mut BatchTx<T>, packet: Packet<T>, shared: &Shared) -> bool {
let ok = feed_packet(packet, shared, &mut |item| out.push(item));
ok && !shared.is_cancelled() && out.flush()
}
fn forward<T>(rx: Receiver<Packet<T>>, tx: Sender<Packet<T>>, shared: Arc<Shared>) {
loop {
if shared.is_cancelled() {
break;
}
match rx.recv() {
Ok(packet) => {
if shared.is_cancelled() || tx.send(packet).is_err() {
break;
}
}
Err(_) => break,
}
}
}
fn route<T, A, B, F>(
rx: Receiver<Packet<T>>,
tx_a: Sender<Packet<A>>,
tx_b: Sender<Packet<B>>,
limit: usize,
f: F,
shared: &Shared,
) where
F: Fn(T) -> (Option<A>, Option<B>),
{
let mut out_a = BatchTx::new(tx_a, limit);
let mut out_b = BatchTx::new(tx_b, limit);
let mut a_open = true;
let mut b_open = true;
loop {
if shared.is_cancelled() || !(a_open || b_open) {
break;
}
let step = pull(&rx, shared, true, |item| {
if shared.is_cancelled() {
return false;
}
let (a, b) = f(item);
if a_open {
if let Some(value) = a {
if !out_a.push(value) {
a_open = false;
}
}
}
if b_open {
if let Some(value) = b {
if !out_b.push(value) {
b_open = false;
}
}
}
a_open || b_open
});
if a_open && !out_a.flush() {
a_open = false;
}
if b_open && !out_b.flush() {
b_open = false;
}
if step != Step::Yield || !(a_open || b_open) {
break;
}
}
}
struct ChannelEnds<T> {
txs: Vec<Sender<Packet<T>>>,
rxs: Vec<Receiver<Packet<T>>>,
}
fn open_channels<T>(n: usize, capacity: Option<usize>) -> ChannelEnds<T> {
let mut txs = Vec::with_capacity(n);
let mut rxs = Vec::with_capacity(n);
for _ in 0..n {
let (tx, rx) = match capacity {
Some(capacity) => bounded(capacity),
None => unbounded(),
};
txs.push(tx);
rxs.push(rx);
}
ChannelEnds { txs, rxs }
}
fn flush_open<T>(outs: &mut [Option<BatchTx<T>>]) {
for slot in outs.iter_mut() {
let failed = match slot.as_mut() {
Some(out) => !out.flush(),
None => false,
};
if failed {
*slot = None;
}
}
}
fn route_by<T, F>(
rx: Receiver<Packet<T>>,
txs: Vec<Sender<Packet<T>>>,
limit: usize,
f: F,
shared: &Shared,
) where
F: Fn(&T) -> usize,
{
let n = txs.len();
let mut outs: Vec<Option<BatchTx<T>>> = txs
.into_iter()
.map(|tx| Some(BatchTx::new(tx, limit)))
.collect();
loop {
if shared.is_cancelled() || outs.iter().all(Option::is_none) {
break;
}
let step = pull(&rx, shared, true, |item| {
if shared.is_cancelled() {
return false;
}
let index = f(&item);
if index >= n {
panic!("split index {index} is outside 0..{n}");
}
let disconnected = match outs[index].as_mut() {
Some(out) => !out.push(item),
None => false,
};
if disconnected {
outs[index] = None;
}
outs.iter().any(Option::is_some)
});
flush_open(&mut outs);
if step != Step::Yield || outs.iter().all(Option::is_none) {
break;
}
}
}
fn route_fanout<T, U, const N: usize, F>(
rx: Receiver<Packet<T>>,
txs: Vec<Sender<Packet<U>>>,
limit: usize,
f: F,
shared: &Shared,
) where
F: Fn(T) -> [Option<U>; N],
{
let mut outs: Vec<Option<BatchTx<U>>> = txs
.into_iter()
.map(|tx| Some(BatchTx::new(tx, limit)))
.collect();
loop {
if shared.is_cancelled() || outs.iter().all(Option::is_none) {
break;
}
let step = pull(&rx, shared, true, |item| {
if shared.is_cancelled() {
return false;
}
let parts = f(item);
for (index, part) in parts.into_iter().enumerate() {
let Some(value) = part else {
continue;
};
let disconnected = match outs[index].as_mut() {
Some(out) => !out.push(value),
None => false,
};
if disconnected {
outs[index] = None;
}
}
outs.iter().any(Option::is_some)
});
flush_open(&mut outs);
if step != Step::Yield || outs.iter().all(Option::is_none) {
break;
}
}
}
fn note_skipped<U: Clone>(
mode: &ErrorMode<U>,
stage: &str,
err: &dyn std::fmt::Display,
out: &mut BatchTx<U>,
) -> bool {
match mode {
ErrorMode::Skip => true,
ErrorMode::SkipAndLog => {
eprintln!("olympipe-rs: [{stage}] skipped item: {err}");
true
}
ErrorMode::Replace(value) => out.push(value.clone()),
ErrorMode::FailFast => unreachable!("FailFast does not recover per item"),
}
}
impl<T: Send + 'static> Pipeline<T> {
pub fn collect(self) -> Vec<T> {
match self.try_collect() {
Ok(items) => items,
Err(err) => panic!("olympipe-rs pipeline failed: {err}"),
}
}
pub fn for_each<F>(self, f: F)
where
F: FnMut(T),
{
if let Err(err) = self.try_for_each(f) {
panic!("olympipe-rs pipeline failed: {err}");
}
}
pub fn wait_for_completion(self) {
if let Err(err) = self.try_wait_for_completion() {
panic!("olympipe-rs pipeline failed: {err}");
}
}
pub fn try_collect(self) -> Result<Vec<T>, PipelineError> {
self.finish(|rx, shared| {
let mut items = Vec::new();
while let Some(packet) = recv_user(rx, shared) {
match packet {
Packet::One(item) => items.push(item),
Packet::Many(more) => items.extend(more),
}
}
items
})
}
pub fn try_for_each<F>(self, mut f: F) -> Result<(), PipelineError>
where
F: FnMut(T),
{
self.finish(|rx, shared| {
while let Some(packet) = recv_user(rx, shared) {
match packet {
Packet::One(item) => f(item),
Packet::Many(more) => {
for item in more {
f(item);
}
}
}
}
})
}
pub fn try_wait_for_completion(self) -> Result<(), PipelineError> {
self.finish(|rx, shared| while recv_user(rx, shared).is_some() {})
}
pub fn wait_for_results(self, others: Vec<Pipeline<T>>) -> Vec<Vec<T>> {
unwrap_pipeline(self.try_wait_for_results(others))
}
pub fn try_wait_for_results(
self,
others: Vec<Pipeline<T>>,
) -> Result<Vec<Vec<T>>, PipelineError> {
let mut pipes = Vec::with_capacity(1 + others.len());
pipes.push(self);
pipes.extend(others);
collect_concurrent(pipes, Pipeline::try_collect)
}
pub fn wait_for_all_completions(self, others: Vec<Pipeline<T>>) {
unwrap_pipeline(self.try_wait_for_all_completions(others));
}
pub fn try_wait_for_all_completions(
self,
others: Vec<Pipeline<T>>,
) -> Result<(), PipelineError> {
let mut pipes = Vec::with_capacity(1 + others.len());
pipes.push(self);
pipes.extend(others);
collect_concurrent(pipes, Pipeline::try_wait_for_completion).map(|_| ())
}
pub fn wait_and_reduce<U, F>(self, init: U, f: F) -> U
where
U: Send + 'static,
F: Fn(U, T) -> U + Send + 'static,
{
unwrap_pipeline(self.try_wait_and_reduce(init, f))
}
pub fn try_wait_and_reduce<U, F>(self, init: U, f: F) -> Result<U, PipelineError>
where
U: Send + 'static,
F: Fn(U, T) -> U + Send + 'static,
{
let mut reduced = self.reduce(init, f).try_collect()?;
Ok(reduced.pop().expect("reduce emits the accumulator"))
}
}
pub fn try_collect_pair<A, B>(
a: Pipeline<A>,
b: Pipeline<B>,
) -> Result<(Vec<A>, Vec<B>), PipelineError>
where
A: Send + 'static,
B: Send + 'static,
{
thread::scope(|scope| {
let right = scope.spawn(|| b.try_collect());
let left = a.try_collect();
let right = match right.join() {
Ok(result) => result,
Err(payload) => std::panic::resume_unwind(payload),
};
match (left, right) {
(Ok(left), Ok(right)) => Ok((left, right)),
(Err(err), _) => Err(err),
(Ok(_), Err(err)) => Err(err),
}
})
}
fn unwrap_pipeline<T>(result: Result<T, PipelineError>) -> T {
match result {
Ok(value) => value,
Err(err) => panic!("olympipe-rs pipeline failed: {err}"),
}
}
fn collect_concurrent<T, R, Drive>(
pipes: Vec<Pipeline<T>>,
drive: Drive,
) -> Result<Vec<R>, PipelineError>
where
T: Send + 'static,
R: Send,
Drive: Fn(Pipeline<T>) -> Result<R, PipelineError> + Sync,
{
thread::scope(|scope| {
let handles: Vec<_> = pipes
.into_iter()
.map(|pipe| scope.spawn(|| drive(pipe)))
.collect();
let mut values = Vec::with_capacity(handles.len());
let mut failure = None;
for handle in handles {
match handle.join() {
Ok(Ok(value)) => {
if failure.is_none() {
values.push(value);
}
}
Ok(Err(err)) => {
if failure.is_none() {
failure = Some(err);
}
}
Err(payload) => std::panic::resume_unwind(payload),
}
}
match failure {
Some(err) => Err(err),
None => Ok(values),
}
})
}
fn recv_user<T>(rx: &Receiver<Packet<T>>, shared: &Shared) -> Option<Packet<T>> {
if shared.is_cancelled() {
return None;
}
match rx.recv() {
Ok(packet) if !shared.is_cancelled() => Some(packet),
_ => None,
}
}