use std::collections::HashMap;
use std::net::SocketAddr;
use std::sync::RwLock;
use std::time::{Duration, Instant};
pub(crate) trait DNSCache {
fn get(&self, hostname: &str) -> CacheResult;
fn update(&self, hostname: &str, addrs: Vec<SocketAddr>) -> ();
}
#[derive(PartialEq)]
pub(crate) enum MissReason {
TTLExpired,
NotFound,
}
pub(crate) type Stale = bool;
pub(crate) enum CacheResult {
Hit(Stale, Vec<SocketAddr>),
Miss(MissReason),
}
type InMemoryDNSCacheValue = Option<(Instant, Vec<SocketAddr>)>;
pub(crate) struct InMemoryDNSCache {
ttl: Duration,
max_stale: Duration,
cache: RwLock<HashMap<String, RwLock<InMemoryDNSCacheValue>>>,
}
unsafe impl Send for InMemoryDNSCache {}
unsafe impl Sync for InMemoryDNSCache {}
impl DNSCache for InMemoryDNSCache {
fn get(&self, hostname: &str) -> CacheResult {
if let Some(value) = self.cache.read().unwrap().get(hostname) {
let value = value.read().unwrap();
if let Some((inserted_time, addrs)) = value.as_ref() {
let now = Instant::now();
if now < *inserted_time + self.ttl {
return CacheResult::Hit(false, addrs.clone());
} else if now < *inserted_time + self.ttl + self.max_stale {
return CacheResult::Hit(true, addrs.clone());
}
return CacheResult::Miss(MissReason::TTLExpired);
}
}
self.create_key(hostname);
CacheResult::Miss(MissReason::NotFound)
}
fn update(&self, hostname: &str, addrs: Vec<SocketAddr>) -> () {
let cache = self.cache.read().unwrap();
let value = cache.get(hostname).unwrap();
let mut guard = value.write().unwrap();
*guard = Some((Instant::now(), addrs));
}
}
impl InMemoryDNSCache {
pub(crate) fn new(ttl: Duration, max_stale: Duration) -> Self {
InMemoryDNSCache {
ttl: ttl,
max_stale: max_stale,
cache: HashMap::new().into(),
}
}
fn create_key(&self, hostname: &str) -> () {
let mut cache = self.cache.write().unwrap();
cache
.entry(hostname.to_string())
.or_insert(RwLock::new(None));
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicU32, Ordering};
use std::thread::sleep;
use std::vec;
#[test]
fn dns_cache_miss_and_hit() {
let cache = InMemoryDNSCache::new(Duration::from_secs(10), Duration::from_secs(10));
let result = cache.get("foo");
assert!(
matches!(result, CacheResult::Miss(MissReason::NotFound)),
"Not a miss"
);
cache.update("foo", vec!["192.168.0.1:80".parse().unwrap()]);
let result = cache.get("foo");
match result {
CacheResult::Hit(false, addrs) => {
assert_eq!(addrs, vec!["192.168.0.1:80".parse().unwrap()])
}
_ => {
panic!(
"Test failed: Expected CacheResult::Hit(false, addrs), but got something else."
);
}
}
}
#[test]
fn dns_cache_ttl() {
let cache = InMemoryDNSCache::new(Duration::from_millis(1), Duration::from_millis(1));
cache.get("foo");
cache.update("foo", vec!["192.168.0.1:80".parse().unwrap()]);
sleep(Duration::from_millis(100));
let result = cache.get("foo");
assert!(
matches!(result, CacheResult::Miss(MissReason::TTLExpired)),
"Not TTLExpired"
);
}
}