use hickory_resolver::config::*;
use hickory_resolver::net::runtime::TokioRuntimeProvider;
use hickory_resolver::{Resolver, TokioResolver};
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 {
ips: Vec<IpAddr>,
expires_at: Instant,
}
pub struct DnsCache {
resolver: TokioResolver,
cache: Arc<RwLock<HashMap<String, DnsCacheEntry>>>,
default_ttl: Duration,
}
impl DnsCache {
pub async fn new() -> Result<Self, Box<dyn std::error::Error>> {
let resolver = Self::build_resolver()?;
Ok(Self {
resolver,
cache: Arc::new(RwLock::new(HashMap::new())),
default_ttl: Duration::from_secs(300), })
}
pub async fn with_ttl(ttl: Duration) -> Result<Self, Box<dyn std::error::Error>> {
let resolver = Self::build_resolver()?;
Ok(Self {
resolver,
cache: Arc::new(RwLock::new(HashMap::new())),
default_ttl: ttl,
})
}
fn build_resolver() -> Result<TokioResolver, Box<dyn std::error::Error>> {
Ok(Resolver::builder_with_config(
ResolverConfig::default(),
TokioRuntimeProvider::default(),
)
.with_options(ResolverOpts::default())
.build()?)
}
pub async fn resolve(&self, hostname: &str) -> Result<Vec<IpAddr>, Box<dyn std::error::Error>> {
{
let cache = self.cache.read().await;
if let Some(entry) = cache.get(hostname) {
if entry.expires_at > Instant::now() {
return Ok(entry.ips.clone());
}
}
}
let lookup = self.resolver.lookup_ip(hostname).await?;
let ips: Vec<IpAddr> = lookup.iter().collect();
let entry = DnsCacheEntry {
ips: ips.clone(),
expires_at: Instant::now() + self.default_ttl,
};
let mut cache = self.cache.write().await;
cache.insert(hostname.to_string(), entry);
Ok(ips)
}
pub async fn prewarm(&self, hostname: &str) -> Result<(), Box<dyn std::error::Error>> {
self.resolve(hostname).await?;
Ok(())
}
pub async fn clear(&self) {
let mut cache = self.cache.write().await;
cache.clear();
}
pub async fn cache_size(&self) -> usize {
let cache = self.cache.read().await;
cache.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
#[ignore = "requires external DNS/network access"]
async fn test_dns_cache_resolve() {
let cache = DnsCache::new().await.unwrap();
let ips = cache.resolve("clob.polymarket.com").await.unwrap();
assert!(!ips.is_empty());
}
#[tokio::test]
#[ignore = "requires external DNS/network access"]
async fn test_dns_cache_prewarm() {
let cache = DnsCache::new().await.unwrap();
cache.prewarm("clob.polymarket.com").await.unwrap();
assert_eq!(cache.cache_size().await, 1);
}
#[tokio::test]
#[ignore = "requires external DNS/network access"]
async fn test_dns_cache_clear() {
let cache = DnsCache::new().await.unwrap();
cache.prewarm("clob.polymarket.com").await.unwrap();
cache.clear().await;
assert_eq!(cache.cache_size().await, 0);
}
}