use std::{
mem,
pin::pin,
sync::{Arc, Mutex, MutexGuard, PoisonError},
time::Duration,
};
use futures_util::future::join_all;
use monty_proto::pb;
use tokio::{
sync::Notify,
time::{Instant, timeout_at},
};
use crate::{
PoolConfig, PoolError,
checkout::{Checkout, CheckoutOptions, Redial, ReplConfig, request},
worker::Worker,
};
pub struct Pool {
pub(crate) inner: Arc<PoolInner>,
}
pub(crate) struct PoolInner {
pub(crate) config: PoolConfig,
state: Mutex<PoolState>,
available: Notify,
}
struct PoolState {
idle: Vec<Worker>,
total: usize,
next_id: u64,
}
impl Pool {
pub async 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 mut idle = Vec::with_capacity(config.min_processes);
if !config.transport.is_websocket() {
for _ in 0..config.min_processes {
let mut worker = Worker::new(&config, &[]).await?;
worker.id = idle.len() as u64 + 1;
idle.push(worker);
}
}
let total = idle.len();
let pool = Self {
inner: Arc::new(PoolInner {
config,
state: Mutex::new(PoolState {
idle,
total,
next_id: total as u64 + 1,
}),
available: Notify::new(),
}),
};
let workers = worker_count(total);
pool.inner.record_worker_delta(workers, workers);
Ok(pool)
}
pub async fn checkout(&self, repl: &ReplConfig) -> Result<Checkout, PoolError> {
self.checkout_with(repl, CheckoutOptions::default()).await
}
pub async fn checkout_with(&self, repl: &ReplConfig, mut options: CheckoutOptions) -> Result<Checkout, PoolError> {
#[cfg(feature = "telemetry")]
if let Some(telemetry) = &options.telemetry {
options.connect_headers.splice(0..0, telemetry.propagation_headers());
}
let worker = self.inner.acquire_worker(&options.connect_headers).await?;
let config = &self.inner.config;
let redial = (config.auto_resume && config.transport.is_websocket()).then(|| Redial {
repl: repl.clone(),
connect_headers: options.connect_headers.clone(),
#[cfg(feature = "telemetry")]
telemetry: options.telemetry.clone(),
});
#[cfg(feature = "telemetry")]
let worker = worker.with_adapter_context(options.telemetry);
Checkout::create(worker, Arc::clone(&self.inner), repl, redial).await
}
pub async fn close(&self) {
let mut idle: Vec<_> = {
let mut state = lock_ignore_poison(&self.inner.state);
mem::take(&mut state.idle)
}
.into_iter()
.map(|worker| (worker, CapacityGuard::new(&self.inner)))
.collect();
self.inner.record_worker_delta(0, -worker_count(idle.len()));
for _ in &idle {
self.inner.count_termination("closed");
}
for (worker, _) in &mut idle {
let _ = worker
.send(&request(pb::parent_request::Kind::Shutdown(pb::Shutdown {})))
.await;
}
join_all(idle.into_iter().map(|(mut worker, capacity)| async move {
let _capacity = capacity;
worker.reap_or_kill(SHUTDOWN_EXIT_GRACE).await;
}))
.await;
}
#[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()
}
}
const SHUTDOWN_EXIT_GRACE: Duration = Duration::from_millis(500);
impl PoolInner {
pub(crate) async fn acquire_worker(&self, connect_headers: &[(String, String)]) -> Result<Worker, PoolError> {
#[cfg(feature = "telemetry")]
let started = Instant::now();
let mut outcome = "idle";
let worker = self.acquire_worker_inner(connect_headers, &mut outcome).await;
#[cfg(feature = "telemetry")]
if let Some(metrics) = &self.config.metrics {
let outcome = match &worker {
Ok(_) => outcome,
Err(PoolError::Exhausted) => "exhausted",
Err(_) => "error",
};
metrics.checkout_wait(started.elapsed(), outcome);
}
worker
}
async fn acquire_worker_inner(
&self,
connect_headers: &[(String, String)],
outcome: &mut &'static str,
) -> Result<Worker, PoolError> {
let websocket = self.config.transport.is_websocket();
let mut waited = false;
let deadline = self.config.checkout_timeout.map(|t| Instant::now() + t);
loop {
let mut notified = pin!(self.available.notified());
notified.as_mut().enable();
let spawn = {
let mut state = lock_ignore_poison(&self.state);
let mut reused = None;
let mut died_idle = 0;
let mut removed_idle = 0;
if !websocket {
while let Some(mut worker) = state.idle.pop() {
removed_idle += 1;
if worker.is_dead() {
state.total -= 1;
died_idle += 1;
drop(worker); } else {
reused = Some(worker);
break;
}
}
}
let below_cap = reused.is_none() && state.total < self.config.max_processes;
let id = if below_cap {
state.total += 1;
let id = state.next_id;
state.next_id = id.checked_add(1).expect("worker identity exhausted");
Some(id)
} else {
None
};
drop(state); for _ in 0..died_idle {
self.count_termination("died_idle");
}
let spawned = i64::from(below_cap);
self.record_worker_delta(spawned - worker_count(died_idle), -worker_count(removed_idle));
if let Some(worker) = reused {
*outcome = if waited { "waited" } else { "idle" };
return Ok(worker);
}
id
};
if let Some(id) = spawn {
*outcome = if waited { "waited" } else { "spawned" };
let capacity = CapacityGuard::new(self);
let mut worker = Worker::new(&self.config, connect_headers).await?;
worker.id = id;
capacity.disarm();
return Ok(worker);
}
waited = true;
match deadline {
Some(deadline) => {
if timeout_at(deadline, notified).await.is_err() {
return Err(PoolError::Exhausted);
}
}
None => notified.await,
}
}
}
pub(crate) fn count_termination(&self, reason: &'static str) {
#[cfg(feature = "telemetry")]
if let Some(metrics) = &self.config.metrics {
metrics.worker_terminated(reason);
}
#[cfg(not(feature = "telemetry"))]
let _ = reason;
}
pub(crate) fn record_worker_delta(&self, live: i64, idle: i64) {
#[cfg(feature = "telemetry")]
if let Some(metrics) = &self.config.metrics {
if live != 0 {
metrics.live_workers(live);
}
if idle != 0 {
metrics.idle_workers(idle);
}
}
#[cfg(not(feature = "telemetry"))]
let _ = (live, idle);
}
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.count_termination(if websocket { "single_use" } else { "recycled" });
self.release_capacity();
} else {
lock_ignore_poison(&self.state).idle.push(worker);
self.available.notify_one();
self.record_worker_delta(0, 1);
}
}
pub(crate) fn release_capacity(&self) {
lock_ignore_poison(&self.state).total -= 1;
self.available.notify_one();
self.record_worker_delta(-1, 0);
}
}
#[cfg(feature = "telemetry")]
impl Drop for PoolInner {
fn drop(&mut self) {
let (live, idle) = {
let state = self.state.get_mut().unwrap_or_else(PoisonError::into_inner);
(state.total, state.idle.len())
};
self.record_worker_delta(-worker_count(live), -worker_count(idle));
for _ in 0..idle {
self.count_termination("closed");
}
}
}
pub(crate) struct CapacityGuard<'a> {
pool: Option<&'a PoolInner>,
reason: Option<&'static str>,
}
impl<'a> CapacityGuard<'a> {
pub(crate) fn new(pool: &'a PoolInner) -> Self {
Self {
pool: Some(pool),
reason: None,
}
}
pub(crate) fn terminating(pool: &'a PoolInner, reason: &'static str) -> Self {
Self {
pool: Some(pool),
reason: Some(reason),
}
}
pub(crate) const fn set_reason(&mut self, reason: &'static str) {
self.reason = Some(reason);
}
pub(crate) fn disarm(mut self) {
self.pool = None;
}
}
impl Drop for CapacityGuard<'_> {
fn drop(&mut self) {
if let Some(pool) = self.pool {
if let Some(reason) = self.reason {
pool.count_termination(reason);
}
pool.release_capacity();
}
}
}
fn worker_count(count: usize) -> i64 {
i64::try_from(count).unwrap_or(i64::MAX)
}
pub(crate) fn lock_ignore_poison<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
mutex.lock().unwrap_or_else(PoisonError::into_inner)
}