use crate::clock::{Clock, SystemClock};
use crate::error::LimiteronError;
use crate::limiters::traits::{Limiter, RateLimitSnapshot, validate_cost};
use async_trait::async_trait;
use parking_lot::Mutex;
use std::sync::Arc;
use std::sync::atomic::{AtomicI64, AtomicU64, Ordering};
#[derive(Debug, Clone)]
pub struct AdmissionControlConfig {
pub max_concurrent: u64,
pub max_per_second: u64,
}
impl AdmissionControlConfig {
pub fn validate(&self) -> Result<(), LimiteronError> {
if self.max_concurrent == 0 {
return Err(LimiteronError::ConfigError(
"admission-control max_concurrent must be non-zero".to_string(),
));
}
if self.max_per_second == 0 {
return Err(LimiteronError::ConfigError(
"admission-control max_per_second must be non-zero".to_string(),
));
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct AdmissionStats {
pub admitted: u64,
pub rejected_concurrency: u64,
pub rejected_rate: u64,
pub in_flight: i64,
}
#[derive(Debug, Default)]
struct StatsCounters {
admitted: AtomicU64,
rejected_concurrency: AtomicU64,
rejected_rate: AtomicU64,
}
#[derive(Debug)]
struct RateWindowState {
window_start: u64,
count: u64,
}
pub struct AdmissionController {
max_concurrent: u64,
max_per_second: u64,
clock: Arc<dyn Clock>,
in_flight: AtomicI64,
rate_window: Mutex<RateWindowState>,
stats: StatsCounters,
}
#[must_use = "dropping the permit releases the in-flight occupancy immediately; hold it until the request completes"]
pub struct AdmissionPermit {
controller: Arc<AdmissionController>,
units: u64,
}
impl Drop for AdmissionPermit {
fn drop(&mut self) {
self.controller
.in_flight
.fetch_sub(self.units as i64, Ordering::AcqRel);
}
}
impl AdmissionController {
pub fn new(config: AdmissionControlConfig) -> Self {
Self::with_clock(config, Arc::new(SystemClock))
}
pub fn with_clock(config: AdmissionControlConfig, clock: Arc<dyn Clock>) -> Self {
config.validate().expect("valid admission control config");
let window_start = clock.unix_timestamp();
Self {
max_concurrent: config.max_concurrent,
max_per_second: config.max_per_second,
clock,
in_flight: AtomicI64::new(0),
rate_window: Mutex::new(RateWindowState {
window_start,
count: 0,
}),
stats: StatsCounters::default(),
}
}
pub async fn try_acquire(
self: &Arc<Self>,
cost: u64,
) -> Result<Option<AdmissionPermit>, LimiteronError> {
let _cost = validate_cost(cost)?;
let mut in_flight = self.in_flight.load(Ordering::Acquire);
let occupied = loop {
if in_flight + cost as i64 > self.max_concurrent as i64 {
self.stats
.rejected_concurrency
.fetch_add(1, Ordering::Relaxed);
return Ok(None);
}
match self.in_flight.compare_exchange(
in_flight,
in_flight + cost as i64,
Ordering::AcqRel,
Ordering::Acquire,
) {
Ok(_) => break true,
Err(actual) => in_flight = actual,
}
};
debug_assert!(occupied);
let granted = {
let mut state = self.rate_window.lock();
self.advance_rate_window(&mut state);
if state.count + cost <= self.max_per_second {
state.count += cost;
true
} else {
false
}
};
if !granted {
self.in_flight.fetch_sub(cost as i64, Ordering::AcqRel);
self.stats.rejected_rate.fetch_add(1, Ordering::Relaxed);
return Ok(None);
}
self.stats.admitted.fetch_add(1, Ordering::Relaxed);
Ok(Some(AdmissionPermit {
controller: Arc::clone(self),
units: cost,
}))
}
pub async fn stats(&self) -> AdmissionStats {
AdmissionStats {
admitted: self.stats.admitted.load(Ordering::Relaxed),
rejected_concurrency: self.stats.rejected_concurrency.load(Ordering::Relaxed),
rejected_rate: self.stats.rejected_rate.load(Ordering::Relaxed),
in_flight: self.in_flight.load(Ordering::Relaxed),
}
}
fn advance_rate_window(&self, state: &mut RateWindowState) {
let now = self.clock.unix_timestamp();
if now >= state.window_start.saturating_add(1) {
state.window_start = now;
state.count = 0;
}
}
}
#[async_trait]
impl Limiter for AdmissionController {
async fn allow(&self, cost: u64) -> Result<bool, LimiteronError> {
let _cost = validate_cost(cost)?;
let mut in_flight = self.in_flight.load(Ordering::Acquire);
let occupied = loop {
if in_flight + cost as i64 > self.max_concurrent as i64 {
break false;
}
match self.in_flight.compare_exchange(
in_flight,
in_flight + cost as i64,
Ordering::AcqRel,
Ordering::Acquire,
) {
Ok(_) => break true,
Err(actual) => in_flight = actual,
}
};
if !occupied {
self.stats
.rejected_concurrency
.fetch_add(1, Ordering::Relaxed);
return Ok(false);
}
let granted = {
let mut state = self.rate_window.lock();
self.advance_rate_window(&mut state);
if state.count + cost <= self.max_per_second {
state.count += cost;
true
} else {
false
}
};
self.in_flight.fetch_sub(cost as i64, Ordering::AcqRel);
if !granted {
self.stats.rejected_rate.fetch_add(1, Ordering::Relaxed);
return Ok(false);
}
self.stats.admitted.fetch_add(1, Ordering::Relaxed);
Ok(true)
}
async fn peek(&self, cost: u64) -> Result<RateLimitSnapshot, LimiteronError> {
let _cost = validate_cost(cost)?;
let mut state = self.rate_window.lock();
self.advance_rate_window(&mut state);
Ok(RateLimitSnapshot {
limit: self.max_per_second,
remaining: self.max_per_second.saturating_sub(state.count),
reset_secs: state
.window_start
.saturating_add(1)
.saturating_sub(self.clock.unix_timestamp()),
})
}
async fn remaining(&self) -> Result<RateLimitSnapshot, LimiteronError> {
self.peek(1).await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::clock::MockClock;
fn controller(max_concurrent: u64, max_per_second: u64) -> Arc<AdmissionController> {
Arc::new(AdmissionController::new(AdmissionControlConfig {
max_concurrent,
max_per_second,
}))
}
#[tokio::test]
async fn test_acquire_within_limits_grants_permit() {
let controller = controller(10, 100);
let permit = controller.try_acquire(2).await.unwrap();
assert!(permit.is_some(), "within limits should be admitted");
let stats = controller.stats().await;
assert_eq!(stats.admitted, 1);
assert_eq!(stats.in_flight, 2);
}
#[tokio::test]
async fn test_permit_drop_releases_concurrency() {
let controller = controller(4, 100);
drop(controller.try_acquire(3).await.unwrap().unwrap());
let stats = controller.stats().await;
assert_eq!(stats.in_flight, 0, "dropping permit should release units");
assert!(controller.try_acquire(4).await.unwrap().is_some());
}
#[tokio::test]
async fn test_concurrency_gate_rejects_and_counts() {
let controller = controller(4, 100);
let _permit = controller.try_acquire(3).await.unwrap().unwrap();
assert!(controller.try_acquire(2).await.unwrap().is_none());
let stats = controller.stats().await;
assert_eq!(stats.rejected_concurrency, 1);
assert_eq!(stats.rejected_rate, 0);
assert_eq!(stats.in_flight, 3, "rejected request must not occupy units");
}
#[tokio::test]
async fn test_rate_gate_rejects_and_counts() {
let controller = controller(100, 3);
let mut permits = Vec::new();
for _ in 0..3 {
permits.push(
controller
.try_acquire(1)
.await
.unwrap()
.expect("within rate limit should be admitted"),
);
}
assert!(controller.try_acquire(1).await.unwrap().is_none());
let stats = controller.stats().await;
assert_eq!(stats.admitted, 3);
assert_eq!(stats.rejected_rate, 1);
assert_eq!(
stats.in_flight, 3,
"rate-rejected request must release concurrency reservation; held permits remain"
);
drop(permits);
}
#[tokio::test]
async fn test_rate_window_advances() {
let clock = Arc::new(MockClock::new());
let controller = Arc::new(AdmissionController::with_clock(
AdmissionControlConfig {
max_concurrent: 100,
max_per_second: 2,
},
clock.clone(),
));
let _first = controller.try_acquire(2).await.unwrap().unwrap();
assert!(controller.try_acquire(1).await.unwrap().is_none());
clock.advance(std::time::Duration::from_secs(2));
let _second = controller.try_acquire(2).await.unwrap().unwrap();
}
#[tokio::test]
async fn test_limiter_allow_is_non_leasing_verdict() {
let controller = controller(2, 100);
assert!(controller.allow(2).await.unwrap());
let stats = controller.stats().await;
assert_eq!(
stats.in_flight, 0,
"Limiter::allow must not leak in-flight units"
);
assert_eq!(stats.admitted, 1);
}
#[tokio::test]
async fn test_peek_does_not_consume() {
let controller = controller(10, 50);
let _permit = controller.try_acquire(5).await.unwrap().unwrap();
let snapshot = controller.peek(1).await.unwrap();
assert_eq!(snapshot.limit, 50);
assert_eq!(snapshot.remaining, 45);
}
#[tokio::test]
async fn test_zero_cost_rejected() {
let controller = controller(10, 10);
assert!(controller.try_acquire(0).await.is_err());
assert!(controller.allow(0).await.is_err());
}
#[test]
#[should_panic(expected = "valid admission control config")]
fn test_invalid_config_panics_at_construction() {
let _ = AdmissionController::new(AdmissionControlConfig {
max_concurrent: 0,
max_per_second: 10,
});
}
}