use super::{LOG_TARGET, now_ms};
use ahash::RandomState;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::time::Duration;
use tinyufo::TinyUfo;
use tracing::debug;
#[derive(Debug, Default)]
struct TtlLimit {
count: AtomicUsize,
created_at: AtomicU64,
}
pub struct TtlLruLimit {
ttl: u64,
ufo: TinyUfo<u64, Arc<TtlLimit>>,
max: usize,
hasher: RandomState,
}
impl TtlLruLimit {
pub fn new(size: usize, ttl: Duration, max: usize) -> Self {
Self {
ttl: ttl.as_millis() as u64,
max,
ufo: TinyUfo::new(size, size),
hasher: RandomState::new(),
}
}
pub fn new_compact(size: usize, ttl: Duration, max: usize) -> Self {
Self {
ttl: ttl.as_millis() as u64,
max,
ufo: TinyUfo::new_compact(size, size),
hasher: RandomState::new(),
}
}
#[inline]
fn hash_key(&self, key: &str) -> u64 {
self.hasher.hash_one(key)
}
pub fn validate(&self, key: &str) -> bool {
let key = self.hash_key(key);
let Some(value) = self.ufo.get(&key) else {
return true;
};
debug!(
target: LOG_TARGET,
key,
?value,
"ttl lru limit"
);
let created_at = value.created_at.load(Ordering::Relaxed);
if created_at == 0 {
return true;
}
if now_ms().saturating_sub(created_at) > self.ttl {
value.created_at.store(0, Ordering::Relaxed);
value.count.store(0, Ordering::Relaxed);
return true;
}
value.count.load(Ordering::Relaxed) < self.max
}
pub fn inc(&self, key: &str) {
let key = self.hash_key(key);
if let Some(value) = self.ufo.get(&key) {
let _ = value.created_at.compare_exchange(
0,
now_ms(),
Ordering::Relaxed,
Ordering::Relaxed,
);
value.count.fetch_add(1, Ordering::Relaxed);
return;
}
self.ufo.put(
key,
Arc::new(TtlLimit {
count: AtomicUsize::new(1),
created_at: AtomicU64::new(now_ms()),
}),
1,
);
}
}
#[cfg(test)]
mod test {
use super::TtlLruLimit;
use pretty_assertions::assert_eq;
use std::sync::Arc;
use std::time::Duration;
#[test]
fn test_ttl_lru_limit() {
for limit in [
TtlLruLimit::new(5, Duration::from_millis(500), 3),
TtlLruLimit::new_compact(5, Duration::from_millis(500), 3),
] {
let key = "abc";
assert_eq!(true, limit.validate(key));
limit.inc(key);
limit.inc(key);
assert_eq!(true, limit.validate(key));
limit.inc(key);
assert_eq!(false, limit.validate(key));
std::thread::sleep(Duration::from_millis(600));
assert_eq!(true, limit.validate(key));
limit.inc(key);
limit.inc(key);
limit.inc(key);
assert_eq!(false, limit.validate(key));
}
}
#[test]
fn test_concurrent_inc_loses_nothing() {
let limit = Arc::new(TtlLruLimit::new(5, Duration::from_secs(10), 800));
limit.inc("abc");
let threads: Vec<_> = (0..8)
.map(|_| {
let limit = limit.clone();
std::thread::spawn(move || {
for _ in 0..100 {
limit.inc("abc");
}
})
})
.collect();
for thread in threads {
thread.join().expect("thread");
}
assert_eq!(false, limit.validate("abc"));
}
}