use std::time::Duration;
use ratelimit::Ratelimiter;
const CAPACITY: u64 = 20;
const REFILL_PER_SEC: u64 = 2;
const STRIKE_LIMIT: u32 = 100;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Verdict {
Allow,
Drop,
Close,
}
pub struct ControlGate {
limiter: Ratelimiter,
strikes: u32,
strike_limit: u32,
}
impl ControlGate {
pub fn new() -> Self {
Self::with_params(CAPACITY, REFILL_PER_SEC, STRIKE_LIMIT)
}
pub fn with_params(capacity: u64, refill_per_sec: u64, strike_limit: u32) -> Self {
let limiter = Ratelimiter::builder(refill_per_sec, Duration::from_secs(1))
.max_tokens(capacity)
.initial_available(capacity)
.build()
.expect("valid ratelimiter parameters");
Self {
limiter,
strikes: 0,
strike_limit,
}
}
pub fn check(&mut self) -> Verdict {
match self.limiter.try_wait() {
Ok(()) => {
self.strikes = self.strikes.saturating_sub(1);
Verdict::Allow
}
Err(_) => {
self.strikes = self.strikes.saturating_add(1);
if self.strikes >= self.strike_limit {
Verdict::Close
} else {
Verdict::Drop
}
}
}
}
}
impl Default for ControlGate {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn admits_a_burst_up_to_capacity() {
let mut gate = ControlGate::with_params(5, 1, 100);
for _ in 0..5 {
assert_eq!(gate.check(), Verdict::Allow);
}
assert_eq!(gate.check(), Verdict::Drop);
}
#[test]
fn sustained_flood_trips_close() {
let mut gate = ControlGate::with_params(3, 1, 10);
for _ in 0..3 {
assert_eq!(gate.check(), Verdict::Allow);
}
let mut verdicts = Vec::new();
for _ in 0..20 {
verdicts.push(gate.check());
}
assert!(
verdicts.contains(&Verdict::Close),
"expected a Close verdict under sustained flood"
);
}
#[test]
fn strikes_decay_so_a_chatty_peer_is_never_closed() {
let mut gate = ControlGate::with_params(2, 1, 5);
gate.strikes = 4; assert_eq!(gate.check(), Verdict::Allow);
assert_eq!(gate.check(), Verdict::Allow);
assert_eq!(gate.strikes, 2);
assert_eq!(gate.check(), Verdict::Drop);
}
}