use std::collections::HashMap;
use std::net::IpAddr;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::RwLock;
#[derive(Clone, Debug)]
struct DnsCacheEntry {
ip_addresses: Vec<IpAddr>,
timestamp: Instant,
}
pub struct DnsCache {
cache: Arc<RwLock<HashMap<String, DnsCacheEntry>>>,
ttl: Duration,
}
impl DnsCache {
pub fn new(ttl: Duration) -> Self {
Self {
cache: Arc::new(RwLock::new(HashMap::new())),
ttl,
}
}
pub fn with_default_ttl() -> Self {
Self::new(Duration::from_secs(600))
}
}
impl Default for DnsCache {
fn default() -> Self {
Self::with_default_ttl()
}
}
impl DnsCache {
pub async fn get(&self, hostname: &str) -> Option<Vec<IpAddr>> {
let cache = self.cache.read().await;
if let Some(entry) = cache.get(hostname) {
if entry.timestamp.elapsed() < self.ttl {
return Some(entry.ip_addresses.clone());
}
}
None
}
pub async fn insert(&self, hostname: String, ip_addresses: Vec<IpAddr>) {
let mut cache = self.cache.write().await;
cache.insert(
hostname,
DnsCacheEntry {
ip_addresses,
timestamp: Instant::now(),
},
);
}
pub async fn cleanup_expired(&self) -> usize {
let mut cache = self.cache.write().await;
let initial_size = cache.len();
cache.retain(|_, entry| entry.timestamp.elapsed() < self.ttl);
initial_size - cache.len()
}
pub async fn clear(&self) {
let mut cache = self.cache.write().await;
cache.clear();
}
pub async fn size(&self) -> usize {
let cache = self.cache.read().await;
cache.len()
}
pub async fn stats(&self) -> CacheStats {
let cache = self.cache.read().await;
let total = cache.len();
let expired = cache
.values()
.filter(|entry| entry.timestamp.elapsed() >= self.ttl)
.count();
CacheStats {
total_entries: total,
expired_entries: expired,
active_entries: total - expired,
}
}
}
#[derive(Debug, Clone)]
pub struct CacheStats {
pub total_entries: usize,
pub expired_entries: usize,
pub active_entries: usize,
}
use std::sync::OnceLock;
static DNS_CACHE: OnceLock<DnsCache> = OnceLock::new();
pub fn global_cache() -> &'static DnsCache {
DNS_CACHE.get_or_init(DnsCache::default)
}
#[cfg(test)]
mod tests {
use super::*;
use std::net::{Ipv4Addr, Ipv6Addr};
#[tokio::test]
async fn test_cache_insert_and_get() {
let cache = DnsCache::new(Duration::from_secs(60));
let hostname = "example.com".to_string();
let ips = vec![IpAddr::V4(Ipv4Addr::new(93, 184, 216, 34))];
cache.insert(hostname.clone(), ips.clone()).await;
let cached = cache.get(&hostname).await;
assert!(cached.is_some());
assert_eq!(cached.unwrap(), ips);
}
#[tokio::test]
async fn test_cache_expiration() {
let cache = DnsCache::new(Duration::from_millis(100));
let hostname = "example.com".to_string();
let ips = vec![IpAddr::V4(Ipv4Addr::new(93, 184, 216, 34))];
cache.insert(hostname.clone(), ips).await;
assert!(cache.get(&hostname).await.is_some());
tokio::time::sleep(Duration::from_millis(150)).await;
assert!(cache.get(&hostname).await.is_none());
}
#[tokio::test]
async fn test_cache_cleanup() {
let cache = DnsCache::new(Duration::from_millis(100));
cache
.insert(
"example1.com".to_string(),
vec![IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1))],
)
.await;
cache
.insert(
"example2.com".to_string(),
vec![IpAddr::V4(Ipv4Addr::new(2, 2, 2, 2))],
)
.await;
assert_eq!(cache.size().await, 2);
tokio::time::sleep(Duration::from_millis(150)).await;
let removed = cache.cleanup_expired().await;
assert_eq!(removed, 2);
assert_eq!(cache.size().await, 0);
}
#[tokio::test]
async fn test_cache_stats() {
let cache = DnsCache::new(Duration::from_secs(60));
cache
.insert(
"example.com".to_string(),
vec![IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1))],
)
.await;
let stats = cache.stats().await;
assert_eq!(stats.total_entries, 1);
assert_eq!(stats.active_entries, 1);
assert_eq!(stats.expired_entries, 0);
}
#[tokio::test]
async fn test_ipv6_support() {
let cache = DnsCache::default();
let hostname = "ipv6.example.com".to_string();
let ips = vec![IpAddr::V6(Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 1))];
cache.insert(hostname.clone(), ips.clone()).await;
let cached = cache.get(&hostname).await;
assert!(cached.is_some());
assert_eq!(cached.unwrap(), ips);
}
}