use std::{
collections::VecDeque,
fmt,
future::Future,
io,
num::NonZeroUsize,
panic::{self, AssertUnwindSafe},
pin::Pin,
sync::{Arc, Condvar, Mutex},
task::{Context, Poll},
thread,
time::{Duration, Instant},
};
use crate::{
driver::Wakeup,
runtime::Schedule,
task::{JoinHandle, Task, new_task},
};
type JobWork = Box<dyn FnOnce() + Send + 'static>;
struct Job {
work: JobWork,
mandatory: bool,
}
pub struct BlockingPool {
inner: Arc<PoolInner>,
}
#[derive(Clone)]
pub struct BlockingPoolHandle {
inner: Arc<PoolInner>,
}
struct PoolInner {
shared: Mutex<Shared>,
condvar: Condvar,
thread_cap: NonZeroUsize,
keep_alive: Duration,
}
struct Shared {
queue: VecDeque<Job>,
num_notify: u32,
num_threads: usize,
num_idle: usize,
shutdown: bool,
shutdown_complete: bool,
worker_threads: Vec<thread::JoinHandle<()>>,
next_thread_id: usize,
}
impl BlockingPool {
pub fn new(thread_cap: usize, keep_alive: Duration) -> Self {
let thread_cap = NonZeroUsize::new(thread_cap).expect("blocking pool thread cap must be nonzero");
Self {
inner: Arc::new(PoolInner {
shared: Mutex::new(Shared {
queue: VecDeque::new(),
num_notify: 0,
num_threads: 0,
num_idle: 0,
shutdown: false,
shutdown_complete: false,
worker_threads: Vec::new(),
next_thread_id: 0,
}),
condvar: Condvar::new(),
thread_cap,
keep_alive,
}),
}
}
pub fn handle(&self) -> BlockingPoolHandle {
BlockingPoolHandle {
inner: Arc::clone(&self.inner),
}
}
pub fn dispatch<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
self.handle().dispatch(f);
}
pub(crate) fn shutdown_and_join(&self) {
self.inner.shutdown(None);
}
pub(crate) fn shutdown_with_timeout(&self, timeout: Duration) {
self.inner.shutdown(Some(timeout));
}
#[cfg(test)]
pub(crate) fn num_threads(&self) -> usize {
self.inner.shared.lock().unwrap_or_else(|e| e.into_inner()).num_threads
}
}
impl Drop for BlockingPool {
fn drop(&mut self) {
self.shutdown_and_join();
}
}
impl fmt::Debug for BlockingPool {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let shared = self.inner.shared.lock().unwrap_or_else(|e| e.into_inner());
f.debug_struct("BlockingPool")
.field("thread_cap", &self.inner.thread_cap)
.field("num_threads", &shared.num_threads)
.finish()
}
}
impl BlockingPoolHandle {
pub fn dispatch<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
if let Err(err) = self.try_dispatch(Box::new(f)) {
panic!("OS can't spawn blocking worker thread: {err}");
}
}
pub(crate) fn try_dispatch(&self, job: JobWork) -> io::Result<()> {
self.inner.dispatch(Job {
work: job,
mandatory: false,
})
}
#[allow(dead_code)]
pub(crate) fn try_dispatch_mandatory(&self, job: JobWork) -> io::Result<()> {
self.inner.dispatch(Job {
work: job,
mandatory: true,
})
}
}
impl fmt::Debug for BlockingPoolHandle {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let shared = self.inner.shared.lock().unwrap_or_else(|e| e.into_inner());
f.debug_struct("BlockingPoolHandle")
.field("thread_cap", &self.inner.thread_cap)
.field("num_threads", &shared.num_threads)
.finish()
}
}
impl PoolInner {
fn dispatch(self: &Arc<Self>, job: Job) -> io::Result<()> {
self.dispatch_with(job, |id| self.spawn_worker(id))
}
fn dispatch_with(
self: &Arc<Self>,
job: Job,
spawn_worker: impl FnOnce(usize) -> io::Result<thread::JoinHandle<()>>,
) -> io::Result<()> {
let mut shared = self.shared.lock().unwrap_or_else(|e| e.into_inner());
if shared.shutdown {
return Err(io::Error::new(io::ErrorKind::BrokenPipe, "blocking pool is shut down"));
}
shared.queue.push_back(job);
if shared.num_idle > 0 {
shared.num_idle -= 1;
shared.num_notify = shared.num_notify.saturating_add(1);
self.condvar.notify_one();
return Ok(());
}
if shared.num_threads < self.thread_cap.get() {
let id = shared.next_thread_id;
shared.next_thread_id = shared.next_thread_id.wrapping_add(1);
match spawn_worker(id) {
Ok(handle) => {
shared.num_threads += 1;
shared.worker_threads.push(handle);
Ok(())
}
Err(err) => {
if shared.num_threads > 0 {
Ok(())
} else {
let _ = shared.queue.pop_back();
Err(err)
}
}
}
} else {
Ok(())
}
}
fn spawn_worker(self: &Arc<Self>, id: usize) -> io::Result<thread::JoinHandle<()>> {
let inner = Arc::clone(self);
thread::Builder::new()
.name(format!("karmaio-blocking-{id}"))
.spawn(move || inner.worker_loop())
}
fn worker_loop(self: Arc<Self>) {
let mut shared = self.shared.lock().unwrap_or_else(|e| e.into_inner());
loop {
while let Some(job) = shared.queue.pop_front() {
if shared.shutdown && !job.mandatory {
drop(job);
continue;
}
drop(shared);
let _ = panic::catch_unwind(AssertUnwindSafe(job.work));
shared = self.shared.lock().unwrap_or_else(|e| e.into_inner());
}
if shared.shutdown {
break;
}
shared.num_idle += 1;
let (guard, timeout_result) = self
.condvar
.wait_timeout(shared, self.keep_alive)
.unwrap_or_else(|e| e.into_inner());
shared = guard;
if shared.num_notify > 0 {
shared.num_notify -= 1;
continue;
}
shared.num_idle = shared.num_idle.saturating_sub(1);
if shared.shutdown {
break;
}
if timeout_result.timed_out() && shared.queue.is_empty() {
break;
}
}
shared.num_threads = shared.num_threads.saturating_sub(1);
self.condvar.notify_all();
}
fn shutdown(&self, timeout: Option<Duration>) {
let mut shared = self.shared.lock().unwrap_or_else(|e| e.into_inner());
if shared.shutdown {
let deadline = timeout.map(|duration| Instant::now() + duration);
while !shared.shutdown_complete {
match deadline {
Some(deadline) => {
let now = Instant::now();
if now >= deadline {
break;
}
let (guard, _) = self
.condvar
.wait_timeout(shared, deadline - now)
.unwrap_or_else(|e| e.into_inner());
shared = guard;
}
None => shared = self.condvar.wait(shared).unwrap_or_else(|e| e.into_inner()),
}
}
return;
}
shared.shutdown = true;
self.condvar.notify_all();
loop {
match shared.queue.pop_front() {
Some(job) if job.mandatory => {
drop(shared);
let _ = panic::catch_unwind(AssertUnwindSafe(job.work));
shared = self.shared.lock().unwrap_or_else(|e| e.into_inner());
}
Some(job) => {
drop(job);
}
None => break,
}
}
let workers = std::mem::take(&mut shared.worker_threads);
let deadline = timeout.map(|duration| Instant::now() + duration);
while shared.num_threads > 0 {
match deadline {
Some(deadline) => {
let now = Instant::now();
if now >= deadline {
break;
}
let (guard, _) = self
.condvar
.wait_timeout(shared, deadline - now)
.unwrap_or_else(|e| e.into_inner());
shared = guard;
}
None => shared = self.condvar.wait(shared).unwrap_or_else(|e| e.into_inner()),
}
}
let all_exited = shared.num_threads == 0;
shared.shutdown_complete = true;
self.condvar.notify_all();
drop(shared);
if all_exited {
for handle in workers {
let _ = handle.join();
}
} else {
drop(workers);
}
}
}
pub(crate) struct BlockingTask<F> {
func: Option<F>,
}
impl<F> Unpin for BlockingTask<F> {}
impl<F> BlockingTask<F> {
pub(crate) fn new(func: F) -> Self {
Self { func: Some(func) }
}
}
impl<F, R> Future for BlockingTask<F>
where
F: FnOnce() -> R,
{
type Output = R;
fn poll(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<R> {
let func = self.func.take().expect("blocking task polled more than once");
Poll::Ready(func())
}
}
pub(crate) struct BlockingSchedule;
impl Schedule for BlockingSchedule {
fn schedule(&self, _task: Task<Self>) {
unreachable!("blocking tasks are never rescheduled");
}
}
pub(crate) fn spawn_blocking_task<F, R>(pool: &BlockingPoolHandle, wakeup: Wakeup, f: F) -> JoinHandle<R>
where
F: FnOnce() -> R + Send + 'static,
R: Send + 'static,
{
let (task, join_handle) = new_task(BlockingTask::new(f), BlockingSchedule);
pool.dispatch(move || {
task.run();
wakeup.wake();
});
join_handle
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::mpsc;
use std::time::Instant;
#[test]
#[should_panic(expected = "blocking pool thread cap must be nonzero")]
fn zero_thread_cap_is_rejected() {
let _pool = BlockingPool::new(0, Duration::from_secs(1));
}
#[test]
fn failed_first_worker_does_not_leave_queued_work() {
let pool = BlockingPool::new(1, Duration::from_secs(1));
let result = pool.inner.dispatch_with(
Job {
work: Box::new(|| panic!("failed worker must not run this job")),
mandatory: false,
},
|_| Err(io::Error::other("injected worker spawn failure")),
);
assert_eq!(result.unwrap_err().kind(), io::ErrorKind::Other);
{
let shared = pool.inner.shared.lock().unwrap_or_else(|error| error.into_inner());
assert!(shared.queue.is_empty());
assert_eq!(shared.num_threads, 0);
}
let (done_tx, done_rx) = mpsc::channel();
pool.dispatch(move || done_tx.send(()).expect("report recovered dispatch"));
done_rx
.recv_timeout(Duration::from_secs(2))
.expect("a later dispatch should start a worker");
}
#[test]
fn dispatch_runs_job() {
let pool = BlockingPool::new(4, Duration::from_secs(5));
let (tx, rx) = mpsc::channel();
pool.dispatch(move || {
tx.send(42).unwrap();
});
assert_eq!(rx.recv_timeout(Duration::from_secs(2)).unwrap(), 42);
}
#[test]
fn dispatch_after_shutdown_returns_an_error() {
let pool = BlockingPool::new(1, Duration::from_secs(1));
let handle = pool.handle();
drop(pool);
let result = handle.try_dispatch(Box::new(|| {}));
assert_eq!(result.unwrap_err().kind(), io::ErrorKind::BrokenPipe);
}
#[test]
fn shutdown_discards_queued_jobs_and_joins_running_work() {
struct DropFlag(Arc<AtomicBool>);
impl Drop for DropFlag {
fn drop(&mut self) {
self.0.store(true, Ordering::Release);
}
}
let pool = BlockingPool::new(1, Duration::from_secs(5));
let (started_tx, started_rx) = mpsc::channel();
let (release_tx, release_rx) = mpsc::channel();
pool.dispatch(move || {
started_tx.send(()).expect("report running job");
release_rx.recv().expect("release running job");
});
started_rx
.recv_timeout(Duration::from_secs(2))
.expect("worker should start the first job");
let queued_job_dropped = Arc::new(AtomicBool::new(false));
let drop_flag = DropFlag(Arc::clone(&queued_job_dropped));
pool.dispatch(move || drop(drop_flag));
let releaser = thread::spawn(move || {
thread::sleep(Duration::from_millis(25));
release_tx.send(()).expect("release worker");
});
pool.shutdown_and_join();
releaser.join().expect("release thread should finish");
assert!(queued_job_dropped.load(Ordering::Acquire));
pool.shutdown_and_join();
}
#[test]
fn many_jobs_complete() {
let pool = BlockingPool::new(4, Duration::from_secs(5));
let counter = Arc::new(AtomicUsize::new(0));
let (tx, rx) = mpsc::channel();
let n = 32usize;
for _ in 0..n {
let counter = Arc::clone(&counter);
let tx = tx.clone();
pool.dispatch(move || {
counter.fetch_add(1, Ordering::SeqCst);
let _ = tx.send(());
});
}
drop(tx);
for _ in 0..n {
rx.recv_timeout(Duration::from_secs(5)).expect("job timed out");
}
assert_eq!(counter.load(Ordering::SeqCst), n);
}
#[test]
fn jobs_queue_when_at_cap() {
let pool = BlockingPool::new(2, Duration::from_secs(5));
let (start_tx, start_rx) = mpsc::channel();
let (done_tx, done_rx) = mpsc::channel();
for _ in 0..2 {
let start_tx = start_tx.clone();
let done_tx = done_tx.clone();
pool.dispatch(move || {
start_tx.send(()).unwrap();
thread::sleep(Duration::from_millis(100));
done_tx.send(()).unwrap();
});
}
start_rx.recv_timeout(Duration::from_secs(2)).unwrap();
start_rx.recv_timeout(Duration::from_secs(2)).unwrap();
assert!(pool.num_threads() <= 2);
for _ in 0..4 {
let done_tx = done_tx.clone();
pool.dispatch(move || {
done_tx.send(()).unwrap();
});
}
drop(done_tx);
for _ in 0..6 {
done_rx
.recv_timeout(Duration::from_secs(5))
.expect("queued job should complete");
}
}
#[test]
fn panic_in_job_does_not_kill_pool() {
let pool = BlockingPool::new(2, Duration::from_secs(5));
pool.dispatch(|| panic!("boom"));
let (tx, rx) = mpsc::channel();
pool.dispatch(move || {
tx.send(7).unwrap();
});
assert_eq!(rx.recv_timeout(Duration::from_secs(2)).unwrap(), 7);
}
#[test]
fn idle_workers_eventually_exit() {
let pool = BlockingPool::new(4, Duration::from_millis(80));
let (tx, rx) = mpsc::channel();
pool.dispatch(move || {
tx.send(()).unwrap();
});
rx.recv_timeout(Duration::from_secs(2)).unwrap();
let deadline = Instant::now() + Duration::from_secs(3);
while pool.num_threads() > 0 {
if Instant::now() > deadline {
panic!("workers did not idle-exit; still {}", pool.num_threads());
}
thread::sleep(Duration::from_millis(20));
}
}
#[test]
fn shutdown_runs_queued_mandatory_jobs() {
let pool = BlockingPool::new(1, Duration::from_secs(5));
let (started_tx, started_rx) = mpsc::channel();
let (release_tx, release_rx) = mpsc::channel::<()>();
let handle = pool.handle();
pool.dispatch(move || {
started_tx.send(()).unwrap();
release_rx.recv().ok();
});
started_rx
.recv_timeout(Duration::from_secs(2))
.expect("worker should start the first job");
let (ran_tx, ran_rx) = mpsc::channel();
handle
.try_dispatch_mandatory(Box::new(move || {
ran_tx.send(()).unwrap();
}))
.expect("mandatory dispatch should succeed");
let shutdown_thread = thread::spawn(move || pool.shutdown_and_join());
ran_rx
.recv_timeout(Duration::from_secs(2))
.expect("queued mandatory job should run at shutdown");
drop(release_tx);
shutdown_thread.join().expect("shutdown should finish");
}
#[test]
fn mandatory_dispatch_rejected_after_shutdown() {
let pool = BlockingPool::new(1, Duration::from_secs(1));
let handle = pool.handle();
drop(pool);
let result = handle.try_dispatch_mandatory(Box::new(|| {}));
assert_eq!(result.unwrap_err().kind(), io::ErrorKind::BrokenPipe);
}
#[test]
fn shutdown_with_zero_timeout_detaches_stuck_worker() {
let pool = BlockingPool::new(1, Duration::from_secs(5));
let (started_tx, started_rx) = mpsc::channel();
let (release_tx, release_rx) = mpsc::channel::<()>();
pool.dispatch(move || {
started_tx.send(()).unwrap();
release_rx.recv().ok();
});
started_rx
.recv_timeout(Duration::from_secs(2))
.expect("worker should start the job");
let start = Instant::now();
pool.shutdown_with_timeout(Duration::from_millis(0));
assert!(start.elapsed() < Duration::from_secs(1));
drop(release_tx);
}
#[test]
fn spawn_blocking_task_runs_and_wakes() {
let pool = BlockingPool::new(2, Duration::from_secs(5));
let wakes = Arc::new(AtomicUsize::new(0));
let wakeup = crate::driver::Wakeup::new({
let wakes = Arc::clone(&wakes);
move || {
wakes.fetch_add(1, Ordering::Relaxed);
}
});
let handle = spawn_blocking_task(&pool.handle(), wakeup, || 42usize);
assert!(!handle.is_finished());
let start = Instant::now();
loop {
if handle.is_finished() {
break;
}
if start.elapsed() > Duration::from_secs(2) {
panic!("blocking task did not finish");
}
thread::sleep(Duration::from_millis(1));
}
assert!(wakes.load(Ordering::Relaxed) >= 1);
}
#[test]
fn spawn_blocking_task_reports_panics() {
let pool = BlockingPool::new(2, Duration::from_secs(5));
let wakeup = crate::driver::Wakeup::new(|| {});
let handle = spawn_blocking_task(&pool.handle(), wakeup, || -> usize {
panic!("boom");
});
let start = Instant::now();
loop {
if handle.is_finished() {
break;
}
if start.elapsed() > Duration::from_secs(2) {
panic!("blocking task did not finish");
}
thread::sleep(Duration::from_millis(1));
}
}
}