use super::traits::{Limiter, RateLimitSnapshot, 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,
admission_lock: parking_lot::Mutex<()>,
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),
admission_lock: parking_lot::Mutex::new(()),
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 _guard = self.admission_lock.lock();
let current_count = self.calculate_window_count(now_secs);
let allowed = if cost > self.max_requests || current_count > self.max_requests - cost {
false
} else {
self.increment_shard(shard_index, now_secs, cost);
true
};
drop(_guard);
if allowed {
self.maybe_cleanup(now_secs);
}
allowed
}
#[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))
}
async fn peek(&self, cost: u64) -> Result<RateLimitSnapshot, LimiteronError> {
let cost = validate_cost(cost)?;
let _ = cost;
Ok(self.current_snapshot())
}
async fn remaining(&self) -> Result<RateLimitSnapshot, LimiteronError> {
Ok(self.current_snapshot())
}
}
impl ShardedSlidingWindowLimiter {
fn current_snapshot(&self) -> RateLimitSnapshot {
let (_, now_secs) = self.get_current_shard();
let count = self.calculate_window_count(now_secs);
let shard_duration = self.shard_duration_secs.max(1);
let reset_secs = if count == 0 {
0
} else {
self.window_size_secs
.saturating_sub(now_secs.saturating_sub(self.oldest_active_shard_secs(now_secs)))
.min(self.window_size_secs)
.max(shard_duration)
};
RateLimitSnapshot {
limit: self.max_requests,
remaining: self.max_requests.saturating_sub(count),
reset_secs,
}
}
fn oldest_active_shard_secs(&self, now_secs: u64) -> u64 {
let window_start = now_secs.saturating_sub(self.window_size_secs);
let mut oldest = now_secs;
for ts in self.shard_timestamps.iter() {
let t = ts.load(Ordering::Acquire);
if t > window_start && t < oldest {
oldest = t;
}
}
oldest
}
}
#[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());
}
#[tokio::test]
async fn test_sharded_no_overshoot_under_concurrency() {
let max_requests = 25u64;
let limiter = Arc::new(ShardedSlidingWindowLimiter::new(
Duration::from_secs(60),
max_requests,
));
let tasks = 64usize;
let barrier = Arc::new(tokio::sync::Barrier::new(tasks));
let success = Arc::new(AtomicU64::new(0));
let mut handles = Vec::with_capacity(tasks);
for _ in 0..tasks {
let limiter = Arc::clone(&limiter);
let barrier = Arc::clone(&barrier);
let success = Arc::clone(&success);
handles.push(tokio::spawn(async move {
barrier.wait().await;
if limiter.allow(1).await.unwrap() {
success.fetch_add(1, Ordering::SeqCst);
}
}));
}
for h in handles {
h.await.unwrap();
}
assert!(
success.load(Ordering::SeqCst) <= max_requests,
"并发放行数 {} 超过上限 {}",
success.load(Ordering::SeqCst),
max_requests
);
assert_eq!(limiter.get_window_count(), success.load(Ordering::SeqCst));
}
#[tokio::test]
async fn test_sharded_overflow_safe_cost_comparison() {
let limiter = ShardedSlidingWindowLimiter::new(Duration::from_secs(60), u64::MAX);
assert!(limiter.allow(1).await.unwrap());
assert!(limiter.allow(u64::MAX).await.is_err()); assert!(limiter.allow(1_000_000).await.unwrap());
assert_eq!(limiter.get_window_count(), 1_000_001);
}
}