QuickDID is a high-performance AT Protocol identity resolution service written in Rust. It provides handle-to-DID resolution with Redis-backed caching and queue processing.
at main 8.8 kB view raw
1//! In-memory caching handle resolver. 2//! 3//! This module provides a handle resolver that caches resolution results in memory 4//! with a configurable TTL. This is useful for reducing DNS/HTTP lookups and 5//! improving performance when Redis is not available. 6 7use super::errors::HandleResolverError; 8use super::traits::HandleResolver; 9use crate::metrics::SharedMetricsPublisher; 10use async_trait::async_trait; 11use std::collections::HashMap; 12use std::sync::Arc; 13use std::time::{SystemTime, UNIX_EPOCH}; 14use tokio::sync::RwLock; 15 16/// Result of a handle resolution cached in memory. 17#[derive(Clone, Debug)] 18enum ResolveHandleResult { 19 /// Handle was successfully resolved to a DID 20 Found(u64, String), 21 /// Handle resolution failed 22 NotFound(u64, String), 23} 24 25/// In-memory caching wrapper for handle resolvers. 26/// 27/// This resolver wraps another resolver and caches results in memory with 28/// a configurable TTL. Both successful and failed resolutions are cached 29/// to avoid repeated lookups. 30/// 31/// # Example 32/// 33/// ```no_run 34/// use std::sync::Arc; 35/// use quickdid::handle_resolver::{create_caching_resolver, create_base_resolver, HandleResolver}; 36/// use quickdid::metrics::NoOpMetricsPublisher; 37/// 38/// # async fn example() { 39/// # use atproto_identity::resolve::HickoryDnsResolver; 40/// # use reqwest::Client; 41/// # let dns_resolver = Arc::new(HickoryDnsResolver::create_resolver(&[])); 42/// # let http_client = Client::new(); 43/// # let metrics = Arc::new(NoOpMetricsPublisher); 44/// let base_resolver = create_base_resolver(dns_resolver, http_client, metrics.clone()); 45/// let caching_resolver = create_caching_resolver( 46/// base_resolver, 47/// 300, // 5 minute TTL 48/// metrics 49/// ); 50/// 51/// // First call hits the underlying resolver 52/// let (did1, timestamp1) = caching_resolver.resolve("alice.bsky.social").await.unwrap(); 53/// 54/// // Second call returns cached result 55/// let (did2, timestamp2) = caching_resolver.resolve("alice.bsky.social").await.unwrap(); 56/// # } 57/// ``` 58pub(super) struct CachingHandleResolver { 59 inner: Arc<dyn HandleResolver>, 60 cache: Arc<RwLock<HashMap<String, ResolveHandleResult>>>, 61 ttl_seconds: u64, 62 metrics: SharedMetricsPublisher, 63} 64 65impl CachingHandleResolver { 66 /// Create a new caching handle resolver. 67 /// 68 /// # Arguments 69 /// 70 /// * `inner` - The underlying resolver to use for actual resolution 71 /// * `ttl_seconds` - How long to cache results in seconds 72 /// * `metrics` - Metrics publisher for telemetry 73 pub fn new( 74 inner: Arc<dyn HandleResolver>, 75 ttl_seconds: u64, 76 metrics: SharedMetricsPublisher, 77 ) -> Self { 78 Self { 79 inner, 80 cache: Arc::new(RwLock::new(HashMap::new())), 81 ttl_seconds, 82 metrics, 83 } 84 } 85 86 fn current_timestamp() -> u64 { 87 SystemTime::now() 88 .duration_since(UNIX_EPOCH) 89 .unwrap_or_default() 90 .as_secs() 91 } 92 93 fn is_expired(&self, timestamp: u64) -> bool { 94 let current = Self::current_timestamp(); 95 current > timestamp && (current - timestamp) > self.ttl_seconds 96 } 97} 98 99#[async_trait] 100impl HandleResolver for CachingHandleResolver { 101 async fn resolve(&self, s: &str) -> Result<(String, u64), HandleResolverError> { 102 let handle = s.to_string(); 103 104 // Check cache first 105 { 106 let cache = self.cache.read().await; 107 if let Some(cached) = cache.get(&handle) { 108 match cached { 109 ResolveHandleResult::Found(timestamp, did) => { 110 if !self.is_expired(*timestamp) { 111 tracing::debug!("Cache hit for handle {}: {}", handle, did); 112 self.metrics.incr("resolver.memory.cache_hit").await; 113 return Ok((did.clone(), *timestamp)); 114 } 115 tracing::debug!("Cache entry expired for handle {}", handle); 116 self.metrics.incr("resolver.memory.cache_expired").await; 117 } 118 ResolveHandleResult::NotFound(timestamp, error) => { 119 if !self.is_expired(*timestamp) { 120 tracing::debug!( 121 "Cache hit (not found) for handle {}: {}", 122 handle, 123 error 124 ); 125 self.metrics 126 .incr("resolver.memory.cache_hit_not_resolved") 127 .await; 128 return Err(HandleResolverError::HandleNotFoundCached(error.clone())); 129 } 130 tracing::debug!("Cache entry expired for handle {}", handle); 131 self.metrics.incr("resolver.memory.cache_expired").await; 132 } 133 } 134 } 135 } 136 137 // Not in cache or expired, resolve through inner resolver 138 tracing::debug!("Cache miss for handle {}, resolving...", handle); 139 self.metrics.incr("resolver.memory.cache_miss").await; 140 let result = self.inner.resolve(s).await; 141 142 // Store in cache 143 { 144 let mut cache = self.cache.write().await; 145 match &result { 146 Ok((did, timestamp)) => { 147 cache.insert( 148 handle.clone(), 149 ResolveHandleResult::Found(*timestamp, did.clone()), 150 ); 151 self.metrics.incr("resolver.memory.cache_set").await; 152 tracing::debug!( 153 "Cached successful resolution for handle {}: {}", 154 handle, 155 did 156 ); 157 } 158 Err(e) => { 159 let timestamp = Self::current_timestamp(); 160 cache.insert( 161 handle.clone(), 162 ResolveHandleResult::NotFound(timestamp, e.to_string()), 163 ); 164 self.metrics.incr("resolver.memory.cache_set_error").await; 165 tracing::debug!("Cached failed resolution for handle {}: {}", handle, e); 166 } 167 } 168 169 // Track cache size 170 let cache_size = cache.len() as u64; 171 self.metrics 172 .gauge("resolver.memory.cache_entries", cache_size) 173 .await; 174 } 175 176 result 177 } 178 179 async fn set(&self, handle: &str, did: &str) -> Result<(), HandleResolverError> { 180 // Normalize the handle to lowercase 181 let handle = handle.to_lowercase(); 182 183 // Update the in-memory cache 184 { 185 let mut cache = self.cache.write().await; 186 let timestamp = Self::current_timestamp(); 187 cache.insert( 188 handle.clone(), 189 ResolveHandleResult::Found(timestamp, did.to_string()), 190 ); 191 self.metrics.incr("resolver.memory.set").await; 192 tracing::debug!("Set handle {} -> DID {} in memory cache", handle, did); 193 194 // Track cache size 195 let cache_size = cache.len() as u64; 196 self.metrics 197 .gauge("resolver.memory.cache_entries", cache_size) 198 .await; 199 } 200 201 // Chain to inner resolver 202 self.inner.set(&handle, did).await 203 } 204} 205 206/// Create a new in-memory caching handle resolver. 207/// 208/// This factory function creates a resolver that caches resolution results 209/// in memory with a configurable TTL. 210/// 211/// # Arguments 212/// 213/// * `inner` - The underlying resolver to use for actual resolution 214/// * `ttl_seconds` - How long to cache results in seconds 215/// * `metrics` - Metrics publisher for telemetry 216/// 217/// # Example 218/// 219/// ```no_run 220/// use std::sync::Arc; 221/// use quickdid::handle_resolver::{create_base_resolver, create_caching_resolver, HandleResolver}; 222/// use quickdid::metrics::NoOpMetricsPublisher; 223/// 224/// # async fn example() { 225/// # use atproto_identity::resolve::HickoryDnsResolver; 226/// # use reqwest::Client; 227/// # let dns_resolver = Arc::new(HickoryDnsResolver::create_resolver(&[])); 228/// # let http_client = Client::new(); 229/// # let metrics = Arc::new(NoOpMetricsPublisher); 230/// let base = create_base_resolver( 231/// dns_resolver, 232/// http_client, 233/// metrics.clone(), 234/// ); 235/// 236/// let resolver = create_caching_resolver(base, 300, metrics); // 5 minute TTL 237/// let did = resolver.resolve("alice.bsky.social").await.unwrap(); 238/// # } 239/// ``` 240pub fn create_caching_resolver( 241 inner: Arc<dyn HandleResolver>, 242 ttl_seconds: u64, 243 metrics: SharedMetricsPublisher, 244) -> Arc<dyn HandleResolver> { 245 Arc::new(CachingHandleResolver::new(inner, ttl_seconds, metrics)) 246}