use std::collections::{HashMap, HashSet, VecDeque};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Condvar, Mutex};
use std::thread;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct JobKey {
pub owner: u64,
pub slot: u32,
}
impl JobKey {
pub const fn new(owner: u64, slot: u32) -> Self {
Self { owner, slot }
}
}
pub type Work = Box<dyn FnOnce(&AtomicBool) + Send>;
struct Queued {
work: Work,
cancel: Arc<AtomicBool>,
}
#[derive(Default)]
struct Queue {
waiting: HashMap<JobKey, Queued>,
order: VecDeque<JobKey>,
busy: HashSet<JobKey>,
closed: bool,
}
impl Queue {
fn take(&mut self) -> Option<(JobKey, Queued)> {
let at = self.order.iter().position(|k| !self.busy.contains(k))?;
let key = self.order.remove(at)?;
let queued = self.waiting.remove(&key)?;
self.busy.insert(key);
Some((key, queued))
}
}
struct Shared {
queue: Mutex<Queue>,
ready: Condvar,
}
pub struct WorkPool {
shared: Arc<Shared>,
latest: Mutex<HashMap<JobKey, Arc<AtomicBool>>>,
}
impl WorkPool {
pub fn new(threads: usize) -> Self {
let shared = Arc::new(Shared { queue: Mutex::new(Queue::default()), ready: Condvar::new() });
for i in 0..threads.max(1) {
let shared = shared.clone();
thread::Builder::new().name(format!("dspk-app-work-{i}")).spawn(move || work(&shared)).expect("failed to spawn a work thread");
}
Self { shared, latest: Mutex::default() }
}
pub fn default_threads() -> usize {
thread::available_parallelism().map_or(2, |n| (n.get() / 2).clamp(2, 4))
}
pub fn request(&self, key: JobKey, work: Work) {
let cancel = Arc::new(AtomicBool::new(false));
if let Some(previous) = self.latest.lock().expect("work pool lock").insert(key, cancel.clone()) {
previous.store(true, Ordering::Relaxed);
}
let mut q = self.shared.queue.lock().expect("work pool lock");
if q.waiting.insert(key, Queued { work, cancel }).is_none() {
q.order.push_back(key);
}
drop(q);
self.shared.ready.notify_one();
}
pub fn forget(&self, owner: u64) {
self.latest.lock().expect("work pool lock").retain(|k, flag| {
let mine = k.owner == owner;
if mine {
flag.store(true, Ordering::Relaxed);
}
!mine
});
let mut q = self.shared.queue.lock().expect("work pool lock");
q.waiting.retain(|k, _| k.owner != owner);
q.order.retain(|k| k.owner != owner);
}
}
impl Drop for WorkPool {
fn drop(&mut self) {
self.shared.queue.lock().expect("work pool lock").closed = true;
self.shared.ready.notify_all();
}
}
fn work(shared: &Shared) {
loop {
let (key, Queued { work, cancel }) = {
let mut q = shared.queue.lock().expect("work pool lock");
loop {
if q.closed {
return;
}
if let Some(next) = q.take() {
break next;
}
q = shared.ready.wait(q).expect("work pool lock");
}
};
if !cancel.load(Ordering::Relaxed) {
work(&cancel);
}
let mut q = shared.queue.lock().expect("work pool lock");
q.busy.remove(&key);
let more = q.waiting.contains_key(&key);
drop(q);
if more {
shared.ready.notify_one();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::BTreeMap;
use std::sync::mpsc;
use std::time::Duration;
fn job(value: u32, key: JobKey, tx: &mpsc::Sender<(JobKey, u32)>) -> Work {
let tx = Mutex::new(tx.clone());
Box::new(move |_| tx.lock().unwrap().send((key, value)).unwrap())
}
#[test]
fn test_runs_the_latest_job_per_key_in_order() {
let (tx, rx) = mpsc::channel();
let pool = WorkPool::new(3);
let (a, b) = (JobKey::new(1, 0), JobKey::new(1, 1));
for v in [100u32, 200, 300, 400] {
pool.request(a, job(v, a, &tx));
pool.request(b, job(v + 1, b, &tx));
}
let mut last = BTreeMap::new();
while let Ok((key, v)) = rx.recv_timeout(Duration::from_secs(5)) {
if let Some(prev) = last.insert(key, v) {
assert!(v > prev, "results of one key arrive in request order");
}
if last.get(&a) == Some(&400) && last.get(&b) == Some(&401) {
break;
}
}
assert_eq!((last.get(&a), last.get(&b)), (Some(&400), Some(&401)));
}
#[test]
fn test_newer_job_cancels_the_one_in_progress() {
let (tx, rx) = mpsc::channel();
let pool = WorkPool::new(2);
let key = JobKey::new(1, 0);
let (started_tx, started_rx) = mpsc::channel();
let slow_tx = Mutex::new(tx.clone());
pool.request(
key,
Box::new(move |cancel| {
started_tx.send(()).unwrap();
while !cancel.load(Ordering::Relaxed) {
thread::sleep(Duration::from_millis(1));
}
let _ = &slow_tx;
}),
);
started_rx.recv_timeout(Duration::from_secs(5)).unwrap();
pool.request(key, job(7, key, &tx));
assert_eq!(rx.recv_timeout(Duration::from_secs(5)).unwrap(), (key, 7), "only the newer job reports");
}
#[test]
fn test_forget_drops_an_owners_waiting_jobs() {
let (tx, rx) = mpsc::channel();
let pool = WorkPool::new(1);
let (gate_tx, gate_rx) = mpsc::channel::<()>();
let gate = Mutex::new(gate_rx);
pool.request(JobKey::new(1, 0), Box::new(move |_| gate.lock().unwrap().recv().unwrap()));
pool.request(JobKey::new(2, 0), job(5, JobKey::new(2, 0), &tx));
pool.request(JobKey::new(2, 1), job(6, JobKey::new(2, 1), &tx));
pool.forget(2);
pool.request(JobKey::new(3, 0), job(9, JobKey::new(3, 0), &tx));
gate_tx.send(()).unwrap();
assert_eq!(rx.recv_timeout(Duration::from_secs(5)).unwrap(), (JobKey::new(3, 0), 9));
assert!(rx.recv_timeout(Duration::from_millis(100)).is_err());
}
}