use std::{
collections::VecDeque,
sync::{Arc, Condvar, Mutex, MutexGuard, PoisonError},
};
use super::parallel_compressor::CompactionSpawner;
pub trait OrderedJob: Send + 'static {
type Context: Send + Sync + 'static;
type Output: Send + 'static;
fn run(self, context: &Self::Context) -> Self::Output;
}
struct State<J: OrderedJob> {
queue: VecDeque<(u64, J)>,
slots: Box<[Option<J::Output>]>,
tokens: usize,
idle: usize,
closed: bool,
writer_waiting: bool,
}
impl<J: OrderedJob> State<J> {
fn slot(&mut self, seq: u64) -> Option<&mut Option<J::Output>> {
#[expect(
clippy::cast_possible_truncation,
reason = "a remainder of a usize-sized ring length"
)]
let at = (seq % self.slots.len() as u64) as usize;
self.slots.get_mut(at)
}
fn publish(&mut self, seq: u64, output: J::Output) -> bool {
if let Some(slot) = self.slot(seq) {
debug_assert!(slot.is_none(), "an ordered pipeline slot was reused");
*slot = Some(output);
}
self.writer_waiting
}
}
struct Shared<J: OrderedJob> {
state: Mutex<State<J>>,
woke: Condvar,
queued: Condvar,
writer: std::thread::ThreadId,
linger: std::time::Duration,
context: J::Context,
}
const LINGER: std::time::Duration = std::time::Duration::from_millis(1);
impl<J: OrderedJob> Shared<J> {
fn lock(&self) -> MutexGuard<'_, State<J>> {
self.state.lock().unwrap_or_else(PoisonError::into_inner)
}
fn work(&self) {
let may_park = std::thread::current().id() != self.writer;
let mut done: Option<(u64, J::Output)> = None;
loop {
let mut state = self.lock();
if done
.take()
.is_some_and(|(seq, output)| state.publish(seq, output))
{
self.woke.notify_one();
}
let mut next = state.queue.pop_front();
if next.is_none() && may_park && !state.closed {
state.idle += 1;
state = self
.queued
.wait_timeout_while(state, self.linger, |s| s.queue.is_empty() && !s.closed)
.unwrap_or_else(PoisonError::into_inner)
.0;
state.idle -= 1;
next = state.queue.pop_front();
}
if next.is_none() {
state.tokens -= 1;
}
drop(state);
let Some((seq, job)) = next else {
return;
};
done = Some((seq, job.run(&self.context)));
}
}
fn help(&self) -> bool {
let Some((seq, job)) = self.lock().queue.pop_front() else {
return false;
};
let output = job.run(&self.context);
self.lock().publish(seq, output);
true
}
}
pub struct OrderedPipeline<J: OrderedJob> {
spawner: Arc<dyn CompactionSpawner>,
shared: Arc<Shared<J>>,
concurrency: usize,
capacity: usize,
next_submit: u64,
next_drain: u64,
}
impl<J: OrderedJob> OrderedPipeline<J> {
pub fn new(
spawner: Arc<dyn CompactionSpawner>,
context: J::Context,
concurrency: usize,
capacity: usize,
) -> Self {
Self::with_linger(spawner, context, concurrency, capacity, LINGER)
}
fn with_linger(
spawner: Arc<dyn CompactionSpawner>,
context: J::Context,
concurrency: usize,
capacity: usize,
linger: std::time::Duration,
) -> Self {
let capacity = capacity.max(1);
let slots = core::iter::repeat_with(|| None).take(capacity).collect();
Self {
spawner,
shared: Arc::new(Shared {
state: Mutex::new(State {
queue: VecDeque::with_capacity(capacity),
slots,
tokens: 0,
idle: 0,
closed: false,
writer_waiting: false,
}),
woke: Condvar::new(),
queued: Condvar::new(),
writer: std::thread::current().id(),
linger,
context,
}),
concurrency: concurrency.max(1),
capacity,
next_submit: 0,
next_drain: 0,
}
}
pub fn pending(&self) -> usize {
#[expect(
clippy::cast_possible_truncation,
reason = "bounded by the ring's usize length"
)]
let pending = (self.next_submit - self.next_drain) as usize;
pending
}
pub fn submit(&mut self, job: J) {
let seq = self.take_seq();
let spawn = {
let mut state = self.shared.lock();
state.queue.push_back((seq, job));
if state.idle > 0 {
self.shared.queued.notify_one();
}
let spawn = state.tokens < self.concurrency && state.queue.len() > state.idle;
if spawn {
state.tokens += 1;
}
spawn
};
if spawn {
let shared = Arc::clone(&self.shared);
self.spawner.spawn(Box::new(move || shared.work()));
}
}
pub fn submit_inline(&mut self, job: J) {
let seq = self.take_seq();
let output = job.run(&self.shared.context);
self.shared.lock().publish(seq, output);
}
fn take_seq(&mut self) -> u64 {
debug_assert!(
self.pending() < self.capacity,
"more results outstanding than the ring holds",
);
let seq = self.next_submit;
self.next_submit += 1;
seq
}
pub fn take_next(&mut self) -> Option<J::Output> {
if self.next_drain == self.next_submit {
return None;
}
let seq = self.next_drain;
loop {
let ready = self.shared.lock().slot(seq).and_then(Option::take);
if let Some(output) = ready {
self.next_drain += 1;
return Some(output);
}
if self.shared.help() {
continue;
}
let mut state = self.shared.lock();
loop {
if let Some(output) = state.slot(seq).and_then(Option::take) {
state.writer_waiting = false;
self.next_drain += 1;
return Some(output);
}
state.writer_waiting = true;
state = self
.shared
.woke
.wait(state)
.unwrap_or_else(PoisonError::into_inner);
}
}
}
}
impl<J: OrderedJob> Drop for OrderedPipeline<J> {
fn drop(&mut self) {
let abandoned = {
let mut state = self.shared.lock();
state.closed = true;
core::mem::take(&mut state.queue)
};
self.shared.queued.notify_all();
drop(abandoned);
}
}
#[cfg(test)]
mod tests;