use std::time::Duration;
use crate::algorithm::{current_timestamp_ms, timestamp_to_instant, Algorithm};
use crate::decision::{Decision, DecisionMetadata, RateLimitInfo};
use crate::error::Result;
use crate::quota::Quota;
use crate::storage::{Storage, StorageEntry};
#[derive(Debug, Clone, Default)]
pub struct GCRA;
impl GCRA {
pub fn new() -> Self {
Self
}
fn calculate_decision(
&self,
current_tat: Option<u64>,
now: u64,
quota: &Quota,
) -> (bool, u64) {
let period_ms = quota.period().as_millis() as u64;
let max_tat_offset_ms = quota.max_tat_offset().as_millis() as u64;
let effective_tat = current_tat.unwrap_or(now);
let new_tat = effective_tat.max(now) + period_ms;
let tat_offset = new_tat.saturating_sub(now);
if tat_offset <= max_tat_offset_ms + period_ms {
(true, new_tat)
} else {
(false, effective_tat)
}
}
fn build_info(&self, tat: u64, now: u64, quota: &Quota, allowed: bool) -> RateLimitInfo {
let period_ms = quota.period().as_millis() as u64;
let max_tat_offset_ms = quota.max_tat_offset().as_millis() as u64;
let limit = quota.effective_burst();
let tat_offset = tat.saturating_sub(now);
let remaining = if tat_offset == 0 {
limit
} else {
let used = (tat_offset / period_ms) + 1;
limit.saturating_sub(used)
};
let reset_at = if tat > now {
timestamp_to_instant(tat)
} else {
timestamp_to_instant(now)
};
let mut info = RateLimitInfo::new(limit, remaining, reset_at, timestamp_to_instant(now))
.with_algorithm("gcra")
.with_metadata(DecisionMetadata::new().with_tat(tat));
if !allowed {
let wait_ms = tat.saturating_sub(now).saturating_sub(max_tat_offset_ms);
if wait_ms > 0 {
info = info.with_retry_after(Duration::from_millis(wait_ms));
}
}
info
}
}
impl Algorithm for GCRA {
fn name(&self) -> &'static str {
"gcra"
}
async fn check_and_record<S: Storage>(
&self,
storage: &S,
key: &str,
quota: &Quota,
) -> Result<Decision> {
let now = current_timestamp_ms();
let period_ms = quota.period().as_millis() as u64;
let ttl = Duration::from_millis(
quota.max_tat_offset().as_millis() as u64 + period_ms * 2
);
let decision = storage
.execute_atomic(key, ttl, |entry| {
let current_tat = entry.and_then(|e| e.tat);
let (allowed, new_tat) = self.calculate_decision(current_tat, now, quota);
let new_entry = StorageEntry::with_tat(new_tat);
let info = self.build_info(new_tat, now, quota, allowed);
let decision = if allowed {
Decision::allowed(info)
} else {
Decision::denied(info)
};
(new_entry, decision)
})
.await?;
Ok(decision)
}
async fn check<S: Storage>(
&self,
storage: &S,
key: &str,
quota: &Quota,
) -> Result<Decision> {
let now = current_timestamp_ms();
let entry = storage.get(key).await?;
let current_tat = entry.and_then(|e| e.tat);
let (allowed, effective_tat) = self.calculate_decision(current_tat, now, quota);
let info = self.build_info(effective_tat, now, quota, allowed);
Ok(if allowed {
Decision::allowed(info)
} else {
Decision::denied(info)
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::storage::MemoryStorage;
#[tokio::test]
async fn test_gcra_basic() {
let algorithm = GCRA::new();
let storage = MemoryStorage::new();
let quota = Quota::per_second(10);
let decision = algorithm
.check_and_record(&storage, "user:1", "a)
.await
.unwrap();
assert!(decision.is_allowed());
}
#[tokio::test]
async fn test_gcra_burst() {
let algorithm = GCRA::new();
let storage = MemoryStorage::new();
let quota = Quota::per_second(1).with_burst(5);
for i in 1..=5 {
let decision = algorithm
.check_and_record(&storage, "user:1", "a)
.await
.unwrap();
assert!(decision.is_allowed(), "Request {} should be allowed", i);
}
let decision = algorithm
.check_and_record(&storage, "user:1", "a)
.await
.unwrap();
assert!(decision.is_denied(), "Request 6 should be denied");
assert!(decision.info().retry_after.is_some());
}
#[tokio::test]
async fn test_gcra_recovery() {
let algorithm = GCRA::new();
let storage = MemoryStorage::new();
let quota = Quota::per_second(10).with_burst(2);
algorithm.check_and_record(&storage, "user:1", "a).await.unwrap();
algorithm.check_and_record(&storage, "user:1", "a).await.unwrap();
let decision = algorithm
.check_and_record(&storage, "user:1", "a)
.await
.unwrap();
assert!(decision.is_denied());
tokio::time::sleep(Duration::from_millis(150)).await;
let decision = algorithm
.check_and_record(&storage, "user:1", "a)
.await
.unwrap();
assert!(decision.is_allowed());
}
#[tokio::test]
async fn test_gcra_check_without_record() {
let algorithm = GCRA::new();
let storage = MemoryStorage::new();
let quota = Quota::per_second(10).with_burst(5);
let decision = algorithm.check(&storage, "user:1", "a).await.unwrap();
assert!(decision.is_allowed());
let decision = algorithm.check(&storage, "user:1", "a).await.unwrap();
assert!(decision.is_allowed());
algorithm.check_and_record(&storage, "user:1", "a).await.unwrap();
let decision = algorithm.check(&storage, "user:1", "a).await.unwrap();
assert!(decision.info().remaining < 5);
}
#[tokio::test]
async fn test_gcra_separate_keys() {
let algorithm = GCRA::new();
let storage = MemoryStorage::new();
let quota = Quota::per_second(1).with_burst(1);
algorithm.check_and_record(&storage, "user:1", "a).await.unwrap();
let decision = algorithm.check_and_record(&storage, "user:1", "a).await.unwrap();
assert!(decision.is_denied());
let decision = algorithm.check_and_record(&storage, "user:2", "a).await.unwrap();
assert!(decision.is_allowed());
}
#[tokio::test]
async fn test_gcra_reset() {
let algorithm = GCRA::new();
let storage = MemoryStorage::new();
let quota = Quota::per_second(1).with_burst(1);
algorithm.check_and_record(&storage, "user:1", "a).await.unwrap();
let decision = algorithm.check_and_record(&storage, "user:1", "a).await.unwrap();
assert!(decision.is_denied());
algorithm.reset(&storage, "user:1").await.unwrap();
let decision = algorithm.check_and_record(&storage, "user:1", "a).await.unwrap();
assert!(decision.is_allowed());
}
#[test]
fn test_algorithm_name() {
let algorithm = GCRA::new();
assert_eq!(algorithm.name(), "gcra");
}
}