use crate::error::RateLimitEvidence;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
pub const DEFAULT_COOLDOWN_SECS: u64 = 300;
fn now_epoch_secs() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |d| d.as_secs())
}
#[derive(Debug)]
pub struct RateLimitGate {
reset_at: AtomicU64,
cooldown_secs: u64,
verified: AtomicBool,
}
impl RateLimitGate {
#[must_use]
pub const fn new(cooldown_secs: u64) -> Self {
Self {
reset_at: AtomicU64::new(0),
cooldown_secs,
verified: AtomicBool::new(false),
}
}
#[must_use]
pub fn is_tripped(&self) -> bool {
let reset_at = self.reset_at.load(Ordering::Relaxed);
reset_at != 0 && now_epoch_secs() < reset_at
}
#[must_use]
pub fn verified(&self) -> bool {
self.verified.load(Ordering::Relaxed)
}
#[must_use]
pub fn evidence(&self) -> RateLimitEvidence {
if self.verified() {
RateLimitEvidence::Confirmed
} else {
RateLimitEvidence::Inferred
}
}
pub fn trip(&self) {
self.trip_with(false);
}
pub fn trip_verified(&self) {
self.trip_with(true);
}
fn trip_with(&self, verified: bool) {
self.verified.store(verified, Ordering::Relaxed);
self.reset_at
.store(now_epoch_secs() + self.cooldown_secs, Ordering::Relaxed);
}
#[cfg(test)]
fn trip_until_epoch_secs(&self, reset_at: u64) {
self.reset_at.store(reset_at, Ordering::Relaxed);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rate_limit_gate_starts_untripped() {
let gate = RateLimitGate::new(300);
assert!(!gate.is_tripped());
}
#[test]
fn test_rate_limit_gate_trips_and_stays_tripped_within_cooldown() {
let gate = RateLimitGate::new(300);
gate.trip();
assert!(gate.is_tripped());
}
#[test]
fn test_rate_limit_gate_clears_after_reset_time_passes() {
let gate = RateLimitGate::new(300);
gate.trip_until_epoch_secs(1); assert!(!gate.is_tripped());
}
#[test]
fn test_rate_limit_gate_starts_unverified() {
let gate = RateLimitGate::new(300);
assert!(!gate.verified());
}
#[test]
fn test_rate_limit_gate_trip_is_unverified() {
let gate = RateLimitGate::new(300);
gate.trip();
assert!(gate.is_tripped());
assert!(!gate.verified());
}
#[test]
fn test_rate_limit_gate_trip_verified_is_verified() {
let gate = RateLimitGate::new(300);
gate.trip_verified();
assert!(gate.is_tripped());
assert!(gate.verified());
}
#[test]
fn test_rate_limit_gate_trip_after_trip_verified_clears_verified_flag() {
let gate = RateLimitGate::new(300);
gate.trip_verified();
assert!(gate.verified());
gate.trip();
assert!(!gate.verified());
}
}