use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use sha2::{Digest, Sha256};
use tokio::sync::OwnedMutexGuard;
use crate::hash::{hex, random_bytes};
pub struct RateLimit {
per_minute: f64,
key: Vec<u8>,
state: Mutex<(i64, HashMap<String, u64>)>,
}
impl RateLimit {
pub fn new(per_minute: f64) -> RateLimit {
RateLimit { per_minute, key: random_bytes(16), state: Mutex::new((0, HashMap::new())) }
}
pub fn allow(&self, ip: &str, now: i64) -> bool {
if ip.is_empty() {
return true;
}
let window = now.div_euclid(60_000);
let id = self.hash(ip);
let mut state = self.state.lock().unwrap_or_else(|e| e.into_inner());
if window != state.0 {
state.0 = window;
state.1.clear();
}
let count = state.1.entry(id).or_insert(0);
*count += 1;
(*count as f64) <= self.per_minute
}
fn hash(&self, ip: &str) -> String {
let mut h = Sha256::new();
h.update(&self.key);
h.update(ip.as_bytes());
hex(&h.finalize()[..8])
}
}
#[derive(Default)]
pub(crate) struct Turns(Mutex<HashMap<String, Arc<tokio::sync::Mutex<()>>>>);
pub(crate) struct Turn<'a> {
turns: &'a Turns,
key: String,
lock: Arc<tokio::sync::Mutex<()>>,
held: Option<OwnedMutexGuard<()>>,
}
impl Turns {
pub(crate) async fn take(&self, key: &str) -> Turn<'_> {
let lock = self.0.lock().unwrap_or_else(|e| e.into_inner()).entry(key.to_string()).or_default().clone();
let mut turn = Turn { turns: self, key: key.to_string(), lock, held: None };
turn.held = Some(turn.lock.clone().lock_owned().await);
turn
}
#[cfg(test)]
pub(crate) fn len(&self) -> usize {
self.0.lock().unwrap_or_else(|e| e.into_inner()).len()
}
}
impl Drop for Turn<'_> {
fn drop(&mut self) {
self.held = None;
let mut map = self.turns.0.lock().unwrap_or_else(|e| e.into_inner());
if map.get(&self.key).is_some_and(|l| Arc::ptr_eq(l, &self.lock) && Arc::strong_count(l) == 2) {
map.remove(&self.key);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn a_key_is_forgotten_however_its_turn_ends() {
let turns = Turns::default();
drop(turns.take("a").await);
assert_eq!(turns.len(), 0);
let held = turns.take("a").await;
let waiting = tokio::time::timeout(std::time::Duration::from_millis(50), turns.take("a")).await;
assert!(waiting.is_err());
assert_eq!(turns.len(), 1);
let holding = tokio::time::timeout(std::time::Duration::from_millis(50), async {
drop(held);
let _turn = turns.take("a").await;
std::future::pending::<()>().await;
})
.await;
assert!(holding.is_err());
assert_eq!(turns.len(), 0);
}
}