use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::Duration;
use sz_orm_limit::{RateLimiter, SlidingWindowRateLimiter, TokenBucketRateLimiter};
#[test]
fn stress_limit_sliding_window_10k_keys() {
let limiter = SlidingWindowRateLimiter::new(10, Duration::from_secs(60));
let n: usize = 10_000;
for i in 0..n {
let key = format!("user-{}", i);
for _ in 0..10 {
let result = limiter.acquire(&key).unwrap();
assert!(result.allowed, "key {} should be allowed", key);
}
let result = limiter.acquire(&key).unwrap();
assert!(!result.allowed, "key {} should be rejected", key);
}
}
#[test]
fn stress_limit_token_bucket_10k_keys() {
let limiter = TokenBucketRateLimiter::new(5, 1.0);
let n: usize = 10_000;
for i in 0..n {
let key = format!("user-{}", i);
for _ in 0..5 {
let result = limiter.acquire(&key).unwrap();
assert!(result.allowed, "key {} should be allowed", key);
}
let result = limiter.acquire(&key).unwrap();
assert!(!result.allowed, "key {} should be rejected", key);
}
}
#[test]
fn stress_limit_sliding_window_concurrent_same_key() {
let limiter = Arc::new(SlidingWindowRateLimiter::new(100, Duration::from_secs(60)));
let allowed = Arc::new(AtomicU64::new(0));
let rejected = Arc::new(AtomicU64::new(0));
let mut handles = Vec::new();
for _ in 0..8 {
let l = limiter.clone();
let a = allowed.clone();
let r = rejected.clone();
handles.push(std::thread::spawn(move || {
for _ in 0..1000 {
let result = l.acquire("shared-key").unwrap();
if result.allowed {
a.fetch_add(1, Ordering::SeqCst);
} else {
r.fetch_add(1, Ordering::SeqCst);
}
}
}));
}
for h in handles {
h.join().unwrap();
}
let total_allowed = allowed.load(Ordering::SeqCst);
let total_rejected = rejected.load(Ordering::SeqCst);
assert_eq!(total_allowed + total_rejected, 8000);
assert!(
total_allowed <= 100,
"allowed {} must be <= 100",
total_allowed
);
assert_eq!(total_allowed, 100, "exactly 100 should be allowed");
}
#[test]
fn stress_limit_token_bucket_concurrent_same_key() {
let limiter = Arc::new(TokenBucketRateLimiter::new(50, 0.0001));
let allowed = Arc::new(AtomicU64::new(0));
let rejected = Arc::new(AtomicU64::new(0));
let mut handles = Vec::new();
for _ in 0..8 {
let l = limiter.clone();
let a = allowed.clone();
let r = rejected.clone();
handles.push(std::thread::spawn(move || {
for _ in 0..1000 {
let result = l.acquire("shared-key").unwrap();
if result.allowed {
a.fetch_add(1, Ordering::SeqCst);
} else {
r.fetch_add(1, Ordering::SeqCst);
}
}
}));
}
for h in handles {
h.join().unwrap();
}
let total_allowed = allowed.load(Ordering::SeqCst);
let total_rejected = rejected.load(Ordering::SeqCst);
assert_eq!(total_allowed + total_rejected, 8000);
assert!(
total_allowed <= 100,
"allowed {} should be small",
total_allowed
);
}
#[test]
fn stress_limit_reset_allows_acquire() {
let limiter = SlidingWindowRateLimiter::new(5, Duration::from_secs(60));
for _ in 0..5 {
let result = limiter.acquire("key").unwrap();
assert!(result.allowed);
}
let result = limiter.acquire("key").unwrap();
assert!(!result.allowed);
limiter.reset("key").unwrap();
for _ in 0..5 {
let result = limiter.acquire("key").unwrap();
assert!(result.allowed, "should be allowed after reset");
}
}
#[test]
fn stress_limit_try_acquire_matches_acquire() {
let limiter = SlidingWindowRateLimiter::new(10, Duration::from_secs(60));
for _ in 0..10 {
let r1 = limiter.acquire("key").unwrap();
assert!(r1.allowed);
}
let r2 = limiter.try_acquire("key").unwrap();
assert!(!r2.allowed);
}
#[test]
fn stress_limit_sliding_window_expiry() {
let limiter = SlidingWindowRateLimiter::new(3, Duration::from_millis(100));
for _ in 0..3 {
let result = limiter.acquire("key").unwrap();
assert!(result.allowed);
}
let result = limiter.acquire("key").unwrap();
assert!(!result.allowed);
std::thread::sleep(Duration::from_millis(150));
let result = limiter.acquire("key").unwrap();
assert!(result.allowed, "should be allowed after window expires");
}
#[test]
fn stress_limit_token_bucket_refill() {
let limiter = TokenBucketRateLimiter::new(2, 100.0);
for _ in 0..2 {
let result = limiter.acquire("key").unwrap();
assert!(result.allowed);
}
let result = limiter.acquire("key").unwrap();
assert!(!result.allowed, "should be rejected when bucket empty");
std::thread::sleep(Duration::from_millis(50));
let result = limiter.acquire("key").unwrap();
assert!(result.allowed, "should be allowed after refill");
}
#[test]
fn stress_limit_different_keys_independent() {
let limiter = SlidingWindowRateLimiter::new(5, Duration::from_secs(60));
for i in 0..1000 {
let key = format!("key-{}", i);
for _ in 0..5 {
let result = limiter.acquire(&key).unwrap();
assert!(result.allowed, "key {} should be allowed", key);
}
let result = limiter.acquire(&key).unwrap();
assert!(!result.allowed);
}
}