use std::collections::VecDeque;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Condvar, Mutex};
use std::thread::JoinHandle;
use crate::governor::OpClass;
pub fn default_workers(class: OpClass, cores: usize) -> usize {
match class {
OpClass::LsRemote => (cores * 4).clamp(16, 256),
OpClass::Fetch => (cores * 4).clamp(16, 256),
OpClass::Clone => (cores * 4).clamp(16, 256),
OpClass::Push => (cores * 4).clamp(16, 256),
OpClass::Gc => 2,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SubmitError {
QueueFull,
ShuttingDown,
}
impl std::fmt::Display for SubmitError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SubmitError::QueueFull => {
f.write_str("server busy: the queue for this operation class is full")
}
SubmitError::ShuttingDown => f.write_str("server busy: shutting down"),
}
}
}
struct Queue<T> {
items: Mutex<VecDeque<T>>,
ready: Condvar,
stopping: AtomicBool,
depth: usize,
#[cfg(test)]
before_wait: std::sync::OnceLock<Box<dyn Fn() + Send + Sync>>,
}
impl<T> Queue<T> {
fn push(&self, item: T) -> Result<usize, SubmitError> {
let mut q = self.items.lock().expect("pipeline queue poisoned");
if self.stopping.load(Ordering::Acquire) {
return Err(SubmitError::ShuttingDown);
}
if q.len() >= self.depth {
return Err(SubmitError::QueueFull);
}
q.push_back(item);
let queued = q.len();
drop(q);
self.ready.notify_one();
Ok(queued)
}
fn pop_blocking(&self) -> Option<T> {
let mut q = self.items.lock().expect("pipeline queue poisoned");
loop {
if let Some(item) = q.pop_front() {
return Some(item);
}
if self.stopping.load(Ordering::Acquire) {
return None;
}
#[cfg(test)]
if let Some(hook) = self.before_wait.get() {
hook();
}
q = self.ready.wait(q).expect("pipeline queue poisoned");
}
}
}
pub struct Pipeline<T: Send + 'static> {
class: OpClass,
queue: Arc<Queue<T>>,
workers: Mutex<Vec<JoinHandle<()>>>,
served: Arc<AtomicU64>,
}
impl<T: Send + 'static> Pipeline<T> {
pub fn start<F>(class: OpClass, workers: usize, depth: usize, run: F) -> Pipeline<T>
where
F: Fn(T) + Send + Sync + 'static,
{
let queue = Arc::new(Queue {
items: Mutex::new(VecDeque::with_capacity(depth)),
ready: Condvar::new(),
stopping: AtomicBool::new(false),
depth,
#[cfg(test)]
before_wait: std::sync::OnceLock::new(),
});
let served = Arc::new(AtomicU64::new(0));
let run = Arc::new(run);
let mut handles = Vec::with_capacity(workers);
for i in 0..workers {
let q = Arc::clone(&queue);
let run = Arc::clone(&run);
let served = Arc::clone(&served);
let name = format!("gunnar-{class:?}-{i}").to_lowercase();
let h = std::thread::Builder::new()
.name(name)
.spawn(move || {
while let Some(item) = q.pop_blocking() {
served.fetch_add(1, Ordering::Relaxed);
run(item);
}
})
.expect("spawning a pipeline worker at boot");
handles.push(h);
}
Pipeline {
class,
queue,
workers: Mutex::new(handles),
served,
}
}
#[allow(clippy::type_complexity)]
pub fn start_with_arena<A, M, F>(
class: OpClass,
workers: usize,
depth: usize,
make: M,
run: F,
) -> Pipeline<T>
where
A: Send + 'static,
M: Fn(usize) -> A + Send + Sync + 'static,
F: Fn(&mut A, T) + Send + Sync + 'static,
{
let queue = Arc::new(Queue {
items: Mutex::new(VecDeque::with_capacity(depth)),
ready: Condvar::new(),
stopping: AtomicBool::new(false),
depth,
#[cfg(test)]
before_wait: std::sync::OnceLock::new(),
});
let served = Arc::new(AtomicU64::new(0));
let run = Arc::new(run);
let make = Arc::new(make);
let mut handles = Vec::with_capacity(workers);
for i in 0..workers {
let q = Arc::clone(&queue);
let run = Arc::clone(&run);
let make = Arc::clone(&make);
let served = Arc::clone(&served);
let name = format!("gunnar-{class:?}-{i}").to_lowercase();
let h = std::thread::Builder::new()
.name(name)
.spawn(move || {
let mut arena = make(i);
while let Some(item) = q.pop_blocking() {
served.fetch_add(1, Ordering::Relaxed);
run(&mut arena, item);
}
})
.expect("spawning a pipeline worker at boot");
handles.push(h);
}
Pipeline {
class,
queue,
workers: Mutex::new(handles),
served,
}
}
pub fn submit(&self, item: T) -> Result<usize, SubmitError> {
self.queue.push(item)
}
pub fn class(&self) -> OpClass {
self.class
}
pub fn served(&self) -> u64 {
self.served.load(Ordering::Relaxed)
}
pub fn queued(&self) -> usize {
self.queue
.items
.lock()
.expect("pipeline queue poisoned")
.len()
}
pub fn workers(&self) -> usize {
self.workers
.lock()
.expect("pipeline workers poisoned")
.len()
}
pub fn shutdown(&self) {
{
let _q = self.queue.items.lock().expect("pipeline queue poisoned");
self.queue.stopping.store(true, Ordering::Release);
}
self.queue.ready.notify_all();
let mut handles = self.workers.lock().expect("pipeline workers poisoned");
for h in handles.drain(..) {
let _ = h.join();
}
}
}
impl<T: Send + 'static> Drop for Pipeline<T> {
fn drop(&mut self) {
self.shutdown();
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
use std::sync::mpsc;
use std::time::Duration;
#[test]
fn operations_reuse_the_standing_fan_rather_than_a_thread_each() {
let (tx, rx) = mpsc::channel();
let p = Pipeline::start(OpClass::Clone, 3, 64, move |_op: u32| {
let id = std::thread::current().id();
tx.send(id).expect("collector alive");
});
for i in 0..12 {
p.submit(i).expect("queue has room");
}
let mut seen: HashSet<std::thread::ThreadId> = HashSet::new();
for _ in 0..12 {
seen.insert(rx.recv_timeout(Duration::from_secs(5)).expect("op ran"));
}
assert!(
seen.len() <= 3,
"{} operations ran on {} distinct threads — the fan is being built per request",
12,
seen.len()
);
assert_eq!(p.served(), 12);
}
#[test]
fn the_fan_is_running_before_the_first_operation() {
let p: Pipeline<u32> = Pipeline::start(OpClass::Fetch, 4, 16, |_| {});
assert_eq!(
p.workers(),
4,
"the fan has {} workers before any operation was submitted",
p.workers()
);
assert_eq!(p.served(), 0, "nothing has been served yet");
}
#[test]
fn a_full_queue_refuses_rather_than_growing() {
let (release, held) = mpsc::channel::<()>();
let held = Arc::new(Mutex::new(held));
let p = Pipeline::start(OpClass::Clone, 1, 4, move |_op: u32| {
let _ = held
.lock()
.expect("held")
.recv_timeout(Duration::from_secs(10));
});
let mut accepted = 0usize;
let mut refused = 0usize;
for i in 0..40 {
match p.submit(i) {
Ok(_) => accepted += 1,
Err(SubmitError::QueueFull) => refused += 1,
Err(e) => panic!("unexpected refusal: {e}"),
}
}
assert!(
refused > 0,
"the queue accepted {accepted} items into a 4-deep queue"
);
assert!(
accepted <= 4 + 1,
"accepted {accepted}, which is more than the depth plus the one in service"
);
for _ in 0..accepted {
let _ = release.send(());
}
}
#[test]
fn shutdown_joins_every_worker() {
let p: Pipeline<u32> = Pipeline::start(OpClass::Gc, 3, 8, |_| {});
let q = Arc::clone(&p.queue);
assert!(
Arc::strong_count(&q) > 1,
"workers hold the queue while running"
);
p.shutdown();
assert_eq!(p.workers(), 0, "handles were not drained");
assert_eq!(
Arc::strong_count(&q),
2,
"a worker outlived the pipeline: {} strong refs on the queue after shutdown",
Arc::strong_count(&q)
);
}
#[test]
fn a_shutdown_in_the_window_before_a_worker_waits_still_joins_it() {
let p: Pipeline<u32> = Pipeline::start(OpClass::Gc, 1, 8, |_| {});
let (in_window, window) = mpsc::channel::<()>();
let (go, gate) = mpsc::channel::<()>();
let gate = Mutex::new(Some(gate));
let in_window = Mutex::new(in_window);
let _ = p.queue.before_wait.set(Box::new(move || {
if let Some(g) = gate.lock().unwrap().take() {
let _ = in_window.lock().unwrap().send(());
let _ = g.recv_timeout(Duration::from_secs(10));
}
}));
p.submit(1).expect("queued");
window
.recv_timeout(Duration::from_secs(10))
.expect("the worker reached the window");
let (done, ended) = mpsc::channel::<()>();
std::thread::spawn(move || {
p.shutdown();
let _ = done.send(());
});
std::thread::sleep(Duration::from_millis(300));
let _ = go.send(());
assert!(
ended.recv_timeout(Duration::from_secs(10)).is_ok(),
"shutdown never joined: the worker waits on a wake-up that came before it waited"
);
}
#[test]
fn the_arena_is_built_once_per_worker_and_reused_by_every_operation() {
let built = Arc::new(AtomicU64::new(0));
let b = Arc::clone(&built);
let (tx, rx) = mpsc::channel();
let p = Pipeline::start_with_arena(
OpClass::Clone,
2,
64,
move |_i| {
b.fetch_add(1, Ordering::Relaxed);
Vec::<u8>::with_capacity(4096)
},
move |arena: &mut Vec<u8>, _op: u32| {
let before = arena.capacity();
arena.clear();
arena.extend_from_slice(&[7u8; 1024]);
tx.send((before, arena.capacity()))
.expect("collector alive");
},
);
for i in 0..20 {
p.submit(i).expect("queue has room");
}
let mut reused = 0usize;
for _ in 0..20 {
let (before, _after) = rx.recv_timeout(Duration::from_secs(5)).expect("op ran");
if before >= 4096 {
reused += 1;
}
}
let n = built.load(Ordering::Relaxed);
assert!(
(1..=2).contains(&n),
"the arena was rebuilt: {n} constructions for 20 operations across 2 workers"
);
assert_eq!(
reused, 20,
"only {reused} of 20 operations saw a warm arena — capacity is being thrown away"
);
}
#[test]
fn transfer_fans_are_wide_and_gc_stays_small() {
for cores in [1usize, 4, 32, 256] {
assert_eq!(
default_workers(OpClass::Gc, cores),
2,
"gc at {cores} cores"
);
for class in [
OpClass::LsRemote,
OpClass::Fetch,
OpClass::Clone,
OpClass::Push,
] {
let n = default_workers(class, cores);
assert!(
n >= 16,
"{class:?} at {cores} cores got {n} workers — 102 concurrent \
clients against a fan this small queue until they time out"
);
assert!(n <= 256, "{class:?} at {cores} cores is unbounded: {n}");
}
}
assert!(
default_workers(OpClass::Clone, 32) >= 102,
"the fan must hold multiuser_scaling's 102 clients without queueing"
);
}
}
pub type Job = Box<dyn FnOnce() + Send + 'static>;
pub struct Pipelines {
ls_remote: Pipeline<Job>,
fetch: Pipeline<Job>,
clone: Pipeline<Job>,
push: Pipeline<Job>,
gc: Pipeline<Job>,
}
impl Pipelines {
pub fn start(cores: usize, depth: usize) -> Pipelines {
fn fan(class: OpClass, cores: usize, depth: usize) -> Pipeline<Job> {
Pipeline::start(class, default_workers(class, cores), depth, |job: Job| {
job()
})
}
Pipelines {
ls_remote: fan(OpClass::LsRemote, cores, depth),
fetch: fan(OpClass::Fetch, cores, depth),
clone: fan(OpClass::Clone, cores, depth),
push: fan(OpClass::Push, cores, depth),
gc: fan(OpClass::Gc, cores, depth),
}
}
fn of(&self, class: OpClass) -> &Pipeline<Job> {
match class {
OpClass::LsRemote => &self.ls_remote,
OpClass::Fetch => &self.fetch,
OpClass::Clone => &self.clone,
OpClass::Push => &self.push,
OpClass::Gc => &self.gc,
}
}
pub fn submit(&self, class: OpClass, job: Job) -> Result<usize, SubmitError> {
self.of(class).submit(job)
}
pub fn served(&self, class: OpClass) -> u64 {
self.of(class).served()
}
pub fn queued(&self, class: OpClass) -> usize {
self.of(class).queued()
}
pub fn workers(&self, class: OpClass) -> usize {
self.of(class).workers()
}
}
static FANS: std::sync::OnceLock<Pipelines> = std::sync::OnceLock::new();
pub fn fans() -> &'static Pipelines {
FANS.get_or_init(|| {
let cores = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(4);
Pipelines::start(cores, 64)
})
}
#[cfg(test)]
mod pipelines_tests {
use super::*;
use std::time::Duration;
#[test]
fn every_class_has_a_standing_fan_at_boot() {
let p = Pipelines::start(32, 16);
for class in [
OpClass::LsRemote,
OpClass::Fetch,
OpClass::Clone,
OpClass::Push,
OpClass::Gc,
] {
assert!(
p.workers(class) > 0,
"the {class:?} fan has 0 workers before any request"
);
assert_eq!(p.served(class), 0, "{class:?} served something at boot");
}
assert_eq!(p.workers(OpClass::Gc), 2, "gc must stay small");
assert!(
p.workers(OpClass::LsRemote) > p.workers(OpClass::Gc),
"ls-remote must out-scale gc"
);
}
#[test]
fn every_caller_gets_the_same_process_wide_fans() {
let a = fans();
let b = fans();
assert!(
std::ptr::eq(a, b),
"two callers got two different fan sets — SSH and HTTP would each have their own"
);
}
#[test]
fn the_occupancy_counters_are_not_constants() {
let (release, held) = std::sync::mpsc::channel::<()>();
let held = Arc::new(Mutex::new(held));
let p: Pipeline<u32> = Pipeline::start(OpClass::Clone, 1, 64, move |_| {
let _ = held
.lock()
.expect("held")
.recv_timeout(Duration::from_secs(10));
});
let mut deepest = 0usize;
for i in 0..24 {
if let Ok(depth) = p.submit(i) {
deepest = deepest.max(depth);
}
}
assert!(
deepest > 1,
"the queue never reported a waiting item across 24 submissions against \
1 worker — the depth this logs is a constant"
);
assert!(
p.queued() > 0,
"queued() says {} with {deepest} known to have piled up",
p.queued()
);
assert_eq!(p.workers(), 1, "workers() lost count of its own fan");
for _ in 0..24 {
let _ = release.send(());
}
let mut waited = 0;
while p.served() < 24 && waited < 200 {
std::thread::sleep(Duration::from_millis(25));
waited += 1;
}
assert_eq!(
p.served(),
24,
"served() reports {} after 24 jobs were released",
p.served()
);
}
#[test]
fn a_job_runs_on_its_own_class_fan() {
let p = Pipelines::start(8, 16);
let (tx, rx) = std::sync::mpsc::channel();
p.submit(OpClass::Push, Box::new(move || tx.send(()).unwrap()))
.expect("queue has room");
rx.recv_timeout(std::time::Duration::from_secs(5))
.expect("the job ran on the fan");
assert_eq!(p.served(OpClass::Push), 1);
assert_eq!(p.served(OpClass::Clone), 0, "it ran on the wrong fan");
}
}