use std::collections::HashMap;
use std::sync::Mutex;
use std::time::{Duration, Instant};
const WINDOW: Duration = Duration::from_secs(60);
type BindingKey = (String, String, String);
#[derive(Default)]
pub struct BindingRateLimiter {
windows: Mutex<HashMap<BindingKey, (Instant, u32)>>,
}
impl BindingRateLimiter {
pub fn new() -> Self {
Self::default()
}
pub fn try_acquire(
&self,
context: &str,
account: &str,
consumer: &str,
per_minute: u32,
) -> bool {
self.try_acquire_at(context, account, consumer, per_minute, Instant::now())
}
fn try_acquire_at(
&self,
context: &str,
account: &str,
consumer: &str,
per_minute: u32,
now: Instant,
) -> bool {
let mut windows = self.windows.lock().unwrap_or_else(|p| p.into_inner());
windows.retain(|_, (start, _)| now.duration_since(*start) < WINDOW);
let entry = windows
.entry((
context.to_string(),
account.to_string(),
consumer.to_string(),
))
.or_insert((now, 0));
if entry.1 >= per_minute {
return false;
}
entry.1 += 1;
true
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_binding_gets_its_rate_and_no_more_until_the_window_closes() {
let l = BindingRateLimiter::new();
let t0 = Instant::now();
assert!(l.try_acquire_at("c", "a", "did:x", 2, t0));
assert!(l.try_acquire_at("c", "a", "did:x", 2, t0));
assert!(!l.try_acquire_at("c", "a", "did:x", 2, t0));
assert!(l.try_acquire_at("c", "a", "did:y", 2, t0));
assert!(l.try_acquire_at("c", "a", "did:x", 2, t0 + WINDOW));
}
}