use crate::{BlockingResult, Error, ErrorKind, Notifier, OpId, Payload, Result, queue::Queue};
#[cfg(not(target_arch = "wasm32"))]
use std::sync::atomic::AtomicUsize;
use std::{
net::{SocketAddr, ToSocketAddrs},
sync::{
Arc,
atomic::{AtomicBool, Ordering},
},
time::Duration,
};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct PoolConfig {
pub threads: usize,
pub queue_capacity: usize,
pub long_threads_max: usize,
pub long_idle_timeout: Duration,
}
impl Default for PoolConfig {
fn default() -> Self {
Self {
threads: 4,
queue_capacity: 1024,
long_threads_max: 512,
long_idle_timeout: Duration::from_secs(10),
}
}
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
pub enum Occupancy {
#[default]
Bounded,
Long,
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
pub struct PoolStats {
pub threads: usize,
pub busy: usize,
pub queued: usize,
pub reserved: usize,
pub long_threads: usize,
pub long_busy: usize,
}
pub fn pool_stats() -> PoolStats {
#[cfg(not(target_arch = "wasm32"))]
{
native::stats()
}
#[cfg(target_arch = "wasm32")]
{
PoolStats::default()
}
}
pub struct Cancellation {
cancel: Arc<AtomicBool>,
port: Arc<WorkPort>,
}
impl Cancellation {
pub(crate) fn new(cancel: Arc<AtomicBool>, port: Arc<WorkPort>) -> Self {
Self { cancel, port }
}
pub fn requested(&self) -> bool {
self.cancel.load(Ordering::Acquire) || self.port.is_closed()
}
}
#[derive(Debug)]
pub struct DnsRequest {
pub host: String,
pub port: u16,
}
pub(crate) enum WorkOutput {
ExternalWait(crate::WaitResult),
#[cfg(not(target_arch = "wasm32"))]
Fs(crate::fs::Reply),
Blocking(Payload),
Resolved(Vec<SocketAddr>),
}
pub(crate) struct WorkResult {
pub op: OpId,
pub result: Result<WorkOutput>,
}
pub(crate) struct WorkPort {
queue: Queue<WorkResult>,
notifier: Notifier,
closed: AtomicBool,
}
impl WorkPort {
pub fn new(capacity: usize, notifier: Notifier) -> Arc<Self> {
Arc::new(Self {
queue: Queue::new(capacity.max(2).next_power_of_two()),
notifier,
closed: AtomicBool::new(false),
})
}
pub fn is_empty(&self) -> bool {
self.queue.is_empty()
}
pub fn pop(&self) -> Option<WorkResult> {
self.queue.pop()
}
pub fn close(&self) {
self.closed.store(true, Ordering::Release);
}
pub(crate) fn is_closed(&self) -> bool {
self.closed.load(Ordering::Acquire)
}
#[cfg(target_arch = "wasm32")]
pub(crate) fn complete_during_turn(&self, result: WorkResult) {
if !self.closed.load(Ordering::Acquire) {
assert!(
self.queue.push(result).is_ok(),
"local completion credit invariant"
);
}
}
pub(crate) fn complete(&self, result: WorkResult) {
if self.closed.load(Ordering::Acquire) {
return;
}
assert!(
self.queue.push(result).is_ok(),
"blocking completion credit invariant"
);
let _ = self.notifier.notify();
}
}
#[cfg(not(target_arch = "wasm32"))]
pub(crate) trait ReusableWork: Send + Sync {
fn run(&self);
}
#[cfg(not(target_arch = "wasm32"))]
mod native {
use super::*;
use std::{
collections::VecDeque,
panic::{AssertUnwindSafe, catch_unwind},
sync::{Condvar, Mutex, OnceLock},
thread,
};
pub(super) struct Job {
op: OpId,
cancel: Arc<AtomicBool>,
port: Arc<WorkPort>,
f: Box<dyn FnOnce() -> Result<WorkOutput> + Send>,
}
enum Task {
Boxed(Job),
Reusable(Arc<dyn ReusableWork>),
}
struct Busy<'a>(&'a AtomicUsize);
impl Busy<'_> {
fn start(counter: &AtomicUsize) -> Busy<'_> {
counter.fetch_add(1, Ordering::Relaxed);
Busy(counter)
}
}
impl Drop for Busy<'_> {
fn drop(&mut self) {
self.0.fetch_sub(1, Ordering::Relaxed);
}
}
fn run(job: Job, busy: &AtomicUsize) {
let _busy = Busy::start(busy);
let result = if job.cancel.load(Ordering::Acquire) {
Err(Error::new(ErrorKind::Cancelled))
} else {
catch_unwind(AssertUnwindSafe(job.f))
.unwrap_or_else(|_| Err(Error::new(ErrorKind::Other)))
};
job.port.complete(WorkResult { op: job.op, result });
}
struct State {
jobs: Mutex<VecDeque<Task>>,
reserved: AtomicUsize,
queued: AtomicUsize,
busy: AtomicUsize,
ready: Condvar,
stopping: AtomicBool,
started: Mutex<usize>,
started_changed: Condvar,
}
struct Pool {
state: Arc<State>,
long: Arc<LongSet>,
config: PoolConfig,
}
static POOL: OnceLock<Result<Pool>> = OnceLock::new();
fn start(config: PoolConfig) -> Result<Pool> {
let state = Arc::new(State {
jobs: Mutex::new(VecDeque::with_capacity(config.queue_capacity)),
reserved: AtomicUsize::new(0),
queued: AtomicUsize::new(0),
busy: AtomicUsize::new(0),
ready: Condvar::new(),
stopping: AtomicBool::new(false),
started: Mutex::new(0),
started_changed: Condvar::new(),
});
let mut handles = Vec::with_capacity(config.threads);
for i in 0..config.threads {
let s = state.clone();
let worker = thread::Builder::new()
.name(format!("turnloop-blocking-{i}"))
.spawn(move || {
*s.started
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) += 1;
s.started_changed.notify_all();
loop {
let job = {
let mut jobs = s
.jobs
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
while jobs.is_empty() && !s.stopping.load(Ordering::Acquire) {
jobs = s
.ready
.wait(jobs)
.unwrap_or_else(std::sync::PoisonError::into_inner);
}
if s.stopping.load(Ordering::Acquire) {
return;
}
let task = jobs.pop_front().expect("nonempty job queue");
s.queued.store(jobs.len(), Ordering::Relaxed);
task
};
match job {
Task::Boxed(job) => run(job, &s.busy),
Task::Reusable(work) => {
let _busy = Busy::start(&s.busy);
work.run();
}
}
}
});
match worker {
Ok(handle) => handles.push(handle),
Err(e) => {
state.stopping.store(true, Ordering::Release);
state.ready.notify_all();
for h in handles {
let _ = h.join();
}
return Err(e.into());
}
}
}
let mut started = state
.started
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
while *started < config.threads {
started = state
.started_changed
.wait(started)
.unwrap_or_else(std::sync::PoisonError::into_inner);
}
drop(started);
Ok(Pool {
state,
long: LongSet::new(config.long_threads_max, config.long_idle_timeout),
config,
})
}
pub(super) fn submit(
config: PoolConfig,
occupancy: Occupancy,
op: OpId,
cancel: Arc<AtomicBool>,
port: Arc<WorkPort>,
f: Box<dyn FnOnce() -> Result<WorkOutput> + Send>,
) -> Result<()> {
let pool = pool(config)?;
let job = Job {
op,
cancel,
port,
f,
};
match occupancy {
Occupancy::Long => pool.long.submit(job),
Occupancy::Bounded => {
let mut jobs = lock(pool);
if jobs.len() + pool.state.reserved.load(Ordering::Relaxed)
>= pool.config.queue_capacity
{
return Err(Error::new(ErrorKind::ResourceLimit));
}
jobs.push_back(Task::Boxed(job));
pool.state.queued.store(jobs.len(), Ordering::Relaxed);
drop(jobs);
pool.state.ready.notify_one();
Ok(())
}
}
}
pub(super) fn stats() -> PoolStats {
let Some(Ok(pool)) = POOL.get().map(|p| p.as_ref()) else {
return PoolStats::default();
};
let (long_threads, long_busy) = pool.long.stats();
PoolStats {
threads: pool.config.threads,
busy: pool.state.busy.load(Ordering::Relaxed),
queued: pool.state.queued.load(Ordering::Relaxed),
reserved: pool.state.reserved.load(Ordering::Relaxed),
long_threads,
long_busy,
}
}
fn pool(config: PoolConfig) -> Result<&'static Pool> {
if config.threads == 0 || config.queue_capacity == 0 {
return Err(Error::new(ErrorKind::InvalidInput));
}
let pool = POOL
.get_or_init(|| start(config))
.as_ref()
.map_err(|&e| e)?;
if pool.config != config {
return Err(Error::new(ErrorKind::InvalidInput));
}
Ok(pool)
}
fn lock(pool: &Pool) -> std::sync::MutexGuard<'_, VecDeque<Task>> {
pool.state
.jobs
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
#[cfg(any(turnloop_backend = "kqueue", turnloop_backend = "epoll"))]
pub(crate) fn reusable(config: PoolConfig, work: Arc<dyn ReusableWork>) -> Result<()> {
let pool = pool(config)?;
let mut jobs = lock(pool);
if jobs.len() + pool.state.reserved.load(Ordering::Relaxed) >= pool.config.queue_capacity {
return Err(Error::new(ErrorKind::ResourceLimit));
}
jobs.push_back(Task::Reusable(work));
pool.state.queued.store(jobs.len(), Ordering::Relaxed);
pool.state.ready.notify_one();
Ok(())
}
pub(crate) fn reserve(config: PoolConfig) -> Result<()> {
let pool = pool(config)?;
let jobs = lock(pool);
let reserved = pool.state.reserved.load(Ordering::Relaxed);
if jobs.len() + reserved >= pool.config.queue_capacity {
return Err(Error::new(ErrorKind::ResourceLimit));
}
pool.state.reserved.store(reserved + 1, Ordering::Relaxed);
Ok(())
}
pub(crate) fn unreserve() {
let pool = POOL
.get()
.and_then(|p| p.as_ref().ok())
.expect("reserved pool");
let _jobs = lock(pool);
let reserved = pool.state.reserved.load(Ordering::Relaxed);
pool.state.reserved.store(reserved - 1, Ordering::Relaxed);
}
pub(crate) fn push_reserved(work: Arc<dyn ReusableWork>) {
let pool = POOL
.get()
.and_then(|p| p.as_ref().ok())
.expect("reserved pool");
let mut jobs = lock(pool);
let reserved = pool.state.reserved.load(Ordering::Relaxed);
pool.state.reserved.store(reserved - 1, Ordering::Relaxed);
debug_assert!(jobs.len() < pool.config.queue_capacity);
jobs.push_back(Task::Reusable(work));
pool.state.queued.store(jobs.len(), Ordering::Relaxed);
drop(jobs);
pool.state.ready.notify_one();
}
pub(super) struct LongSet {
handoff: Mutex<Long>,
ready: Condvar,
live: AtomicUsize,
busy: AtomicUsize,
max: usize,
idle_timeout: Duration,
}
struct Long {
jobs: VecDeque<Job>,
threads: usize,
idle: usize,
started: usize,
}
impl LongSet {
pub(super) fn new(max: usize, idle_timeout: Duration) -> Arc<Self> {
Arc::new(Self {
handoff: Mutex::new(Long {
jobs: VecDeque::new(),
threads: 0,
idle: 0,
started: 0,
}),
ready: Condvar::new(),
live: AtomicUsize::new(0),
busy: AtomicUsize::new(0),
max,
idle_timeout,
})
}
fn lock(&self) -> std::sync::MutexGuard<'_, Long> {
self.handoff
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
pub(super) fn submit(self: &Arc<Self>, job: Job) -> Result<()> {
let mut long = self.lock();
if long.jobs.len() < long.idle {
long.jobs.push_back(job);
drop(long);
self.ready.notify_one();
return Ok(());
}
if long.threads >= self.max {
return Err(Error::new(ErrorKind::ResourceLimit));
}
long.threads += 1;
self.live.store(long.threads, Ordering::Relaxed);
let ordinal = long.started;
long.started += 1;
drop(long);
let set = self.clone();
match thread::Builder::new()
.name(format!("turnloop-long-{ordinal}"))
.spawn(move || set.worker(job))
{
Ok(_detached) => Ok(()),
Err(e) => {
let mut long = self.lock();
long.threads -= 1;
self.live.store(long.threads, Ordering::Relaxed);
Err(e.into())
}
}
}
fn worker(self: Arc<Self>, first: Job) {
struct Retire<'a>(&'a LongSet);
impl Drop for Retire<'_> {
fn drop(&mut self) {
let mut long = self.0.lock();
long.threads -= 1;
self.0.live.store(long.threads, Ordering::Relaxed);
}
}
let _retire = Retire(&self);
let mut next = Some(first);
while let Some(job) = next.take() {
run(job, &self.busy);
let mut long = self.lock();
long.idle += 1;
loop {
if let Some(job) = long.jobs.pop_front() {
long.idle -= 1;
next = Some(job);
break;
}
let (guard, wait) = self
.ready
.wait_timeout(long, self.idle_timeout)
.unwrap_or_else(std::sync::PoisonError::into_inner);
long = guard;
if wait.timed_out() && long.jobs.is_empty() {
long.idle -= 1;
drop(long);
return;
}
}
}
}
pub(super) fn stats(&self) -> (usize, usize) {
(
self.live.load(Ordering::Relaxed),
self.busy.load(Ordering::Relaxed),
)
}
#[cfg(all(test, not(loom)))]
fn idle(&self) -> usize {
self.lock().idle
}
}
#[cfg(all(test, not(loom)))]
mod long_tests {
use super::*;
use std::{
sync::mpsc,
time::{Duration, Instant},
};
struct NoWake;
impl crate::backend::Wake for NoWake {
fn wake(&self) -> Result<()> {
Ok(())
}
fn syscall_count(&self) -> u64 {
0
}
}
fn port() -> Arc<WorkPort> {
WorkPort::new(64, Notifier::new(std::sync::Arc::new(NoWake)))
}
fn job(
port: &Arc<WorkPort>,
key: u64,
cancel: Arc<AtomicBool>,
f: impl FnOnce() -> Result<WorkOutput> + Send + 'static,
) -> Job {
Job {
op: OpId { owner: 1, key },
cancel,
port: port.clone(),
f: Box::new(f),
}
}
fn live() -> Arc<AtomicBool> {
Arc::new(AtomicBool::new(false))
}
#[track_caller]
fn until(what: &str, mut ready: impl FnMut() -> bool) {
let deadline = Instant::now() + Duration::from_secs(10);
while !ready() {
assert!(Instant::now() < deadline, "{what}");
thread::yield_now();
}
}
#[track_caller]
fn failure(result: Result<WorkOutput>) -> ErrorKind {
match result {
Ok(_) => panic!("expected a failed job"),
Err(e) => e.kind,
}
}
#[track_caller]
fn take(port: &Arc<WorkPort>) -> WorkResult {
let deadline = Instant::now() + Duration::from_secs(10);
loop {
if let Some(result) = port.pop() {
return result;
}
assert!(Instant::now() < deadline, "a result arrived");
thread::yield_now();
}
}
#[test]
fn long_workers_are_reused_and_retire_when_idle() {
let set = LongSet::new(4, Duration::from_millis(50));
let port = port();
set.submit(job(&port, 1, live(), || {
Ok(WorkOutput::Blocking(Payload::U64(1)))
}))
.expect("first long job");
assert!(matches!(
take(&port).result,
Ok(WorkOutput::Blocking(Payload::U64(1)))
));
assert_eq!(set.stats().0, 1, "one worker started for one long job");
until("the worker parked", || set.idle() == 1);
set.submit(job(&port, 2, live(), || {
Ok(WorkOutput::Blocking(Payload::U64(2)))
}))
.expect("second long job");
assert!(matches!(
take(&port).result,
Ok(WorkOutput::Blocking(Payload::U64(2)))
));
assert_eq!(
set.stats().0,
1,
"the parked worker served it instead of a new thread"
);
until("the idle worker retired", || set.stats() == (0, 0));
}
#[test]
fn long_submissions_at_the_ceiling_are_refused_rather_than_queued() {
let set = LongSet::new(2, Duration::from_secs(30));
let port = port();
let entered = Arc::new(AtomicUsize::new(0));
let (release, held) = mpsc::channel::<()>();
let held = Arc::new(Mutex::new(held));
for key in 0..2 {
let (entered, held) = (entered.clone(), held.clone());
set.submit(job(&port, key, live(), move || {
entered.fetch_add(1, Ordering::Release);
let _ = held.lock().expect("held").recv();
Ok(WorkOutput::Blocking(Payload::U64(key)))
}))
.expect("within the ceiling");
}
until("both long jobs are running", || {
entered.load(Ordering::Acquire) == 2
});
assert_eq!(set.stats(), (2, 2), "both workers are occupied");
let refused = set
.submit(job(&port, 9, live(), || {
unreachable!("a refused submission never runs")
}))
.expect_err("above the ceiling");
assert_eq!(refused.kind, ErrorKind::ResourceLimit);
assert!(
port.is_empty(),
"a refused submission owes no completion at all"
);
drop(release);
assert!(matches!(take(&port).result, Ok(WorkOutput::Blocking(_))));
assert!(matches!(take(&port).result, Ok(WorkOutput::Blocking(_))));
until("a worker is free again", || set.idle() > 0);
set.submit(job(&port, 10, live(), || {
Ok(WorkOutput::Blocking(Payload::U64(10)))
}))
.expect("the released capacity is usable");
assert!(matches!(
take(&port).result,
Ok(WorkOutput::Blocking(Payload::U64(10)))
));
}
#[test]
fn a_cancelled_or_panicking_long_job_delivers_exactly_one_result() {
let set = LongSet::new(2, Duration::from_millis(50));
let port = port();
let ran = Arc::new(AtomicUsize::new(0));
let counter = ran.clone();
let cancelled = Arc::new(AtomicBool::new(true));
set.submit(job(&port, 1, cancelled, move || {
counter.fetch_add(1, Ordering::Release);
Ok(WorkOutput::Blocking(Payload::U64(1)))
}))
.expect("accepted");
let result = take(&port);
assert_eq!(result.op.key, 1);
assert_eq!(failure(result.result), ErrorKind::Cancelled);
assert_eq!(ran.load(Ordering::Acquire), 0, "the work never started");
set.submit(job(&port, 2, live(), || panic!("a job may panic")))
.expect("accepted");
let result = take(&port);
assert_eq!(result.op.key, 2);
assert_eq!(failure(result.result), ErrorKind::Other);
set.submit(job(&port, 3, live(), || {
Ok(WorkOutput::Blocking(Payload::U64(3)))
}))
.expect("accepted");
let result = take(&port);
assert_eq!(result.op.key, 3);
assert!(matches!(
result.result,
Ok(WorkOutput::Blocking(Payload::U64(3)))
));
assert!(port.is_empty(), "three jobs delivered three results");
}
}
}
pub(crate) fn submit(
config: PoolConfig,
occupancy: Occupancy,
op: OpId,
cancel: Arc<AtomicBool>,
port: Arc<WorkPort>,
f: Box<dyn FnOnce() -> Result<WorkOutput> + Send>,
) -> Result<()> {
#[cfg(not(target_arch = "wasm32"))]
{
native::submit(config, occupancy, op, cancel, port, f)
}
#[cfg(target_arch = "wasm32")]
{
let _ = (config, occupancy, op, cancel, port, f);
Err(Error::new(ErrorKind::Unsupported))
}
}
pub(crate) fn blocking(
f: impl FnOnce() -> BlockingResult + Send + 'static,
) -> Box<dyn FnOnce() -> Result<WorkOutput> + Send> {
Box::new(move || f().map(WorkOutput::Blocking))
}
pub(crate) fn blocking_cancellable(
f: impl FnOnce(&Cancellation) -> BlockingResult + Send + 'static,
cancel: Arc<AtomicBool>,
port: Arc<WorkPort>,
) -> Box<dyn FnOnce() -> Result<WorkOutput> + Send> {
let stop = Cancellation::new(cancel, port);
Box::new(move || f(&stop).map(WorkOutput::Blocking))
}
pub(crate) fn resolve(request: DnsRequest) -> Box<dyn FnOnce() -> Result<WorkOutput> + Send> {
Box::new(move || {
let addresses: Vec<_> = (request.host.as_str(), request.port)
.to_socket_addrs()
.map_err(Error::from)?
.collect();
if addresses.is_empty() {
return Err(Error::new(ErrorKind::NotFound));
}
Ok(WorkOutput::Resolved(addresses))
})
}
#[cfg(all(test, loom))]
mod models {
use super::*;
use crate::{
backend::Wake,
sync::{AtomicUsize, Ordering as ModelOrdering},
};
struct WakeCounter(AtomicUsize);
impl Wake for WakeCounter {
fn wake(&self) -> Result<()> {
self.0.fetch_add(1, ModelOrdering::Relaxed);
Ok(())
}
fn syscall_count(&self) -> u64 {
self.0.load(ModelOrdering::Relaxed) as u64
}
}
#[test]
fn pool_result_publication_races_loop_parking() {
loom::model(|| {
let wake = Arc::new(WakeCounter(AtomicUsize::new(0)));
let notifier = Notifier::new(wake.clone());
let port = WorkPort::new(2, notifier.clone());
let worker = port.clone();
let op = OpId {
owner: 7,
key: 1 << 32,
};
let t = loom::thread::spawn(move || {
worker.complete(WorkResult {
op,
result: Ok(WorkOutput::Blocking(Payload::U64(42))),
})
});
let parked = notifier.park();
t.join().expect("pool worker");
let completion = port.pop().expect("published completion");
assert_eq!(completion.op, op);
assert!(matches!(
completion.result,
Ok(WorkOutput::Blocking(Payload::U64(42)))
));
assert!(port.pop().is_none());
if parked {
assert_eq!(wake.syscall_count(), 1);
} else {
assert!(notifier.begin());
}
});
}
}
#[cfg(any(turnloop_backend = "kqueue", turnloop_backend = "epoll"))]
pub(crate) use native::reusable;
#[cfg(not(target_arch = "wasm32"))]
pub(crate) use native::{push_reserved, reserve, unreserve};