use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
use std::sync::{Arc, Mutex, PoisonError, RwLock};
use rateguard_core::gcra::Quota;
use rateguard_core::limiter::Limit as CoreLimit;
use crate::config::Error;
use crate::key::class_hash;
use crate::key_limits::{Entry, KeyLimit};
use crate::limit::Limit;
pub(crate) const MAX_CLASSES: usize = 4096;
pub(crate) fn checked<S: Into<String>>(
classes: impl IntoIterator<Item = (S, Limit)>,
max: usize,
) -> Result<Vec<(String, Limit, CoreLimit)>, Error> {
let mut named: HashMap<u64, (String, Limit, CoreLimit)> = HashMap::new();
for (name, limit) in classes {
let name = name.into();
if name.is_empty() {
return Err(Error::EmptyClassName);
}
let core = limit.to_core()?;
named.insert(class_hash(&name), (name, limit, core));
if named.len() > max {
return Err(Error::TooManyClasses(max));
}
}
Ok(named.into_values().collect())
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) struct Resolved {
pub limit: CoreLimit,
pub known: bool,
pub shadow: Option<CoreLimit>,
}
pub(crate) fn shadow_quota(
main: Quota,
limit: CoreLimit,
shadow: Option<CoreLimit>,
) -> Option<Quota> {
shadow.map(|shadow| {
Quota::per_second(main.rate_per_sec() * shadow.rate / limit.rate, shadow.burst)
})
}
#[derive(Debug, Clone)]
pub(crate) struct Table {
pub default: CoreLimit,
default_given: Limit,
named: HashMap<u64, (String, Limit, CoreLimit)>,
shadows: HashMap<u64, (Limit, CoreLimit)>,
}
impl Table {
pub fn lookup(&self, class: u64) -> (CoreLimit, bool) {
if class == 0 {
return (self.default, true);
}
match self.named.get(&class) {
Some(&(_, _, limit)) => (limit, true),
None => (self.default, false),
}
}
pub fn resolve(&self, overrides: &HashMap<u64, Entry>, class: u64, key: u64) -> Resolved {
let class = match overrides.get(&key) {
Some((_, _, KeyLimit::Limit(limit))) => match limit.to_core() {
Ok(limit) => {
return Resolved {
limit,
known: true,
shadow: None,
};
}
Err(_) => class,
},
Some((_, _, KeyLimit::Class(name))) => class_hash(name),
None => class,
};
let (limit, known) = self.lookup(class);
let shadow_of = if known { class } else { 0 };
Resolved {
limit,
known,
shadow: self.shadows.get(&shadow_of).map(|&(_, core)| core),
}
}
pub fn resolve_given(
&self,
overrides: &HashMap<u64, Entry>,
class: u64,
key: u64,
) -> (Limit, Option<Limit>) {
let class = match overrides.get(&key) {
Some((_, _, KeyLimit::Limit(limit))) if limit.to_core().is_ok() => {
return (*limit, None);
}
Some((_, _, KeyLimit::Class(name))) => class_hash(name),
_ => class,
};
let (limit, shadow_of) = match self.named.get(&class) {
Some(&(_, given, _)) => (given, class),
None => (self.default_given, 0),
};
(limit, self.shadows.get(&shadow_of).map(|&(given, _)| given))
}
pub fn has_shadows(&self) -> bool {
!self.shadows.is_empty()
}
fn class_named(&self, name: &str) -> Result<u64, Error> {
if name.is_empty() {
return Ok(0);
}
let class = class_hash(name);
if self.named.contains_key(&class) {
Ok(class)
} else {
Err(Error::UnknownClass(name.to_owned()))
}
}
pub fn strictest(&self) -> CoreLimit {
self.named
.values()
.fold(self.default, |strictest, &(_, _, limit)| CoreLimit {
rate: strictest.rate.min(limit.rate),
burst: strictest.burst.min(limit.burst),
})
}
}
#[derive(Debug)]
pub(crate) struct Classes {
max: usize,
default_rate: AtomicU64,
default_burst: AtomicU32,
has_shadows: AtomicBool,
table: RwLock<Arc<Table>>,
writer: Mutex<()>,
}
impl Classes {
pub fn new(
given: Limit,
default: CoreLimit,
named: Vec<(String, Limit, CoreLimit)>,
max: usize,
) -> Self {
let named = named
.into_iter()
.map(|(name, given, limit)| (class_hash(&name), (name, given, limit)))
.collect();
Self {
max,
default_rate: AtomicU64::new(default.rate.to_bits()),
default_burst: AtomicU32::new(default.burst),
has_shadows: AtomicBool::new(false),
table: RwLock::new(Arc::new(Table {
default,
default_given: given,
named,
shadows: HashMap::new(),
})),
writer: Mutex::new(()),
}
}
pub fn snapshot(&self) -> Arc<Table> {
self.table
.read()
.unwrap_or_else(PoisonError::into_inner)
.clone()
}
pub fn limit_of(&self, class: u64) -> CoreLimit {
if class == 0 {
return CoreLimit {
rate: f64::from_bits(self.default_rate.load(Ordering::Relaxed)),
burst: self.default_burst.load(Ordering::Relaxed),
};
}
self.table
.read()
.unwrap_or_else(PoisonError::into_inner)
.lookup(class)
.0
}
pub fn limit_for(
&self,
overrides: &HashMap<u64, Entry>,
class: u64,
key: u64,
) -> (CoreLimit, Option<CoreLimit>) {
if self.has_shadows() || overrides.contains_key(&key) {
let resolved = self.snapshot().resolve(overrides, class, key);
(resolved.limit, resolved.shadow)
} else {
(self.limit_of(class), None)
}
}
pub fn has_shadows(&self) -> bool {
self.has_shadows.load(Ordering::Relaxed)
}
pub fn set(&self, name: &str, limit: Limit) -> Result<(), Error> {
if name.is_empty() {
return Err(Error::EmptyClassName);
}
let core = limit.to_core()?;
self.change(|table| {
let hash = class_hash(name);
if !table.named.contains_key(&hash) && table.named.len() == self.max {
return Err(Error::TooManyClasses(self.max));
}
table.named.insert(hash, (name.to_owned(), limit, core));
Ok(())
})
}
pub fn remove(&self, name: &str) -> bool {
let mut removed = false;
let _ = self.change(|table| {
let hash = class_hash(name);
removed = table.named.remove(&hash).is_some();
if removed {
table.shadows.remove(&hash);
}
Ok(())
});
removed
}
pub fn set_default(&self, limit: Limit) -> Result<(), Error> {
let given = limit;
let limit = limit.to_core()?;
self.change(|table| {
table.default = limit;
table.default_given = given;
Ok(())
})
}
pub fn replace<S: Into<String>>(
&self,
classes: impl IntoIterator<Item = (S, Limit)>,
) -> Result<(), Error> {
let named: HashMap<_, _> = checked(classes, self.max)?
.into_iter()
.map(|(name, given, limit)| (class_hash(&name), (name, given, limit)))
.collect();
self.change(|table| {
table
.shadows
.retain(|class, _| *class == 0 || named.contains_key(class));
table.named = named;
Ok(())
})
}
pub fn set_shadow(&self, name: &str, limit: Option<Limit>) -> Result<(), Error> {
let limit = limit
.map(|limit| Ok((limit, limit.to_core()?)))
.transpose()?;
self.change(|table| {
let class = table.class_named(name)?;
match limit {
Some(limit) => table.shadows.insert(class, limit),
None => table.shadows.remove(&class),
};
Ok(())
})
}
pub fn replace_shadows<S: Into<String>>(
&self,
shadows: impl IntoIterator<Item = (S, Limit)>,
) -> Result<(), Error> {
let shadows: Vec<(String, Limit, CoreLimit)> = shadows
.into_iter()
.map(|(name, limit)| Ok((name.into(), limit, limit.to_core()?)))
.collect::<Result<_, Error>>()?;
self.change(|table| {
let mut all = HashMap::new();
for (name, limit, core) in shadows {
all.insert(table.class_named(&name)?, (limit, core));
}
table.shadows = all;
Ok(())
})
}
pub fn shadows(&self) -> Vec<(String, Limit)> {
let table = self.snapshot();
table
.shadows
.iter()
.map(|(&class, &(limit, _))| {
let name = match table.named.get(&class) {
Some((name, _, _)) => name.clone(),
None => String::new(),
};
(name, limit)
})
.collect()
}
pub fn list(&self) -> Vec<(String, Limit)> {
self.snapshot()
.named
.values()
.map(|(name, limit, _)| (name.clone(), *limit))
.collect()
}
fn change(&self, edit: impl FnOnce(&mut Table) -> Result<(), Error>) -> Result<(), Error> {
let _writer = self.writer.lock().unwrap_or_else(PoisonError::into_inner);
let mut table = Table::clone(&self.snapshot());
edit(&mut table)?;
let new = Arc::new(table);
let old = {
let mut current = self.table.write().unwrap_or_else(PoisonError::into_inner);
self.default_rate
.store(new.default.rate.to_bits(), Ordering::Relaxed);
self.default_burst
.store(new.default.burst, Ordering::Relaxed);
self.has_shadows.store(new.has_shadows(), Ordering::Relaxed);
std::mem::replace(&mut *current, new)
};
drop(old);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_class_change_does_not_hold_readers_for_the_copy() {
let named = (0..4096)
.map(|i| {
let limit = Limit::per_second(10);
(format!("class-{i}"), limit, limit.to_core().unwrap())
})
.collect();
let default = Limit::per_second(100);
let classes = Classes::new(default, default.to_core().unwrap(), named, 8192);
let mut ran = false;
classes
.change(|table| {
ran = true;
assert!(classes.table.try_read().is_ok(), "readers are locked out");
assert!(classes.table.try_write().is_ok(), "the lock is held");
assert_eq!(table.named.len(), 4096);
Ok(())
})
.unwrap();
assert!(ran);
classes
.change(|_| {
assert!(classes.writer.try_lock().is_err());
let _ = classes.limit_of(class_hash("class-7"));
Ok(())
})
.unwrap();
}
}