use super::traits::{Limiter, validate_cost};
use crate::error::LimiteronError;
use async_trait::async_trait;
use parking_lot::RwLock;
#[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>,
}
impl GcraLimiter {
pub fn new(capacity: u64, refill_interval_us: u64) -> Self {
let now_us = Self::now_us();
Self {
capacity,
refill_interval_us,
tat: RwLock::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);
Self::new(capacity, refill_interval_us)
}
fn now_us() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_micros() as u64)
.unwrap_or(0)
}
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)) * 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)) * 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)?;
let now_us = Self::now_us();
if cost > self.capacity {
return Ok(false);
}
let allowed = {
let mut tat = self.tat.write();
let eat =
(*tat).saturating_sub((self.capacity.saturating_sub(1)) * self.refill_interval_us);
if now_us >= eat {
*tat = std::cmp::max(*tat, now_us) + cost * self.refill_interval_us;
true
} else {
false
}
};
Ok(allowed)
}
}
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_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);
}
}