use std::collections::HashMap;
use std::net::IpAddr;
use std::sync::{Mutex, OnceLock};
use std::time::{Duration, Instant};
const ANSWER_TTL: Duration = Duration::from_secs(30);
const MAX_ENTRIES: usize = 512;
struct Entry {
ips: Vec<IpAddr>,
expires_at: Instant,
inserted_at: Instant,
}
fn cache() -> &'static Mutex<HashMap<String, Entry>> {
static CACHE: OnceLock<Mutex<HashMap<String, Entry>>> = OnceLock::new();
CACHE.get_or_init(|| Mutex::new(HashMap::new()))
}
#[allow(clippy::redundant_pub_crate)] pub(crate) fn get(domain: &str) -> Option<Vec<IpAddr>> {
let key = domain.to_ascii_lowercase();
let map = cache().lock().unwrap_or_else(std::sync::PoisonError::into_inner);
map.get(&key).and_then(|entry| {
if entry.expires_at > Instant::now() {
Some(entry.ips.clone())
} else {
None
}
})
}
#[allow(clippy::redundant_pub_crate)] pub(crate) fn put(domain: &str, ips: Vec<IpAddr>) {
let key = domain.to_ascii_lowercase();
let mut map = cache().lock().unwrap_or_else(std::sync::PoisonError::into_inner);
if map.len() >= MAX_ENTRIES && !map.contains_key(&key) {
if let Some(oldest_key) = map
.iter()
.min_by_key(|(_, entry)| entry.inserted_at)
.map(|(k, _)| k.clone())
{
map.remove(&oldest_key);
}
}
let now = Instant::now();
map.insert(
key,
Entry {
ips,
expires_at: now + ANSWER_TTL,
inserted_at: now,
},
);
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use std::net::Ipv4Addr;
#[test]
fn test_cache_roundtrip_and_case_insensitivity() {
let domain = format!("cache-test-{:?}.example.", std::thread::current().id());
let ips = vec![IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4))];
put(&domain, ips.clone());
assert_eq!(get(&domain.to_uppercase()), Some(ips));
}
#[test]
fn test_cache_miss_for_unknown_domain() {
assert_eq!(get("definitely-not-cached.invalid."), None);
}
#[test]
fn test_cache_eviction_at_capacity() {
let prefix = format!("evict-test-{:?}-", std::thread::current().id());
for i in 0..MAX_ENTRIES + 8 {
put(
&format!("{prefix}{i}.example."),
vec![IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1))],
);
}
let last = format!("{prefix}{}.example.", MAX_ENTRIES + 7);
assert!(get(&last).is_some());
assert!(cache().lock().unwrap().len() <= MAX_ENTRIES);
}
}