use super::traits::{Limiter, RateLimitSnapshot, validate_cost};
use crate::clock::{Clock, SystemClock};
use crate::constants::{MAX_TOKEN_BUCKET_CAPACITY, MAX_TOKEN_BUCKET_REFILL_RATE};
use crate::error::LimiteronError;
use async_trait::async_trait;
use parking_lot::Mutex;
use std::sync::Arc;
pub struct LeakyBucketLimiter {
capacity: u64,
leak_rate: u64,
clock: Arc<dyn Clock>,
state: Mutex<LeakyState>,
}
#[derive(Debug)]
struct LeakyState {
water: u64,
last_leak_nanos: u64,
}
impl LeakyBucketLimiter {
pub fn new(capacity: u64, leak_rate: u64) -> Result<Self, LimiteronError> {
Self::with_clock(capacity, leak_rate, Arc::new(SystemClock))
}
pub fn with_clock(
capacity: u64,
leak_rate: u64,
clock: Arc<dyn Clock>,
) -> Result<Self, LimiteronError> {
if capacity == 0 {
return Err(LimiteronError::ConfigError(
"leaky bucket capacity must be non-zero".to_string(),
));
}
if leak_rate == 0 {
return Err(LimiteronError::ConfigError(
"leaky bucket leak_rate must be non-zero".to_string(),
));
}
if capacity > MAX_TOKEN_BUCKET_CAPACITY {
return Err(LimiteronError::ConfigError(format!(
"leaky bucket capacity must not exceed {MAX_TOKEN_BUCKET_CAPACITY}"
)));
}
if leak_rate > MAX_TOKEN_BUCKET_REFILL_RATE {
return Err(LimiteronError::ConfigError(format!(
"leaky bucket leak_rate must not exceed {MAX_TOKEN_BUCKET_REFILL_RATE}"
)));
}
let now = clock.unix_timestamp_nanos();
Ok(Self {
capacity,
leak_rate,
clock,
state: Mutex::new(LeakyState {
water: 0,
last_leak_nanos: now,
}),
})
}
pub fn capacity(&self) -> u64 {
self.capacity
}
pub fn leak_rate(&self) -> u64 {
self.leak_rate
}
pub async fn water_level(&self) -> u64 {
let state = self.state.lock();
self.virtual_water(&state)
}
fn virtual_water(&self, state: &LeakyState) -> u64 {
let now = self.clock.unix_timestamp_nanos();
let elapsed = now.saturating_sub(state.last_leak_nanos);
let leaked = ((elapsed as u128) * (self.leak_rate as u128) / 1_000_000_000u128)
.try_into()
.unwrap_or(u64::MAX);
state.water.saturating_sub(leaked)
}
fn leak(&self, state: &mut LeakyState) {
let now = self.clock.unix_timestamp_nanos();
let elapsed = now.saturating_sub(state.last_leak_nanos);
if elapsed == 0 {
return;
}
let leaked = ((elapsed as u128) * (self.leak_rate as u128) / 1_000_000_000u128)
.try_into()
.unwrap_or(u64::MAX);
if leaked == 0 {
return;
}
if leaked >= state.water {
state.water = 0;
state.last_leak_nanos = now;
} else {
state.water -= leaked;
let consumed: u64 = (leaked as u128)
.checked_mul(1_000_000_000u128)
.map(|v| v / (self.leak_rate as u128))
.and_then(|v| u64::try_from(v).ok())
.unwrap_or(u64::MAX);
state.last_leak_nanos += consumed.min(elapsed);
}
}
}
#[async_trait]
impl Limiter for LeakyBucketLimiter {
async fn allow(&self, cost: u64) -> Result<bool, LimiteronError> {
validate_cost(cost)?;
let mut state = self.state.lock();
self.leak(&mut state);
if cost > self.capacity.saturating_sub(state.water) {
return Ok(false);
}
state.water += cost;
Ok(true)
}
async fn peek(&self, cost: u64) -> Result<RateLimitSnapshot, LimiteronError> {
validate_cost(cost)?;
let state = self.state.lock();
let water = self.virtual_water(&state);
Ok(self.snapshot(water))
}
async fn remaining(&self) -> Result<RateLimitSnapshot, LimiteronError> {
let state = self.state.lock();
let water = self.virtual_water(&state);
Ok(self.snapshot(water))
}
}
impl LeakyBucketLimiter {
fn snapshot(&self, water: u64) -> RateLimitSnapshot {
let reset_secs = water.div_ceil(self.leak_rate);
RateLimitSnapshot {
limit: self.capacity,
remaining: self.capacity.saturating_sub(water),
reset_secs,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::clock::MockClock;
use std::time::Duration;
#[tokio::test]
async fn test_leaky_bucket_basic_allow_and_reject() {
let limiter = LeakyBucketLimiter::new(10, 5).unwrap();
for _ in 0..10 {
assert!(limiter.allow(1).await.unwrap());
}
assert_eq!(limiter.water_level().await, 10);
assert!(!limiter.allow(1).await.unwrap());
}
#[tokio::test]
async fn test_leaky_bucket_cost_exceeding_capacity_always_rejected() {
let limiter = LeakyBucketLimiter::new(10, 5).unwrap();
assert!(!limiter.allow(11).await.unwrap());
assert_eq!(
limiter.water_level().await,
0,
"a rejected injection must not change the water level"
);
}
#[tokio::test]
async fn test_leaky_bucket_constant_leak_with_mock_clock() {
let mock_clock = Arc::new(MockClock::new());
let clock: Arc<dyn Clock> = mock_clock.clone();
let limiter = LeakyBucketLimiter::with_clock(10, 5, clock).unwrap();
assert!(limiter.allow(10).await.unwrap());
assert_eq!(limiter.water_level().await, 10);
mock_clock.advance(Duration::from_secs(1));
assert_eq!(limiter.water_level().await, 5);
assert!(limiter.allow(5).await.unwrap());
assert_eq!(limiter.water_level().await, 10);
assert!(!limiter.allow(1).await.unwrap());
}
#[tokio::test]
async fn test_leaky_bucket_drain_frees_space_for_burst() {
let mock_clock = Arc::new(MockClock::new());
let clock: Arc<dyn Clock> = mock_clock.clone();
let limiter = LeakyBucketLimiter::with_clock(10, 10, clock).unwrap();
assert!(limiter.allow(10).await.unwrap());
assert!(!limiter.allow(1).await.unwrap());
mock_clock.advance(Duration::from_secs(1));
for _ in 0..10 {
assert!(limiter.allow(1).await.unwrap());
}
assert!(!limiter.allow(1).await.unwrap());
}
#[tokio::test]
async fn test_leaky_bucket_leak_capped_at_empty_no_negative_accumulation() {
let mock_clock = Arc::new(MockClock::new());
let clock: Arc<dyn Clock> = mock_clock.clone();
let limiter = LeakyBucketLimiter::with_clock(10, 100, clock).unwrap();
assert!(limiter.allow(1).await.unwrap());
mock_clock.advance(Duration::from_secs(100));
assert_eq!(
limiter.water_level().await,
0,
"leak is capped by the water level, must not go negative"
);
assert!(limiter.allow(10).await.unwrap());
assert!(!limiter.allow(1).await.unwrap());
}
#[tokio::test]
async fn test_leaky_bucket_fraction_conservation() {
let limiter = LeakyBucketLimiter::new(100, 3).unwrap();
for _ in 0..10 {
tokio::time::sleep(Duration::from_millis(500)).await;
assert!(
limiter.allow(1).await.unwrap(),
"a low-rate leaky bucket should keep allowing"
);
}
let water = limiter.water_level().await;
assert!(
water <= 5,
"10x500ms@3/s integral conservation should leak ~15 units (gross injection 10 - leak ~15 <= 0), actual backlog {water}"
);
}
#[tokio::test]
async fn test_leaky_bucket_peek_remaining_pure() {
let mock_clock = Arc::new(MockClock::new());
let clock: Arc<dyn Clock> = mock_clock.clone();
let limiter = LeakyBucketLimiter::with_clock(10, 5, clock).unwrap();
assert!(limiter.allow(10).await.unwrap());
mock_clock.advance(Duration::from_secs(2));
let snap = limiter.remaining().await.unwrap();
assert_eq!(snap.limit, 10);
assert_eq!(
snap.remaining, 10,
"10@5/s leaks 10; after virtual rollout the bucket is empty, the remaining entry space should be full capacity"
);
for _ in 0..100 {
let _ = limiter.peek(1).await.unwrap();
}
assert_eq!(limiter.water_level().await, 0);
assert!(limiter.allow(10).await.unwrap());
assert!(!limiter.allow(1).await.unwrap());
}
#[tokio::test]
async fn test_leaky_bucket_snapshot_reset_secs() {
let mock_clock = Arc::new(MockClock::new());
let clock: Arc<dyn Clock> = mock_clock.clone();
let limiter = LeakyBucketLimiter::with_clock(10, 5, clock).unwrap();
let snap = limiter.remaining().await.unwrap();
assert_eq!(snap.reset_secs, 0);
assert!(limiter.allow(10).await.unwrap());
let snap = limiter.remaining().await.unwrap();
assert_eq!(snap.remaining, 0);
assert_eq!(snap.reset_secs, 2);
}
#[tokio::test]
async fn test_leaky_bucket_zero_cost_rejected() {
let limiter = LeakyBucketLimiter::new(10, 5).unwrap();
assert!(limiter.allow(0).await.is_err());
assert!(limiter.peek(0).await.is_err());
}
#[tokio::test]
async fn test_leaky_bucket_cost_exceeds_max_rejected() {
let limiter = LeakyBucketLimiter::new(10, 5).unwrap();
assert!(limiter.allow(1_000_001).await.is_err());
}
#[tokio::test]
async fn test_leaky_bucket_invalid_config_rejected() {
assert!(
LeakyBucketLimiter::new(0, 5).is_err(),
"capacity 0 must be rejected"
);
assert!(
LeakyBucketLimiter::new(10, 0).is_err(),
"leak rate 0 must be rejected"
);
}
#[tokio::test]
async fn test_leaky_bucket_constructor_enforces_ceiling() {
assert!(
LeakyBucketLimiter::new(MAX_TOKEN_BUCKET_CAPACITY + 1, 5).is_err(),
"over-limit capacity must be rejected"
);
assert!(
LeakyBucketLimiter::new(10, MAX_TOKEN_BUCKET_REFILL_RATE + 1).is_err(),
"over-limit leak rate must be rejected"
);
assert!(
LeakyBucketLimiter::new(MAX_TOKEN_BUCKET_CAPACITY, MAX_TOKEN_BUCKET_REFILL_RATE)
.is_ok()
);
}
#[tokio::test]
async fn test_leaky_bucket_concurrent_allow_respects_capacity() {
let mock_clock = Arc::new(MockClock::new());
let clock: Arc<dyn Clock> = mock_clock.clone();
let limiter = Arc::new(LeakyBucketLimiter::with_clock(100, 1_000_000, clock).unwrap());
let admitted = Arc::new(std::sync::atomic::AtomicU64::new(0));
let mut handles = Vec::new();
for _ in 0..8 {
let limiter = Arc::clone(&limiter);
let admitted = Arc::clone(&admitted);
handles.push(tokio::spawn(async move {
for _ in 0..50 {
if limiter.allow(1).await.unwrap() {
admitted.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
}
}));
}
for handle in handles {
handle.await.unwrap();
}
assert_eq!(
admitted.load(std::sync::atomic::Ordering::Relaxed),
100,
"the total concurrent injection must exactly equal the bucket capacity"
);
assert_eq!(limiter.water_level().await, 100);
}
#[test]
fn test_leaky_bucket_accessors() {
let limiter = LeakyBucketLimiter::new(500, 25).unwrap();
assert_eq!(limiter.capacity(), 500);
assert_eq!(limiter.leak_rate(), 25);
}
#[tokio::test]
async fn test_leaky_bucket_check_default_impl_maps_rejection() {
use crate::limiters::Limiter;
let limiter = LeakyBucketLimiter::new(1, 1).unwrap();
assert!(limiter.check("any_key").await.is_ok());
let result = limiter.check("any_key").await;
assert!(
result.is_err(),
"check must return Err once the bucket is full"
);
}
}