use std::sync::Arc;
use std::thread::available_parallelism;
use tokio::sync::Semaphore;
const SCHED_RESERVE: usize = 2;
const IO_PER_CORE: usize = 64;
const IO_MIN: usize = 64;
const POOL_PER_CORE: usize = 2;
const POOL_MIN: usize = 8;
const POOL_MAX: usize = 64;
const WARM_MIN_IDLE: usize = 4;
fn cores() -> usize {
available_parallelism()
.map(std::num::NonZeroUsize::get)
.unwrap_or(SCHED_RESERVE)
}
#[must_use]
pub fn host_blocking_concurrency_default() -> usize {
cores().saturating_sub(SCHED_RESERVE).max(SCHED_RESERVE)
}
#[must_use]
pub fn host_io_concurrency_default() -> usize {
let by_cores = cores().saturating_mul(IO_PER_CORE).max(IO_MIN);
match fd_soft_limit() {
Some(soft) => by_cores.min((soft / 2).max(1)),
None => by_cores,
}
}
#[must_use]
pub fn host_instance_pool_size_default() -> usize {
cores()
.saturating_mul(POOL_PER_CORE)
.clamp(POOL_MIN, POOL_MAX)
}
#[cfg(unix)]
fn fd_soft_limit() -> Option<usize> {
use nix::sys::resource::{Resource, getrlimit};
let (soft, _hard) = getrlimit(Resource::RLIMIT_NOFILE).ok()?;
let soft = usize::try_from(soft).ok()?;
(soft > 0).then_some(soft)
}
#[cfg(not(unix))]
fn fd_soft_limit() -> Option<usize> {
None
}
#[derive(Debug, Clone, Copy)]
pub struct CapsuleRuntimeLimits {
pub blocking_concurrency: usize,
pub io_concurrency: usize,
pub instance_pool_size: usize,
}
impl CapsuleRuntimeLimits {
#[must_use]
pub fn resolve(
blocking_concurrency: Option<usize>,
io_concurrency: Option<usize>,
instance_pool_size: Option<usize>,
) -> Self {
Self {
blocking_concurrency: blocking_concurrency
.unwrap_or_else(host_blocking_concurrency_default)
.max(1),
io_concurrency: io_concurrency
.unwrap_or_else(host_io_concurrency_default)
.max(1),
instance_pool_size: instance_pool_size
.unwrap_or_else(host_instance_pool_size_default)
.max(1),
}
}
#[must_use]
pub fn blocking_semaphore(self) -> Arc<Semaphore> {
Arc::new(Semaphore::new(self.blocking_concurrency))
}
#[must_use]
pub fn io_semaphore(self) -> Arc<Semaphore> {
Arc::new(Semaphore::new(self.io_concurrency))
}
#[must_use]
pub fn instance_pool_min_idle(self) -> usize {
WARM_MIN_IDLE.min(self.instance_pool_size).max(1)
}
}
impl Default for CapsuleRuntimeLimits {
fn default() -> Self {
Self::resolve(None, None, None)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn blocking_default_reserves_for_scheduler_and_floors() {
let b = host_blocking_concurrency_default();
assert!(b >= SCHED_RESERVE, "blocking floor is SCHED_RESERVE");
assert!(b <= cores().max(SCHED_RESERVE));
}
#[test]
fn io_default_is_large_and_floored() {
let io = host_io_concurrency_default();
match fd_soft_limit() {
Some(soft) if soft / 2 < IO_MIN => assert_eq!(io, (soft / 2).max(1)),
_ => assert!(io >= IO_MIN),
}
let fd_not_scarce = fd_soft_limit().is_none_or(|soft| soft >= cores().saturating_mul(2));
if fd_not_scarce {
assert!(
io >= host_blocking_concurrency_default(),
"with ample fds the io ceiling must not be tighter than blocking"
);
}
}
#[test]
fn resolve_prefers_overrides_and_clamps_zero() {
let r = CapsuleRuntimeLimits::resolve(Some(7), Some(900), Some(40));
assert_eq!(r.blocking_concurrency, 7);
assert_eq!(r.io_concurrency, 900);
assert_eq!(r.instance_pool_size, 40);
let z = CapsuleRuntimeLimits::resolve(Some(0), Some(0), Some(0));
assert_eq!(z.blocking_concurrency, 1);
assert_eq!(z.io_concurrency, 1);
assert_eq!(z.instance_pool_size, 1);
}
#[test]
fn resolve_none_uses_host_defaults() {
let r = CapsuleRuntimeLimits::resolve(None, None, None);
assert_eq!(r.blocking_concurrency, host_blocking_concurrency_default());
assert_eq!(r.io_concurrency, host_io_concurrency_default());
assert_eq!(r.instance_pool_size, host_instance_pool_size_default());
}
#[test]
fn semaphores_match_resolved_counts() {
let r = CapsuleRuntimeLimits::resolve(Some(3), Some(11), None);
assert_eq!(r.blocking_semaphore().available_permits(), 3);
assert_eq!(r.io_semaphore().available_permits(), 11);
}
#[test]
fn pool_default_is_bounded_and_beats_the_old_constant_on_a_typical_box() {
let max = host_instance_pool_size_default();
assert!((POOL_MIN..=POOL_MAX).contains(&max));
assert!(max >= POOL_MIN);
}
#[test]
fn min_idle_is_clamped_to_the_max() {
let big = CapsuleRuntimeLimits::resolve(None, None, Some(32));
assert_eq!(big.instance_pool_min_idle(), WARM_MIN_IDLE);
let one = CapsuleRuntimeLimits::resolve(None, None, Some(1));
assert_eq!(one.instance_pool_min_idle(), 1);
}
}