use super::traits::{Limiter, validate_cost};
use crate::error::LimiteronError;
use async_trait::async_trait;
use parking_lot::RwLock;
use std::sync::atomic::{AtomicU64, Ordering};
#[derive(Debug, Clone)]
pub struct GcraCheckResult {
pub allowed: bool,
pub remaining: u64,
pub retry_after_us: u64,
}
pub struct GcraLimiter {
capacity: u64,
refill_interval_us: u64,
tat: RwLock<u64>,
last_now: AtomicU64,
}
impl GcraLimiter {
pub fn new(capacity: u64, refill_interval_us: u64) -> Self {
let refill_interval_us = refill_interval_us.max(1);
let now_us = Self::wall_us();
Self {
capacity,
refill_interval_us,
tat: RwLock::new(now_us),
last_now: AtomicU64::new(now_us),
}
}
pub fn with_rate(capacity: u64, requests_per_second: u64) -> Self {
let refill_interval_us = 1_000_000u64
.checked_div(requests_per_second)
.unwrap_or(1_000_000)
.max(1);
Self::new(capacity, refill_interval_us)
}
fn wall_us() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_micros() as u64)
.unwrap_or(0)
}
fn monotonic_now(&self, wall: u64) -> u64 {
let last = self.last_now.load(Ordering::Acquire);
let now = wall.max(last);
if now > last {
let _ = self
.last_now
.compare_exchange(last, now, Ordering::Release, Ordering::Relaxed);
}
now
}
fn now_us(&self) -> u64 {
self.monotonic_now(Self::wall_us())
}
pub fn capacity(&self) -> u64 {
self.capacity
}
pub fn refill_interval_us(&self) -> u64 {
self.refill_interval_us
}
pub fn tat(&self) -> u64 {
*self.tat.read()
}
pub fn check(&self, cost: u64) -> GcraCheckResult {
let now_us = self.now_us();
let tat = *self.tat.read();
if cost > self.capacity {
return GcraCheckResult {
allowed: false,
remaining: 0,
retry_after_us: 0,
};
}
let eat = tat.saturating_sub(
self.capacity
.saturating_sub(1)
.saturating_mul(self.refill_interval_us),
);
if now_us >= eat {
let elapsed = now_us.saturating_sub(eat);
let refilled = elapsed / self.refill_interval_us;
let remaining = std::cmp::min(self.capacity, refilled + 1 - cost);
GcraCheckResult {
allowed: true,
remaining,
retry_after_us: 0,
}
} else {
let retry_after = eat.saturating_sub(now_us);
GcraCheckResult {
allowed: false,
remaining: 0,
retry_after_us: retry_after,
}
}
}
pub fn remaining(&self) -> u64 {
let now_us = self.now_us();
let tat = *self.tat.read();
let eat = tat.saturating_sub(
self.capacity
.saturating_sub(1)
.saturating_mul(self.refill_interval_us),
);
if now_us >= eat {
let elapsed = now_us.saturating_sub(eat);
let refilled = elapsed / self.refill_interval_us;
std::cmp::min(self.capacity, refilled + 1)
} else {
0
}
}
}
#[async_trait]
impl Limiter for GcraLimiter {
async fn allow(&self, cost: u64) -> Result<bool, LimiteronError> {
validate_cost(cost)?;
if cost > self.capacity {
return Ok(false);
}
Ok(self.allow_at(cost, self.now_us()))
}
}
impl GcraLimiter {
fn allow_at(&self, cost: u64, now_us: u64) -> bool {
{
let mut tat = self.tat.write();
let eat = (*tat).saturating_sub(
self.capacity
.saturating_sub(1)
.saturating_mul(self.refill_interval_us),
);
if now_us >= eat {
*tat = std::cmp::max(*tat, now_us)
.saturating_add(cost.saturating_mul(self.refill_interval_us));
true
} else {
false
}
}
}
}
impl std::fmt::Debug for GcraLimiter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("GcraLimiter")
.field("capacity", &self.capacity)
.field("refill_interval_us", &self.refill_interval_us)
.field("tat", &self.tat())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_gcra_basic() {
let limiter = GcraLimiter::new(10, 1000);
assert!(limiter.allow(1).await.unwrap());
}
#[tokio::test]
async fn test_gcra_burst() {
let limiter = GcraLimiter::new(10, 1_000_000);
for _ in 0..10 {
assert!(limiter.allow(1).await.unwrap());
}
assert!(!limiter.allow(1).await.unwrap());
}
#[tokio::test]
async fn test_gcra_cost_validation() {
let limiter = GcraLimiter::new(10, 1000);
let result = limiter.allow(0).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_gcra_remaining() {
let limiter = GcraLimiter::new(10, 1000);
let remaining = limiter.remaining();
assert!(remaining <= 10);
}
#[tokio::test]
async fn test_gcra_check() {
let limiter = GcraLimiter::new(10, 1000);
let result = limiter.check(1);
assert!(result.allowed);
assert!(result.retry_after_us == 0);
}
#[test]
fn test_gcra_debug() {
let limiter = GcraLimiter::new(10, 1000);
let debug_str = format!("{:?}", limiter);
assert!(debug_str.contains("GcraLimiter"));
assert!(debug_str.contains("capacity"));
}
#[test]
fn test_gcra_with_rate() {
let limiter = GcraLimiter::with_rate(100, 1000);
assert_eq!(limiter.capacity(), 100);
assert_eq!(limiter.refill_interval_us(), 1000); }
#[tokio::test]
async fn test_gcra_high_cost() {
let limiter = GcraLimiter::new(10, 1000);
assert!(!limiter.allow(11).await.unwrap());
}
#[test]
fn test_gcra_with_rate_zero() {
let limiter = GcraLimiter::with_rate(10, 0);
assert_eq!(limiter.refill_interval_us(), 1_000_000);
assert_eq!(limiter.capacity(), 10);
}
#[test]
fn test_gcra_new_zero_interval_clamped() {
let limiter = GcraLimiter::new(10, 0);
assert_eq!(limiter.refill_interval_us(), 1);
}
#[tokio::test]
async fn test_gcra_with_rate_beyond_1m_rps_still_limits() {
let limiter = GcraLimiter::with_rate(100, 2_000_000);
assert_eq!(
limiter.refill_interval_us(),
1,
"interval should clamp to 1\u{00b5}s"
);
let mut allowed = 0u64;
for _ in 0..10_000 {
if limiter.allow(1).await.unwrap() {
allowed += 1;
}
}
assert!(
allowed < 10_000,
"10_000 requests all allowed with interval=0/1\u{00b5}s, rate limiting is ineffective"
);
assert!(
allowed >= 100,
"burst capacity {{100}} must allow at least 100, got {allowed}"
);
}
#[test]
fn test_gcra_check_cost_exceeds_capacity() {
let limiter = GcraLimiter::new(10, 1000);
let result = limiter.check(11);
assert!(!result.allowed);
assert_eq!(result.remaining, 0);
assert_eq!(result.retry_after_us, 0);
}
#[tokio::test]
async fn test_gcra_check_denied_path() {
let limiter = GcraLimiter::new(2, 100_000); assert!(limiter.allow(1).await.unwrap());
assert!(limiter.allow(1).await.unwrap());
let result = limiter.check(1);
assert!(!result.allowed);
assert_eq!(result.remaining, 0);
assert!(result.retry_after_us > 0);
}
#[tokio::test]
async fn test_gcra_remaining_denied_path() {
let limiter = GcraLimiter::new(2, 100_000);
let _ = limiter.allow(1).await;
let _ = limiter.allow(1).await;
let rem = limiter.remaining();
assert!(
rem <= 1,
"remaining should be low when exhausted, got {}",
rem
);
}
#[test]
fn test_gcra_tat_accessor() {
let limiter = GcraLimiter::new(10, 1000);
let tat = limiter.tat();
assert!(tat > 0);
}
#[tokio::test]
async fn test_gcra_check_allowed_with_remaining() {
let limiter = GcraLimiter::new(10, 1000);
let result = limiter.check(1);
assert!(result.allowed);
assert_eq!(result.retry_after_us, 0);
assert!(result.remaining <= 10);
}
#[test]
fn test_gcra_check_result_fields() {
let result = GcraCheckResult {
allowed: true,
remaining: 5,
retry_after_us: 0,
};
assert!(result.allowed);
assert_eq!(result.remaining, 5);
assert_eq!(result.retry_after_us, 0);
}
#[test]
fn test_gcra_monotonic_guard_freezes_on_rollback() {
let limiter = GcraLimiter::new(10, 1_000_000);
let base = limiter.tat();
assert_eq!(limiter.monotonic_now(base + 1_000_000), base + 1_000_000);
assert_eq!(limiter.monotonic_now(base + 500_000), base + 1_000_000);
assert_eq!(
limiter.monotonic_now(0),
base + 1_000_000,
"a pre-epoch wall clock must not deadlock observation"
);
assert_eq!(limiter.monotonic_now(base + 1_500_000), base + 1_500_000);
}
#[tokio::test]
async fn test_gcra_rollback_no_permanent_lockout() {
let limiter = GcraLimiter::new(5, 1_000_000); let t0 = limiter.tat();
for i in 0..5u64 {
assert!(
limiter.allow_at(1, t0 + i * 1_000),
"burst request {} should be allowed",
i + 1
);
}
assert!(
!limiter.allow_at(1, t0 + 5_000),
"must reject after the burst is exhausted"
);
assert!(!limiter.allow_at(1, t0.saturating_sub(10_000_000)));
assert!(limiter.allow_at(1, t0 + 5_000 + 5_000_000));
}
}