use std::future::Future;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::mpsc;
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::{Duration, Instant};
const JOIN_BOUND: Duration = Duration::from_secs(30);
pub struct VUWorkerPool {
workers: Mutex<Vec<WorkerInner>>,
busy: Mutex<Vec<Arc<AtomicBool>>>,
next_idx: AtomicUsize,
join_bound: Duration,
growth_failed: AtomicBool,
}
struct WorkerInner {
handle: tokio::runtime::Handle,
shutdown: Arc<tokio::sync::Notify>,
thread: Option<thread::JoinHandle<()>>,
exited: Option<mpsc::Receiver<()>>,
}
enum Slot {
Idle(usize, Arc<AtomicBool>),
Grown(usize, Arc<AtomicBool>),
Wrapped(usize),
Inline,
}
struct BusyGuard(Arc<AtomicBool>);
impl Drop for BusyGuard {
fn drop(&mut self) {
self.0.store(false, Ordering::Release);
}
}
impl VUWorkerPool {
pub fn new(count: usize) -> Self {
Self::with_join_bound(count, JOIN_BOUND)
}
fn with_join_bound(count: usize, join_bound: Duration) -> Self {
assert!(count > 0, "VUWorkerPool requires at least 1 worker");
let mut workers = Vec::with_capacity(count);
let mut busy = Vec::with_capacity(count);
for i in 0..count {
if let Some(w) = Self::make_worker(i) {
workers.push(w);
busy.push(Arc::new(AtomicBool::new(false)));
}
}
Self {
workers: Mutex::new(workers),
busy: Mutex::new(busy),
next_idx: AtomicUsize::new(0),
join_bound,
growth_failed: AtomicBool::new(false),
}
}
fn make_worker(i: usize) -> Option<WorkerInner> {
#[cfg(test)]
if FAIL_NEXT_WORKER_BUILD.with(|f| f.replace(false)) {
return None;
}
let runtime = match tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
{
Ok(r) => r,
Err(e) => {
tracing::warn!(
"VUWorkerPool: failed to create worker runtime {} ({}); degrading",
i,
e
);
return None;
}
};
let handle = runtime.handle().clone();
let shutdown = Arc::new(tokio::sync::Notify::new());
let sig = shutdown.clone();
let (exited_tx, exited_rx) = mpsc::channel::<()>();
let thread = match thread::Builder::new()
.name(format!("tropel-worker-{}", i))
.spawn(move || {
runtime.block_on(async {
sig.notified().await;
});
let _ = exited_tx.send(());
}) {
Ok(t) => t,
Err(e) => {
tracing::warn!(
"VUWorkerPool: failed to spawn worker thread {} ({}); degrading",
i,
e
);
return None;
}
};
Some(WorkerInner {
handle,
shutdown,
thread: Some(thread),
exited: Some(exited_rx),
})
}
fn find_idle_slot(&self) -> Option<(usize, Arc<AtomicBool>)> {
let busy = self.busy.lock().unwrap();
for (i, flag) in busy.iter().enumerate() {
if flag
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
.is_ok()
{
return Some((i, flag.clone()));
}
}
None
}
fn acquire_slot(&self, vu_id: u32) -> Slot {
if let Some((idx, flag)) = self.find_idle_slot() {
self.growth_failed.store(false, Ordering::Release);
return Slot::Idle(idx, flag);
}
loop {
let current = self.workers.lock().unwrap().len();
if current >= Self::MAX_WORKERS {
return Slot::Wrapped((vu_id as usize) % current);
}
if self.growth_failed.load(Ordering::Acquire) {
return if current > 0 {
Slot::Wrapped((vu_id as usize) % current)
} else {
Slot::Inline
};
}
let worker = match Self::make_worker(current) {
Some(w) => w,
None => {
self.growth_failed.store(true, Ordering::Release);
if current > 0 {
return Slot::Wrapped((vu_id as usize) % current);
}
return Slot::Inline;
}
};
let flag = Arc::new(AtomicBool::new(true)); let mut workers = self.workers.lock().unwrap();
if workers.len() == current {
workers.push(worker);
self.busy.lock().unwrap().push(flag.clone());
return Slot::Grown(current, flag);
}
drop(workers);
worker.shutdown.notify_one();
drop(worker.thread);
drop(worker.exited);
}
}
pub fn worker_count(&self) -> usize {
self.workers.lock().unwrap().len()
}
pub fn spawn_on<F>(&self, idx: usize, future: F) -> tokio::task::JoinHandle<F::Output>
where
F: Future + Send + 'static,
F::Output: Send + 'static,
{
let handle = self.workers.lock().unwrap()[idx].handle.clone();
handle.spawn(future)
}
pub const MAX_WORKERS: usize = 10_000;
pub fn pids_limit() -> Option<u64> {
for path in [
"/sys/fs/cgroup/pids.max",
"/sys/fs/cgroup/pids/pids.max",
"/sys/fs/cgroup/cpu/pids.max",
] {
if let Ok(s) = std::fs::read_to_string(path) {
let t = s.trim();
if t == "max" || t.is_empty() {
continue;
}
if let Ok(v) = t.parse::<u64>() {
if v > 0 {
return Some(v);
}
}
}
}
None
}
pub fn effective_concurrency(requested: u64) -> u64 {
let pids = Self::pids_limit().unwrap_or(u64::MAX);
requested.min(Self::MAX_WORKERS as u64).min(pids)
}
pub fn spawn_vu<F>(&self, vu_id: u32, future: F) -> tokio::task::JoinHandle<F::Output>
where
F: Future + Send + 'static,
F::Output: Send + 'static,
{
match self.acquire_slot(vu_id) {
Slot::Idle(idx, flag) | Slot::Grown(idx, flag) => self.spawn_on(idx, async move {
let _release = BusyGuard(flag);
future.await
}),
Slot::Wrapped(idx) => {
use std::sync::Once;
static WRAP_WARNED: Once = Once::new();
let current = self.workers.lock().unwrap().len();
WRAP_WARNED.call_once(|| {
tracing::warn!(
"VU pool wrapping: {} VUs requested but only {} workers \
available (MAX_WORKERS={}). Co-located VUs share a \
single-threaded runtime and block each other. The \
reported concurrency exceeds the actual throughput.",
vu_id + 1,
current,
Self::MAX_WORKERS,
);
});
self.spawn_on(idx, future)
}
Slot::Inline => tokio::spawn(future),
}
}
pub fn spawn<F>(&self, future: F) -> (usize, tokio::task::JoinHandle<F::Output>)
where
F: Future + Send + 'static,
F::Output: Send + 'static,
{
let len = self.worker_count();
if len == 0 {
return (0, tokio::spawn(future));
}
let idx = self.next_idx.fetch_add(1, Ordering::Relaxed) % len;
let handle = self.spawn_on(idx, future);
(idx, handle)
}
}
impl Drop for VUWorkerPool {
fn drop(&mut self) {
let workers = self
.workers
.get_mut()
.unwrap_or_else(std::sync::PoisonError::into_inner);
for worker in workers.iter() {
worker.shutdown.notify_one();
}
let deadline = Instant::now() + self.join_bound;
for worker in workers.iter_mut() {
let remaining = deadline.saturating_duration_since(Instant::now());
let exited = match &worker.exited {
Some(rx) => matches!(
rx.recv_timeout(remaining),
Ok(()) | Err(mpsc::RecvTimeoutError::Disconnected)
),
None => false,
};
if exited {
if let Some(thread) = worker.thread.take() {
let _ = thread.join();
}
} else if let Some(thread) = worker.thread.take() {
tracing::warn!(
"VU worker {} did not exit within the {}s join bound — detaching (its VU is wedged in a blocking call)",
thread.thread().name().unwrap_or("?"),
self.join_bound.as_secs()
);
}
}
}
}
#[cfg(test)]
thread_local! {
static FAIL_NEXT_WORKER_BUILD: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[tokio::test]
async fn spawn_vu_pins_each_vu_to_its_own_thread() {
let pool = VUWorkerPool::new(1);
let barrier = Arc::new(tokio::sync::Barrier::new(2));
let (t0, t1) = tokio::join!(
async {
let b = barrier.clone();
let h = pool.spawn_vu(0, async move {
b.wait().await;
std::thread::current().name().map(|s| s.to_string())
});
h.await.unwrap()
},
async {
let b = barrier.clone();
let h = pool.spawn_vu(1, async move {
b.wait().await;
std::thread::current().name().map(|s| s.to_string())
});
h.await.unwrap()
},
);
assert_ne!(t0, t1, "VUs must run on distinct worker threads");
assert_eq!(t0.as_deref(), Some("tropel-worker-0"));
assert_eq!(t1.as_deref(), Some("tropel-worker-1"));
}
#[tokio::test]
async fn sleep_in_one_vu_does_not_block_another() {
let pool = VUWorkerPool::new(1);
let slow = pool.spawn_vu(0, async {
std::thread::sleep(Duration::from_millis(200));
"slow"
});
let fast = tokio::time::timeout(
Duration::from_millis(100),
pool.spawn_vu(1, async { "fast" }),
)
.await
.expect("fast VU was blocked behind another VU's sleep")
.unwrap();
assert_eq!(fast, "fast");
let _ = slow.await.unwrap();
}
#[tokio::test]
async fn worker_pool_grows_only_for_concurrent_vus() {
let pool = VUWorkerPool::new(2);
assert_eq!(pool.worker_count(), 2);
for vu_id in 0..2000u32 {
let h = pool.spawn_vu(vu_id, async {});
assert!(h.await.is_ok());
assert_eq!(
pool.worker_count(),
2,
"sequential VU {vu_id} grew the pool"
);
}
let (a, b, c) = tokio::join!(
pool.spawn_vu(0, async {
std::thread::sleep(std::time::Duration::from_millis(50));
}),
pool.spawn_vu(1, async {
std::thread::sleep(std::time::Duration::from_millis(50));
}),
pool.spawn_vu(2, async {
std::thread::sleep(std::time::Duration::from_millis(50));
}),
);
assert!(a.is_ok() && b.is_ok() && c.is_ok());
assert_eq!(
pool.worker_count(),
3,
"3 concurrent VUs must grow the pool to 3"
);
let h = pool.spawn_vu(3, async {});
assert!(h.await.is_ok());
assert_eq!(pool.worker_count(), 3);
let (idx, h) = pool.spawn(async {});
assert!(h.await.is_ok());
assert!(idx < pool.worker_count());
}
#[test]
fn drop_detaches_wedged_worker_within_join_bound() {
let pool = VUWorkerPool::with_join_bound(1, Duration::from_millis(150));
let _h = pool.spawn_vu(0, async {
std::thread::sleep(Duration::from_secs(2));
});
let start = Instant::now();
drop(pool);
let elapsed = start.elapsed();
assert!(
elapsed < Duration::from_millis(800),
"drop blocked for {elapsed:?} on a wedged worker instead of detaching"
);
}
#[test]
fn drop_joins_healthy_workers_promptly() {
let pool = VUWorkerPool::with_join_bound(2, Duration::from_secs(5));
let _h = pool.spawn_vu(0, async {});
let _h2 = pool.spawn_vu(1, async {});
let start = Instant::now();
drop(pool);
assert!(
start.elapsed() < Duration::from_millis(500),
"healthy teardown took {:?}",
start.elapsed()
);
}
#[tokio::test]
async fn spawn_vu_degrades_to_wrapped_when_worker_build_fails() {
let pool = VUWorkerPool::new(1);
let (tx, rx) = tokio::sync::oneshot::channel();
let hold = pool.spawn_vu(0, async move {
let _ = rx.await;
"held"
});
FAIL_NEXT_WORKER_BUILD.with(|f| f.set(true));
let wrapped = pool.spawn_vu(1, async { "wrapped" });
FAIL_NEXT_WORKER_BUILD.with(|f| f.set(false));
assert_eq!(wrapped.await.expect("wrapped VU panicked"), "wrapped");
let _ = tx.send(());
assert_eq!(hold.await.expect("held VU panicked"), "held");
}
#[tokio::test]
async fn spawn_vu_degrades_to_inline_when_pool_is_empty() {
FAIL_NEXT_WORKER_BUILD.with(|f| f.set(true));
let pool = VUWorkerPool::new(1);
assert_eq!(
pool.worker_count(),
0,
"forced build failure must yield an empty pool"
);
FAIL_NEXT_WORKER_BUILD.with(|f| f.set(true));
let h = pool.spawn_vu(7, async { 42u32 });
FAIL_NEXT_WORKER_BUILD.with(|f| f.set(false));
assert_eq!(h.await.expect("inline VU panicked"), 42);
assert_eq!(pool.worker_count(), 0, "inline VU must not grow the pool");
}
}