use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Mutex, PoisonError, RwLock};
use std::time::{Duration, Instant};
pub(super) struct WarnOnce(AtomicBool);
impl WarnOnce {
pub(super) const fn new() -> Self {
Self(AtomicBool::new(false))
}
pub(super) fn once(&self, emit: impl FnOnce()) {
if !self.0.swap(true, Ordering::Relaxed) {
emit();
}
}
}
pub(super) enum Cached<V> {
Absent,
Miss,
Hit(V),
}
pub(super) struct HostTtlCache<V> {
hits: RwLock<Option<HashMap<String, Entry<V>>>>,
misses: RwLock<Option<HashMap<String, Entry<()>>>>,
ttl: Duration,
cap: usize,
tick: AtomicU64,
flights: Mutex<Option<HashMap<String, Arc<tokio::sync::Mutex<()>>>>>,
}
struct Entry<V> {
value: V,
at: Instant,
used: AtomicU64,
}
pub(super) struct Flight<'a, V> {
cache: &'a HostTtlCache<V>,
host: String,
lock: Arc<tokio::sync::Mutex<()>>,
_guard: tokio::sync::OwnedMutexGuard<()>,
}
impl<V> Drop for Flight<'_, V> {
fn drop(&mut self) {
let Ok(mut g) = self.cache.flights.lock() else {
return;
};
if let Some(map) = g.as_mut() {
if map
.get(&self.host)
.is_some_and(|l| Arc::ptr_eq(l, &self.lock))
{
map.remove(&self.host);
}
}
}
}
impl<V: Clone> HostTtlCache<V> {
pub(super) const fn new(ttl: Duration, cap: usize) -> Self {
Self {
hits: RwLock::new(None),
misses: RwLock::new(None),
ttl,
cap,
tick: AtomicU64::new(0),
flights: Mutex::new(None),
}
}
pub(super) fn get(&self, host: &str) -> Cached<V> {
if let Some(v) = self.lookup(&self.hits, host) {
return Cached::Hit(v);
}
match self.lookup(&self.misses, host) {
Some(()) => Cached::Miss,
None => Cached::Absent,
}
}
fn lookup<T: Clone>(
&self,
map: &RwLock<Option<HashMap<String, Entry<T>>>>,
host: &str,
) -> Option<T> {
let guard = map.read().ok()?;
let e = guard.as_ref()?.get(host)?;
if e.at.elapsed() >= self.ttl {
return None;
}
e.used
.store(self.tick.fetch_add(1, Ordering::Relaxed), Ordering::Relaxed);
Some(e.value.clone())
}
pub(super) fn put(&self, host: &str, value: Option<V>) {
match value {
Some(v) => {
remove(&self.misses, host);
self.insert(&self.hits, self.cap, host, v);
}
None => {
remove(&self.hits, host);
self.insert(&self.misses, (self.cap / 4).max(1), host, ());
}
}
}
fn insert<T>(
&self,
map: &RwLock<Option<HashMap<String, Entry<T>>>>,
cap: usize,
host: &str,
value: T,
) {
let Ok(mut guard) = map.write() else {
return;
};
let map = guard.get_or_insert_with(HashMap::default);
if map.len() >= cap && !map.contains_key(host) {
map.retain(|_, e| e.at.elapsed() < self.ttl);
if map.len() >= cap {
let victim = map
.iter()
.min_by_key(|(_, e)| e.used.load(Ordering::Relaxed))
.map(|(k, _)| k.clone());
if let Some(victim) = victim {
map.remove(&victim);
}
}
}
let used = AtomicU64::new(self.tick.fetch_add(1, Ordering::Relaxed));
map.insert(
host.to_owned(),
Entry {
value,
at: Instant::now(),
used,
},
);
}
pub(super) async fn single_flight(&self, host: &str) -> Flight<'_, V> {
let lock = {
let mut g = self.flights.lock().unwrap_or_else(PoisonError::into_inner);
g.get_or_insert_with(HashMap::default)
.entry(host.to_owned())
.or_default()
.clone()
};
let guard = lock.clone().lock_owned().await;
Flight {
cache: self,
host: host.to_owned(),
lock,
_guard: guard,
}
}
pub(super) fn clear(&self) {
clear(&self.hits);
clear(&self.misses);
}
}
fn remove<T>(map: &RwLock<Option<HashMap<String, Entry<T>>>>, host: &str) {
if let Ok(mut g) = map.write() {
if let Some(m) = g.as_mut() {
m.remove(host);
}
}
}
fn clear<T>(map: &RwLock<Option<HashMap<String, Entry<T>>>>) {
if let Ok(mut g) = map.write() {
if let Some(m) = g.as_mut() {
m.clear();
}
}
}
pub(super) struct GenerationPoll<G> {
state: RwLock<Option<(Option<G>, Instant)>>,
warned: WarnOnce,
}
enum Claim<G> {
NotDue,
Taken(Option<G>),
}
impl<G: Copy + PartialEq> GenerationPoll<G> {
pub(super) const fn new() -> Self {
Self {
state: RwLock::new(None),
warned: WarnOnce::new(),
}
}
fn claim(&self, interval: Duration) -> Claim<G> {
match self.state.write() {
Ok(mut g) => {
let due = g.as_ref().is_none_or(|(_, at)| at.elapsed() >= interval);
if !due {
return Claim::NotDue;
}
let previous = g.as_ref().and_then(|(seen, _)| *seen);
*g = Some((previous, Instant::now()));
Claim::Taken(previous)
}
Err(_) => Claim::NotDue,
}
}
fn record(&self, previous: Option<G>, current: G) -> bool {
match self.state.write() {
Ok(mut g) => {
*g = Some((Some(current), Instant::now()));
previous.is_some_and(|seen| seen != current)
}
Err(_) => false,
}
}
pub(super) fn warn_once(&self, emit: impl FnOnce()) {
self.warned.once(emit);
}
#[cfg(any(test, feature = "testkit"))]
pub(super) fn reset(&self) {
if let Ok(mut g) = self.state.write() {
*g = None;
}
}
#[cfg(any(test, feature = "testkit"))]
pub(super) fn expire(&self, by: Duration, floor: Instant) {
if let Ok(mut g) = self.state.write() {
let previous = g.as_ref().and_then(|(seen, _)| *seen);
let long_ago = Instant::now().checked_sub(by).unwrap_or(floor);
*g = Some((previous, long_ago));
}
}
pub(super) async fn sync<F, Fut, E>(
&self,
interval: Option<Duration>,
read: F,
on_error: impl FnOnce(&E),
on_change: impl FnOnce(),
) where
F: FnOnce() -> Fut,
Fut: std::future::Future<Output = Result<Option<G>, E>>,
{
let Some(interval) = interval else {
return;
};
let Claim::Taken(previous) = self.claim(interval) else {
return;
};
let current = match read().await {
Ok(Some(current)) => current,
Ok(None) => return,
Err(e) => {
on_error(&e);
return;
}
};
if self.record(previous, current) {
on_change();
}
}
}
pub(super) struct Breaker {
at: RwLock<Option<Instant>>,
warned: WarnOnce,
}
impl Breaker {
pub(super) const fn new() -> Self {
Self {
at: RwLock::new(None),
warned: WarnOnce::new(),
}
}
pub(super) fn is_open(&self, window: Duration) -> bool {
match self.at.read() {
Ok(g) => g.is_some_and(|at| at.elapsed() < window),
Err(_) => false,
}
}
pub(super) fn open(&self) {
if let Ok(mut g) = self.at.write() {
*g = Some(Instant::now());
}
}
pub(super) fn close(&self) {
if self.at.read().is_ok_and(|g| g.is_some()) {
if let Ok(mut g) = self.at.write() {
*g = None;
}
}
}
pub(super) fn warn_once(&self, emit: impl FnOnce()) {
self.warned.once(emit);
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::AtomicUsize;
const TTL: Duration = Duration::from_secs(60);
#[test]
fn misses_never_evict_a_resolved_host() {
let cache = HostTtlCache::<u32>::new(TTL, 2);
cache.put("acme.example", Some(1));
for i in 0..64 {
cache.put(&format!("junk{i}.example"), None);
}
assert!(matches!(cache.get("acme.example"), Cached::Hit(1)));
assert!(matches!(cache.get("junk63.example"), Cached::Miss));
}
#[test]
fn a_full_cache_evicts_the_least_recently_used() {
for _ in 0..16 {
let cache = HostTtlCache::<u32>::new(TTL, 2);
cache.put("a", Some(1));
cache.put("b", Some(2));
let _ = cache.get("a");
cache.put("c", Some(3));
assert!(matches!(cache.get("a"), Cached::Hit(1)), "a was just read");
assert!(matches!(cache.get("b"), Cached::Absent), "b was idle");
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn concurrent_cold_lookups_query_once() {
let cache = Arc::new(HostTtlCache::<u32>::new(TTL, 8));
let queries = Arc::new(AtomicUsize::new(0));
let tasks: Vec<_> = (0..8)
.map(|_| {
let (cache, queries) = (cache.clone(), queries.clone());
tokio::spawn(async move {
let _flight = cache.single_flight("acme.example").await;
if matches!(cache.get("acme.example"), Cached::Absent) {
queries.fetch_add(1, Ordering::SeqCst);
tokio::time::sleep(Duration::from_millis(20)).await;
cache.put("acme.example", Some(1));
}
})
})
.collect();
for t in tasks {
t.await.unwrap();
}
assert_eq!(queries.load(Ordering::SeqCst), 1);
}
#[tokio::test]
async fn a_cancelled_waiter_leaves_no_flight_entry() {
let cache = HostTtlCache::<u32>::new(TTL, 8);
let holder = cache.single_flight("h.example").await;
let mut waiter = Box::pin(cache.single_flight("h.example"));
let pending = tokio::time::timeout(Duration::from_millis(5), &mut waiter).await;
assert!(pending.is_err(), "the waiter must queue behind the holder");
drop(holder);
drop(waiter);
let left = cache
.flights
.lock()
.unwrap()
.as_ref()
.map_or(0, HashMap::len);
assert_eq!(left, 0);
}
}