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::time::Duration;
#[derive(Debug, Clone)]
pub struct PriorityQueueConfig {
pub window: Duration,
pub total_per_window: u64,
pub level_weights: Vec<u64>,
pub default_priority: usize,
}
impl PriorityQueueConfig {
pub fn validate(&self) -> Result<(), LimiteronError> {
if self.window.is_zero() {
return Err(LimiteronError::ConfigError(
"priority-queue window must be non-zero".to_string(),
));
}
if self.total_per_window == 0 {
return Err(LimiteronError::ConfigError(
"priority-queue total_per_window must be non-zero".to_string(),
));
}
if self.level_weights.is_empty() {
return Err(LimiteronError::ConfigError(
"priority-queue level_weights must not be empty".to_string(),
));
}
if self.level_weights.contains(&0) {
return Err(LimiteronError::ConfigError(
"priority-queue level_weights must not contain zero".to_string(),
));
}
if self.default_priority >= self.level_weights.len() {
return Err(LimiteronError::ConfigError(format!(
"priority-queue default_priority {} out of range (levels: {})",
self.default_priority,
self.level_weights.len()
)));
}
Ok(())
}
}
#[derive(Debug)]
struct WindowState {
window_start: u64,
used: Vec<u64>,
}
pub struct PriorityQueueLimiter {
total: u64,
window_secs: u64,
default_priority: usize,
reserved: Vec<u64>,
clock: Arc<dyn Clock>,
state: Mutex<WindowState>,
}
impl PriorityQueueLimiter {
pub fn new(config: PriorityQueueConfig) -> Result<Self, LimiteronError> {
Self::with_clock(config, Arc::new(SystemClock))
}
pub fn with_clock(
config: PriorityQueueConfig,
clock: Arc<dyn Clock>,
) -> Result<Self, LimiteronError> {
config.validate()?;
let weights_sum: u64 = config.level_weights.iter().sum();
let mut reserved: Vec<u64> = config
.level_weights
.iter()
.map(|w| config.total_per_window * w / weights_sum)
.collect();
let mut remainder = config.total_per_window - reserved.iter().sum::<u64>();
for slot in reserved.iter_mut() {
if remainder == 0 {
break;
}
*slot += 1;
remainder -= 1;
}
let window_start = clock.unix_timestamp();
Ok(Self {
total: reserved.iter().sum(),
window_secs: config.window.as_secs().max(1),
default_priority: config.default_priority,
reserved,
clock,
state: Mutex::new(WindowState {
window_start,
used: vec![0; config.level_weights.len()],
}),
})
}
pub fn level_count(&self) -> usize {
self.reserved.len()
}
pub async fn level_usage(&self) -> Vec<u64> {
let mut state = self.state.lock();
self.advance_window(&mut state);
state.used.clone()
}
pub async fn allow_with_priority(
&self,
priority: usize,
cost: u64,
) -> Result<bool, LimiteronError> {
let _cost = validate_cost(cost)?;
if priority >= self.reserved.len() {
return Err(LimiteronError::ConfigError(format!(
"priority {} out of range (levels: {})",
priority,
self.reserved.len()
)));
}
let mut state = self.state.lock();
self.advance_window(&mut state);
let total_used: u64 = state.used.iter().sum();
if total_used > self.total.saturating_sub(cost) {
return Ok(false);
}
let level_used = state.used[priority];
if level_used <= self.reserved[priority].saturating_sub(cost) {
state.used[priority] += cost;
return Ok(true);
}
let borrowable: u64 = self.reserved[priority + 1..]
.iter()
.zip(state.used[priority + 1..].iter())
.map(|(reserved, used)| reserved.saturating_sub(*used))
.sum();
let overage = level_used + cost - self.reserved[priority];
if overage <= borrowable {
state.used[priority] += cost;
return Ok(true);
}
Ok(false)
}
fn advance_window(&self, state: &mut WindowState) {
let now = self.clock.unix_timestamp();
if now >= state.window_start.saturating_add(self.window_secs) {
state.window_start = now;
state.used.iter_mut().for_each(|u| *u = 0);
}
}
}
#[async_trait]
impl Limiter for PriorityQueueLimiter {
async fn allow(&self, cost: u64) -> Result<bool, LimiteronError> {
self.allow_with_priority(self.default_priority, cost).await
}
async fn peek(&self, cost: u64) -> Result<RateLimitSnapshot, LimiteronError> {
let _cost = validate_cost(cost)?;
let mut state = self.state.lock();
self.advance_window(&mut state);
let used: u64 = state.used.iter().sum();
Ok(RateLimitSnapshot {
limit: self.total,
remaining: self.total.saturating_sub(used),
reset_secs: state
.window_start
.saturating_add(self.window_secs)
.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 config(total: u64, weights: Vec<u64>) -> PriorityQueueConfig {
PriorityQueueConfig {
window: Duration::from_secs(1),
total_per_window: total,
level_weights: weights,
default_priority: 2,
}
}
#[tokio::test]
async fn test_admits_within_reserved_band() {
let limiter =
PriorityQueueLimiter::new(config(100, vec![5, 3, 2])).expect("config should be valid");
assert!(limiter.allow_with_priority(1, 30).await.unwrap());
assert!(limiter.allow_with_priority(2, 20).await.unwrap());
assert!(!limiter.allow_with_priority(1, 1).await.unwrap());
}
#[tokio::test]
async fn test_high_priority_borrows_from_lower_levels() {
let limiter =
PriorityQueueLimiter::new(config(100, vec![5, 3, 2])).expect("config should be valid");
assert!(limiter.allow_with_priority(0, 50).await.unwrap());
assert!(limiter.allow_with_priority(0, 50).await.unwrap());
assert!(!limiter.allow_with_priority(0, 1).await.unwrap());
}
#[tokio::test]
async fn test_low_priority_cannot_borrow_from_higher_levels() {
let limiter =
PriorityQueueLimiter::new(config(100, vec![5, 3, 2])).expect("config should be valid");
assert!(limiter.allow_with_priority(0, 90).await.unwrap());
assert!(limiter.allow_with_priority(2, 10).await.unwrap());
assert!(!limiter.allow_with_priority(2, 1).await.unwrap());
}
#[tokio::test]
async fn test_window_reset_restores_quota() {
let clock = Arc::new(MockClock::new());
let limiter = PriorityQueueLimiter::with_clock(config(100, vec![5, 3, 2]), clock.clone())
.expect("config should be valid");
assert!(limiter.allow_with_priority(0, 100).await.unwrap());
assert!(!limiter.allow_with_priority(0, 1).await.unwrap());
clock.advance(Duration::from_secs(2));
assert!(limiter.allow_with_priority(0, 100).await.unwrap());
}
#[tokio::test]
async fn test_default_priority_drives_limiter_allow() {
let mut cfg = config(100, vec![5, 3, 2]);
cfg.default_priority = 0;
let limiter = PriorityQueueLimiter::new(cfg).expect("config should be valid");
assert!(limiter.allow(60).await.unwrap());
assert!(limiter.allow(40).await.unwrap());
}
#[tokio::test]
async fn test_peek_does_not_consume() {
let limiter =
PriorityQueueLimiter::new(config(100, vec![5, 3, 2])).expect("config should be valid");
assert!(limiter.allow_with_priority(0, 40).await.unwrap());
let snapshot = limiter.peek(1).await.unwrap();
assert_eq!(snapshot.limit, 100);
assert_eq!(snapshot.remaining, 60);
let again = limiter.remaining().await.unwrap();
assert_eq!(again.remaining, 60);
}
#[tokio::test]
async fn test_out_of_range_priority_rejected() {
let limiter =
PriorityQueueLimiter::new(config(100, vec![5, 3, 2])).expect("config should be valid");
assert!(limiter.allow_with_priority(3, 1).await.is_err());
}
#[test]
fn test_invalid_configs_rejected() {
assert!(PriorityQueueLimiter::new(config(100, vec![])).is_err());
assert!(PriorityQueueLimiter::new(config(0, vec![1, 1])).is_err());
let mut cfg = config(100, vec![5, 3, 2]);
cfg.default_priority = 3;
assert!(PriorityQueueLimiter::new(cfg).is_err());
}
#[tokio::test]
async fn test_level_usage_observable() {
let limiter =
PriorityQueueLimiter::new(config(100, vec![5, 3, 2])).expect("config should be valid");
assert!(limiter.allow_with_priority(1, 10).await.unwrap());
assert_eq!(limiter.level_count(), 3);
assert_eq!(limiter.level_usage().await, vec![0, 10, 0]);
}
}