use std::collections::HashMap;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex, PoisonError, RwLock};
use crate::config::Error;
use crate::limit::Limit;
pub(crate) const MAX_KEY_LIMITS: usize = 1 << 20;
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum KeyLimit {
Limit(Limit),
Class(String),
}
impl KeyLimit {
pub fn class(name: impl Into<String>) -> Self {
KeyLimit::Class(name.into())
}
}
impl From<Limit> for KeyLimit {
fn from(limit: Limit) -> Self {
KeyLimit::Limit(limit)
}
}
pub(crate) type Entry = (String, String, KeyLimit);
#[derive(Debug)]
pub(crate) struct KeyLimits {
max: usize,
count: AtomicUsize,
table: RwLock<Arc<HashMap<u64, Entry>>>,
writer: Mutex<()>,
}
impl KeyLimits {
pub fn new(max: usize) -> Self {
Self {
max,
count: AtomicUsize::new(0),
table: RwLock::default(),
writer: Mutex::new(()),
}
}
pub fn is_empty(&self) -> bool {
self.count.load(Ordering::Relaxed) == 0
}
pub fn snapshot(&self) -> Arc<HashMap<u64, Entry>> {
self.table
.read()
.unwrap_or_else(PoisonError::into_inner)
.clone()
}
pub fn with<T>(&self, read: impl FnOnce(&HashMap<u64, Entry>) -> T) -> T {
read(&self.table.read().unwrap_or_else(PoisonError::into_inner))
}
pub fn set(&self, hash: u64, entry: Entry) -> Result<(), Error> {
check(&entry.2)?;
self.change(|table| {
if !table.contains_key(&hash) && table.len() == self.max {
return Err(Error::TooManyKeyLimits(self.max));
}
table.insert(hash, entry);
Ok(())
})
}
pub fn remove(&self, hash: u64) -> bool {
let _writer = self.writer.lock().unwrap_or_else(PoisonError::into_inner);
let current = self.snapshot();
if !current.contains_key(&hash) {
return false;
}
let mut table = HashMap::clone(¤t);
table.remove(&hash);
self.swap(table);
true
}
pub fn replace(&self, entries: impl IntoIterator<Item = (u64, Entry)>) -> Result<(), Error> {
let mut named = HashMap::new();
for (hash, entry) in entries {
check(&entry.2)?;
named.insert(hash, entry);
if named.len() > self.max {
return Err(Error::TooManyKeyLimits(self.max));
}
}
let _writer = self.writer.lock().unwrap_or_else(PoisonError::into_inner);
self.swap(named);
Ok(())
}
pub fn list(&self) -> Vec<Entry> {
self.snapshot().values().cloned().collect()
}
fn change(
&self,
edit: impl FnOnce(&mut HashMap<u64, Entry>) -> Result<(), Error>,
) -> Result<(), Error> {
let _writer = self.writer.lock().unwrap_or_else(PoisonError::into_inner);
let mut table = HashMap::clone(&self.snapshot());
edit(&mut table)?;
self.swap(table);
Ok(())
}
fn swap(&self, table: HashMap<u64, Entry>) {
self.count.store(table.len(), Ordering::Relaxed);
let table = Arc::new(table);
let old = std::mem::replace(
&mut *self.table.write().unwrap_or_else(PoisonError::into_inner),
table,
);
drop(old);
}
}
fn check(limit: &KeyLimit) -> Result<(), Error> {
match limit {
KeyLimit::Limit(limit) => limit.to_core().map(|_| ()),
KeyLimit::Class(_) => Ok(()),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn entry(key: &str, limit: impl Into<KeyLimit>) -> Entry {
(String::new(), key.to_owned(), limit.into())
}
#[test]
fn the_count_follows_the_table() {
let limits = KeyLimits::new(2);
assert!(limits.is_empty());
limits.set(1, entry("a", Limit::per_second(5))).unwrap();
limits.set(2, entry("b", KeyLimit::class("pro"))).unwrap();
assert!(!limits.is_empty());
assert!(matches!(
limits.set(3, entry("c", Limit::per_second(5))),
Err(Error::TooManyKeyLimits(2))
));
limits.set(1, entry("a", Limit::per_second(6))).unwrap();
assert!(limits.remove(1));
assert!(!limits.remove(1));
assert!(limits.remove(2));
assert!(limits.is_empty());
}
#[test]
fn removing_an_absent_key_leaves_the_table_as_it_is() {
let limits = KeyLimits::new(2);
limits.set(1, entry("a", Limit::per_second(5))).unwrap();
let before = limits.snapshot();
assert!(!limits.remove(2));
assert!(Arc::ptr_eq(&before, &limits.snapshot()), "swapped");
assert!(limits.remove(1));
assert!(!Arc::ptr_eq(&before, &limits.snapshot()));
}
#[test]
fn concurrent_writers_lose_nothing() {
let limits = KeyLimits::new(1000);
std::thread::scope(|scope| {
for t in 0..8u64 {
let limits = &limits;
scope.spawn(move || {
for i in 0..100 {
let hash = t * 100 + i;
limits.set(hash, entry("k", Limit::per_second(5))).unwrap();
}
});
}
});
assert_eq!(limits.list().len(), 800);
assert!(!limits.is_empty());
}
#[test]
fn a_limit_out_of_range_is_refused_when_set() {
let limits = KeyLimits::new(2);
assert!(matches!(
limits.set(1, entry("a", Limit::per_second(0))),
Err(Error::ZeroLimit)
));
assert!(matches!(
limits.set(1, entry("a", Limit::per_second(5).burst(1 << 24))),
Err(Error::TooLargeBurst(_))
));
assert!(limits.is_empty());
}
#[test]
fn a_refused_replace_changes_nothing() {
let limits = KeyLimits::new(1);
limits.set(1, entry("a", Limit::per_second(5))).unwrap();
let refused = limits.replace([
(2, entry("b", Limit::per_second(5))),
(3, entry("c", Limit::per_second(5))),
]);
assert!(matches!(refused, Err(Error::TooManyKeyLimits(1))));
assert_eq!(limits.list(), [entry("a", Limit::per_second(5))]);
limits
.replace([
(2, entry("b", Limit::per_second(5))),
(2, entry("b", Limit::per_second(7))),
])
.unwrap();
assert_eq!(limits.list(), [entry("b", Limit::per_second(7))]);
}
}