use super::traits::{Limiter, validate_cost};
use crate::clock::{Clock, SystemClock};
use crate::error::LimiteronError;
use async_trait::async_trait;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
const DEFAULT_SHARD_COUNT: usize = 60;
pub struct ShardedSlidingWindowLimiter {
shards: Box<[AtomicU64; DEFAULT_SHARD_COUNT]>,
shard_timestamps: Box<[AtomicU64; DEFAULT_SHARD_COUNT]>,
window_size_secs: u64,
shard_duration_secs: u64,
max_requests: u64,
last_cleanup: AtomicU64,
clock: Arc<dyn Clock>,
}
impl ShardedSlidingWindowLimiter {
pub fn new(window_size: Duration, max_requests: u64) -> Self {
Self::with_clock(window_size, max_requests, Arc::new(SystemClock))
}
pub fn with_clock(window_size: Duration, max_requests: u64, clock: Arc<dyn Clock>) -> Self {
let window_size_secs = window_size.as_secs().max(1);
let shard_duration_secs = (window_size_secs / DEFAULT_SHARD_COUNT as u64).max(1);
let now_secs = clock.unix_timestamp();
let shards = Box::new([(); DEFAULT_SHARD_COUNT].map(|_| AtomicU64::new(0)));
let shard_timestamps = Box::new([(); DEFAULT_SHARD_COUNT].map(|_| AtomicU64::new(0)));
Self {
shards,
shard_timestamps,
window_size_secs,
shard_duration_secs,
max_requests,
last_cleanup: AtomicU64::new(now_secs),
clock,
}
}
fn current_timestamp_secs(&self) -> u64 {
self.clock.unix_timestamp()
}
#[inline]
fn get_shard_index(&self, timestamp_secs: u64) -> usize {
(timestamp_secs as usize) % DEFAULT_SHARD_COUNT
}
#[inline]
fn get_current_shard(&self) -> (usize, u64) {
let now_secs = self.current_timestamp_secs();
let shard_index = self.get_shard_index(now_secs);
(shard_index, now_secs)
}
fn increment_shard(&self, shard_index: usize, now_secs: u64, cost: u64) -> u64 {
let shard = &self.shards[shard_index];
let timestamp = &self.shard_timestamps[shard_index];
let expected_timestamp = now_secs / self.shard_duration_secs * self.shard_duration_secs;
loop {
let current_timestamp = timestamp.load(Ordering::Acquire);
if current_timestamp == expected_timestamp {
return shard.fetch_add(cost, Ordering::Release) + cost;
}
match timestamp.compare_exchange(
current_timestamp,
expected_timestamp,
Ordering::Release,
Ordering::Relaxed,
) {
Ok(_) => {
shard.store(cost, Ordering::Release);
return cost;
}
Err(_) => {
std::hint::spin_loop();
}
}
}
}
fn calculate_window_count(&self, now_secs: u64) -> u64 {
let window_start = now_secs.saturating_sub(self.window_size_secs);
let mut total_count = 0u64;
for i in 0..DEFAULT_SHARD_COUNT {
let shard_timestamp = self.shard_timestamps[i].load(Ordering::Acquire);
if shard_timestamp > window_start && shard_timestamp <= now_secs {
total_count += self.shards[i].load(Ordering::Acquire);
}
}
total_count
}
fn cleanup_expired_shards(&self, now_secs: u64) {
let window_start = now_secs.saturating_sub(self.window_size_secs);
for i in 0..DEFAULT_SHARD_COUNT {
let shard_timestamp = self.shard_timestamps[i].load(Ordering::Acquire);
if shard_timestamp <= window_start
&& shard_timestamp != 0
&& self.shard_timestamps[i]
.compare_exchange(shard_timestamp, 0, Ordering::Release, Ordering::Relaxed)
.is_ok()
{
self.shards[i].store(0, Ordering::Release);
}
}
}
fn maybe_cleanup(&self, now_secs: u64) {
let cleanup_interval = (self.shard_duration_secs / 10).max(1);
let last = self.last_cleanup.load(Ordering::Acquire);
if now_secs.saturating_sub(last) >= cleanup_interval
&& self
.last_cleanup
.compare_exchange(last, now_secs, Ordering::Release, Ordering::Relaxed)
.is_ok()
{
self.cleanup_expired_shards(now_secs);
}
}
fn try_acquire(&self, cost: u64) -> bool {
let (shard_index, now_secs) = self.get_current_shard();
let current_count = self.calculate_window_count(now_secs);
if current_count + cost > self.max_requests {
return false;
}
self.increment_shard(shard_index, now_secs, cost);
self.maybe_cleanup(now_secs);
true
}
#[cfg(test)]
pub fn get_window_count(&self) -> u64 {
let now_secs = self.current_timestamp_secs();
self.calculate_window_count(now_secs)
}
#[cfg(test)]
pub fn get_shard_count(&self, index: usize) -> u64 {
if index < DEFAULT_SHARD_COUNT {
self.shards[index].load(Ordering::SeqCst)
} else {
0
}
}
}
#[async_trait]
impl Limiter for ShardedSlidingWindowLimiter {
async fn allow(&self, cost: u64) -> Result<bool, LimiteronError> {
let cost = validate_cost(cost)?;
Ok(self.try_acquire(cost))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::clock::MockClock;
#[tokio::test]
async fn test_sharded_basic() {
let limiter = ShardedSlidingWindowLimiter::new(Duration::from_secs(60), 1000);
for _ in 0..10 {
assert!(limiter.allow(1).await.unwrap());
}
assert_eq!(limiter.get_window_count(), 10);
}
#[tokio::test]
async fn test_sharded_exceed() {
let limiter = ShardedSlidingWindowLimiter::new(Duration::from_secs(60), 5);
for _ in 0..5 {
assert!(limiter.allow(1).await.unwrap());
}
assert!(!limiter.allow(1).await.unwrap());
}
#[tokio::test]
async fn test_sharded_with_mock_clock() {
let mock_clock = Arc::new(MockClock::new());
let clock: Arc<dyn Clock> = mock_clock.clone();
let limiter = ShardedSlidingWindowLimiter::with_clock(Duration::from_secs(60), 5, clock);
for _ in 0..5 {
assert!(limiter.allow(1).await.unwrap());
}
assert!(!limiter.allow(1).await.unwrap());
mock_clock.advance(Duration::from_secs(61));
assert!(limiter.allow(1).await.unwrap());
}
#[tokio::test]
async fn test_sharded_zero_cost_rejected() {
let limiter = ShardedSlidingWindowLimiter::new(Duration::from_secs(60), 1000);
let result = limiter.allow(0).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_sharded_cost_exceeds_max() {
let limiter = ShardedSlidingWindowLimiter::new(Duration::from_secs(60), 1000);
let result = limiter.allow(u64::MAX).await;
assert!(result.is_err());
}
#[test]
fn test_sharded_get_shard_count_out_of_bounds() {
let limiter = ShardedSlidingWindowLimiter::new(Duration::from_secs(60), 1000);
let count = limiter.get_shard_count(999);
assert_eq!(count, 0);
}
#[test]
fn test_sharded_get_shard_count_valid_index() {
let limiter = ShardedSlidingWindowLimiter::new(Duration::from_secs(60), 1000);
let count = limiter.get_shard_count(0);
assert_eq!(count, 0);
}
#[tokio::test]
async fn test_sharded_get_window_count_after_requests() {
let limiter = ShardedSlidingWindowLimiter::new(Duration::from_secs(60), 1000);
for _ in 0..5 {
assert!(limiter.allow(1).await.unwrap());
}
let count = limiter.get_window_count();
assert_eq!(count, 5);
}
#[tokio::test]
async fn test_sharded_multiple_users() {
let limiter = ShardedSlidingWindowLimiter::new(Duration::from_secs(60), 3);
assert!(limiter.allow(1).await.unwrap());
assert!(limiter.allow(1).await.unwrap());
assert!(limiter.allow(1).await.unwrap());
assert!(!limiter.allow(1).await.unwrap());
}
#[tokio::test]
async fn test_sharded_with_small_window() {
let limiter = ShardedSlidingWindowLimiter::new(Duration::from_secs(1), 10);
assert!(limiter.allow(1).await.unwrap());
assert_eq!(limiter.get_window_count(), 1);
}
#[tokio::test]
async fn test_sharded_cost_greater_than_max() {
let limiter = ShardedSlidingWindowLimiter::new(Duration::from_secs(60), 5);
let result = limiter.allow(6).await;
assert!(result.is_ok());
assert!(!result.unwrap());
}
}