use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
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,
}
impl RateLimitGate {
#[must_use]
pub const fn new(cooldown_secs: u64) -> Self {
Self {
reset_at: AtomicU64::new(0),
cooldown_secs,
}
}
#[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
}
pub fn trip(&self) {
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());
}
}