use std::{
sync::{Arc, Condvar, Mutex, PoisonError},
time::Instant,
};
use monty_proto::pb;
use crate::{
PoolConfig, PoolError,
checkout::{Checkout, ReplConfig},
watchdog::Watchdog,
worker::{Worker, lock_ignore_poison},
};
pub struct Pool {
pub(crate) inner: Arc<PoolInner>,
}
pub(crate) struct PoolInner {
pub(crate) config: PoolConfig,
state: Mutex<PoolState>,
available: Condvar,
pub(crate) watchdog: Watchdog,
}
struct PoolState {
idle: Vec<Worker>,
total: usize,
}
impl Pool {
pub fn new(config: PoolConfig) -> Result<Self, PoolError> {
if config.min_processes > config.max_processes || config.max_processes == 0 {
return Err(PoolError::Spawn(format!(
"invalid pool size: min_processes={} max_processes={}",
config.min_processes, config.max_processes
)));
}
let watchdog =
Watchdog::new().map_err(|err| PoolError::Spawn(format!("failed to spawn the watchdog thread: {err}")))?;
let mut idle = Vec::with_capacity(config.min_processes);
if !config.transport.is_websocket() {
for _ in 0..config.min_processes {
idle.push(Worker::new(&config)?);
}
}
let total = idle.len();
Ok(Self {
inner: Arc::new(PoolInner {
config,
state: Mutex::new(PoolState { idle, total }),
available: Condvar::new(),
watchdog,
}),
})
}
pub fn checkout(&self, repl: &ReplConfig) -> Result<Checkout, PoolError> {
let worker = self.inner.acquire_worker()?;
Checkout::create(worker, Arc::clone(&self.inner), repl)
}
#[must_use]
pub fn idle_workers(&self) -> usize {
lock_ignore_poison(&self.inner.state).idle.len()
}
#[must_use]
pub fn idle_worker_pids(&self) -> Vec<u32> {
lock_ignore_poison(&self.inner.state)
.idle
.iter()
.filter_map(Worker::pid)
.collect()
}
}
impl PoolInner {
fn acquire_worker(&self) -> Result<Worker, PoolError> {
let websocket = self.config.transport.is_websocket();
let deadline = self.config.checkout_timeout.map(|t| Instant::now() + t);
let mut state = lock_ignore_poison(&self.state);
loop {
if !websocket {
while let Some(worker) = state.idle.pop() {
if worker.is_dead() {
state.total -= 1;
drop(worker); } else {
return Ok(worker);
}
}
}
if state.total < self.config.max_processes {
state.total += 1;
drop(state);
let acquired = Worker::new(&self.config);
return acquired.inspect_err(|_| self.release_capacity());
}
state = match deadline {
Some(deadline) => {
let now = Instant::now();
if now >= deadline {
return Err(PoolError::Exhausted);
}
let (guard, _) = self
.available
.wait_timeout(state, deadline - now)
.unwrap_or_else(PoisonError::into_inner);
guard
}
None => self.available.wait(state).unwrap_or_else(PoisonError::into_inner),
};
}
}
pub(crate) fn release_worker(&self, worker: Worker) {
let websocket = self.config.transport.is_websocket();
let recycle = websocket
|| self
.config
.max_checkouts_per_worker
.is_some_and(|max| worker.checkouts_served >= max);
if recycle {
drop(worker); self.release_capacity();
} else {
lock_ignore_poison(&self.state).idle.push(worker);
self.available.notify_one();
}
}
pub(crate) fn release_capacity(&self) {
lock_ignore_poison(&self.state).total -= 1;
self.available.notify_one();
}
fn shutdown_idle(&self) {
let mut state = lock_ignore_poison(&self.state);
for worker in &mut state.idle {
let _ = worker.send(&pb::ParentRequest {
kind: Some(pb::parent_request::Kind::Shutdown(pb::Shutdown {})),
});
}
state.idle.clear();
}
}
impl Drop for PoolInner {
fn drop(&mut self) {
self.shutdown_idle();
}
}