use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Gcra {
emission_interval: Duration,
tau: Duration,
burst: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Profile {
pub rate: u64,
pub window: Duration,
pub burst: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Verdict {
pub allowed: bool,
pub new_tat: Duration,
pub remaining: u64,
pub retry_after: Duration,
pub reset_after: Duration,
}
impl Gcra {
pub fn from_profile(profile: Profile) -> Self {
let rate = profile.rate.max(1);
let burst = profile.burst.max(1);
let window_nanos = profile.window.as_nanos().max(1);
let emission_nanos = window_nanos / u128::from(rate);
let emission_interval = duration_from_nanos(emission_nanos.max(1));
let tau = emission_interval * u32::try_from(burst - 1).unwrap_or(u32::MAX);
Self {
emission_interval,
tau,
burst,
}
}
pub fn check(&self, stored_tat: Option<Duration>, now: Duration) -> Verdict {
let t = self.emission_interval;
let tau = self.tau;
let tat = stored_tat.unwrap_or(now);
let admissible = if now + tau >= tat {
let slack = (now + tau) - tat; (1 + div_floor(slack, t)).min(self.burst)
} else {
0
};
if admissible == 0 {
let retry_after = tat.saturating_sub(tau).saturating_sub(now);
Verdict {
allowed: false,
new_tat: tat,
remaining: 0,
retry_after,
reset_after: tat.saturating_sub(now),
}
} else {
let new_tat = tat.max(now) + t;
Verdict {
allowed: true,
new_tat,
remaining: admissible - 1,
retry_after: Duration::ZERO,
reset_after: new_tat.saturating_sub(now),
}
}
}
}
fn div_floor(a: Duration, b: Duration) -> u64 {
let b = b.as_nanos().max(1);
u64::try_from(a.as_nanos() / b).unwrap_or(u64::MAX)
}
fn duration_from_nanos(nanos: u128) -> Duration {
let secs = nanos / 1_000_000_000;
let sub = (nanos % 1_000_000_000) as u32;
match u64::try_from(secs) {
Ok(secs) => Duration::new(secs, sub),
Err(_) => Duration::MAX,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn profile(rate: u64, window_secs: u64, burst: u64) -> Profile {
Profile {
rate,
window: Duration::from_secs(window_secs),
burst,
}
}
#[test]
fn burst_boundary_admits_exactly_burst() {
let g = Gcra::from_profile(profile(60, 60, 3)); let now = Duration::from_secs(100);
let mut tat = None;
for i in 0..3 {
let v = g.check(tat, now);
assert!(v.allowed, "request {i} should be allowed");
assert_eq!(v.remaining, (3 - 1 - i) as u64, "remaining after {i}");
tat = Some(v.new_tat);
}
let v = g.check(tat, now);
assert!(!v.allowed);
assert_eq!(v.remaining, 0);
assert_eq!(v.retry_after, Duration::from_secs(1));
}
#[test]
fn refills_at_steady_rate() {
let g = Gcra::from_profile(profile(60, 60, 2)); let start = Duration::from_secs(0);
let mut tat = None;
for _ in 0..2 {
let v = g.check(tat, start);
assert!(v.allowed);
tat = Some(v.new_tat);
}
assert!(!g.check(tat, start).allowed);
let later = start + Duration::from_secs(1);
let v = g.check(tat, later);
assert!(v.allowed);
tat = Some(v.new_tat);
assert!(!g.check(tat, later).allowed);
}
#[test]
fn tolerates_backward_clock() {
let g = Gcra::from_profile(profile(60, 60, 1)); let now = Duration::from_secs(1000);
let v = g.check(None, now);
assert!(v.allowed);
let tat = Some(v.new_tat);
let back = Duration::from_secs(500);
let v = g.check(tat, back);
assert!(!v.allowed);
assert!(v.retry_after > Duration::ZERO);
assert!(v.retry_after <= Duration::from_secs(501));
}
#[test]
fn reset_after_drains_over_time() {
let g = Gcra::from_profile(profile(60, 60, 5)); let start = Duration::from_secs(0);
let mut tat = None;
let mut last_reset = Duration::MAX;
for _ in 0..5 {
let v = g.check(tat, start);
assert!(v.allowed);
tat = Some(v.new_tat);
last_reset = v.reset_after;
}
assert_eq!(last_reset, Duration::from_secs(5));
}
#[test]
fn idle_key_remaining_capped_to_burst() {
let g = Gcra::from_profile(profile(60, 60, 3)); let first = g.check(None, Duration::from_secs(100));
assert!(first.allowed);
let v = g.check(Some(first.new_tat), Duration::from_secs(1000));
assert!(v.allowed);
assert_eq!(v.remaining, 2, "remaining must be capped to burst-1");
}
#[test]
fn burst_one_is_pure_rate_limit() {
let g = Gcra::from_profile(profile(2, 1, 1)); let now = Duration::from_secs(0);
let v = g.check(None, now);
assert!(v.allowed);
assert!(!g.check(Some(v.new_tat), now).allowed);
}
}