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.
···11+//! Redis-backed caching handle resolver.
22+//!
33+//! This module provides a handle resolver that caches resolution results in Redis
44+//! with configurable expiration times. Redis caching provides persistence across
55+//! service restarts and allows sharing of cached results across multiple instances.
66+77+use super::errors::HandleResolverError;
88+use super::traits::HandleResolver;
19use crate::handle_resolution_result::HandleResolutionResult;
210use async_trait::async_trait;
33-use atproto_identity::resolve::{DnsResolver, resolve_subject};
44-use chrono::Utc;
511use deadpool_redis::{Pool as RedisPool, redis::AsyncCommands};
612use metrohash::MetroHash64;
77-use reqwest::Client;
88-use std::collections::HashMap;
913use std::hash::Hasher as _;
1014use std::sync::Arc;
1111-use thiserror::Error;
1212-use tokio::sync::RwLock;
1313-1414-/// Errors that can occur during handle resolution
1515-#[derive(Error, Debug)]
1616-pub enum HandleResolverError {
1717- #[error("error-quickdid-resolve-1 Failed to resolve subject: {0}")]
1818- ResolutionFailed(String),
1919-2020- #[error("error-quickdid-resolve-2 Handle not found (cached): {0}")]
2121- HandleNotFoundCached(String),
2222-2323- #[error("error-quickdid-resolve-3 Handle not found (cached)")]
2424- HandleNotFound,
25152626- #[error("error-quickdid-resolve-4 Mock resolution failure")]
2727- MockResolutionFailure,
2828-}
2929-3030-#[async_trait]
3131-pub trait HandleResolver: Send + Sync {
3232- async fn resolve(&self, s: &str) -> Result<String, HandleResolverError>;
3333-}
3434-3535-pub struct BaseHandleResolver {
3636- /// DNS resolver for handle-to-DID resolution via TXT records.
3737- pub dns_resolver: Arc<dyn DnsResolver>,
3838- /// HTTP client for DID document retrieval and well-known endpoint queries.
3939- pub http_client: Client,
4040- /// Hostname of the PLC directory server for `did:plc` resolution.
4141- pub plc_hostname: String,
4242-}
4343-4444-#[async_trait]
4545-impl HandleResolver for BaseHandleResolver {
4646- async fn resolve(&self, s: &str) -> Result<String, HandleResolverError> {
4747- resolve_subject(&self.http_client, &*self.dns_resolver, s)
4848- .await
4949- .map_err(|e| HandleResolverError::ResolutionFailed(e.to_string()))
5050- }
5151-}
5252-5353-#[derive(Clone, Debug)]
5454-pub enum ResolveHandleResult {
5555- Found(u64, String),
5656- NotFound(u64, String),
5757-}
5858-5959-pub struct CachingHandleResolver {
6060- inner: Arc<dyn HandleResolver>,
6161- cache: Arc<RwLock<HashMap<String, ResolveHandleResult>>>,
6262- ttl_seconds: u64,
6363-}
6464-6565-impl CachingHandleResolver {
6666- pub fn new(inner: Arc<dyn HandleResolver>, ttl_seconds: u64) -> Self {
6767- Self {
6868- inner,
6969- cache: Arc::new(RwLock::new(HashMap::new())),
7070- ttl_seconds,
7171- }
7272- }
7373-7474- fn current_timestamp() -> u64 {
7575- Utc::now().timestamp() as u64
7676- }
7777-7878- fn is_expired(&self, timestamp: u64) -> bool {
7979- let current = Self::current_timestamp();
8080- current > timestamp && (current - timestamp) > self.ttl_seconds
8181- }
8282-}
8383-8484-#[async_trait]
8585-impl HandleResolver for CachingHandleResolver {
8686- async fn resolve(&self, s: &str) -> Result<String, HandleResolverError> {
8787- let handle = s.to_string();
8888-8989- // Check cache first
9090- {
9191- let cache = self.cache.read().await;
9292- if let Some(cached) = cache.get(&handle) {
9393- match cached {
9494- ResolveHandleResult::Found(timestamp, did) => {
9595- if !self.is_expired(*timestamp) {
9696- tracing::debug!("Cache hit for handle {}: {}", handle, did);
9797- return Ok(did.clone());
9898- }
9999- tracing::debug!("Cache entry expired for handle {}", handle);
100100- }
101101- ResolveHandleResult::NotFound(timestamp, error) => {
102102- if !self.is_expired(*timestamp) {
103103- tracing::debug!(
104104- "Cache hit (not found) for handle {}: {}",
105105- handle,
106106- error
107107- );
108108- return Err(HandleResolverError::HandleNotFoundCached(error.clone()));
109109- }
110110- tracing::debug!("Cache entry expired for handle {}", handle);
111111- }
112112- }
113113- }
114114- }
115115-116116- // Not in cache or expired, resolve through inner resolver
117117- tracing::debug!("Cache miss for handle {}, resolving...", handle);
118118- let result = self.inner.resolve(s).await;
119119- let timestamp = Self::current_timestamp();
120120-121121- // Store in cache
122122- {
123123- let mut cache = self.cache.write().await;
124124- match &result {
125125- Ok(did) => {
126126- cache.insert(
127127- handle.clone(),
128128- ResolveHandleResult::Found(timestamp, did.clone()),
129129- );
130130- tracing::debug!(
131131- "Cached successful resolution for handle {}: {}",
132132- handle,
133133- did
134134- );
135135- }
136136- Err(e) => {
137137- cache.insert(
138138- handle.clone(),
139139- ResolveHandleResult::NotFound(timestamp, e.to_string()),
140140- );
141141- tracing::debug!("Cached failed resolution for handle {}: {}", handle, e);
142142- }
143143- }
144144- }
145145-146146- result
147147- }
148148-}
149149-150150-/// Redis-backed caching handle resolver that caches resolution results in Redis
151151-/// with a configurable expiration time.
1616+/// Redis-backed caching handle resolver.
1717+///
1818+/// This resolver caches handle resolution results in Redis with a configurable TTL.
1919+/// Results are stored in a compact binary format using bincode serialization
2020+/// to minimize storage overhead.
2121+///
2222+/// # Features
2323+///
2424+/// - Persistent caching across service restarts
2525+/// - Shared cache across multiple service instances
2626+/// - Configurable TTL (default: 90 days)
2727+/// - Compact binary storage format
2828+/// - Graceful fallback if Redis is unavailable
2929+///
3030+/// # Example
3131+///
3232+/// ```no_run
3333+/// use std::sync::Arc;
3434+/// use deadpool_redis::Pool;
3535+/// use quickdid::handle_resolver::{RedisHandleResolver, BaseHandleResolver};
3636+///
3737+/// # async fn example() {
3838+/// # let base_resolver: BaseHandleResolver = todo!();
3939+/// # let redis_pool: Pool = todo!();
4040+/// // Create with default 90-day TTL
4141+/// let resolver = RedisHandleResolver::new(
4242+/// Arc::new(base_resolver),
4343+/// redis_pool.clone()
4444+/// );
4545+///
4646+/// // Or with custom TTL
4747+/// let resolver_with_ttl = RedisHandleResolver::with_ttl(
4848+/// Arc::new(base_resolver),
4949+/// redis_pool,
5050+/// 86400 // 1 day in seconds
5151+/// );
5252+/// # }
5353+/// ```
15254pub struct RedisHandleResolver {
15355 /// Base handle resolver to perform actual resolution
15456 inner: Arc<dyn HandleResolver>,
···16163}
1626416365impl RedisHandleResolver {
164164- /// Create a new Redis-backed handle resolver with default 90-day TTL
6666+ /// Create a new Redis-backed handle resolver with default 90-day TTL.
16567 pub fn new(inner: Arc<dyn HandleResolver>, pool: RedisPool) -> Self {
16668 Self::with_ttl(inner, pool, 90 * 24 * 60 * 60) // 90 days default
16769 }
16870169169- /// Create a new Redis-backed handle resolver with custom TTL
7171+ /// Create a new Redis-backed handle resolver with custom TTL.
17072 pub fn with_ttl(inner: Arc<dyn HandleResolver>, pool: RedisPool, ttl_seconds: u64) -> Self {
17173 Self::with_full_config(inner, pool, "handle:".to_string(), ttl_seconds)
17274 }
17375174174- /// Create a new Redis-backed handle resolver with a custom key prefix
7676+ /// Create a new Redis-backed handle resolver with a custom key prefix.
17577 pub fn with_prefix(
17678 inner: Arc<dyn HandleResolver>,
17779 pool: RedisPool,
···18082 Self::with_full_config(inner, pool, key_prefix, 90 * 24 * 60 * 60)
18183 }
18284183183- /// Create a new Redis-backed handle resolver with full configuration
8585+ /// Create a new Redis-backed handle resolver with full configuration.
18486 pub fn with_full_config(
18587 inner: Arc<dyn HandleResolver>,
18688 pool: RedisPool,
···19597 }
19698 }
19799198198- /// Generate the Redis key for a handle
100100+ /// Generate the Redis key for a handle.
101101+ ///
102102+ /// Uses MetroHash64 to generate a consistent hash of the handle
103103+ /// for use as the Redis key. This provides better key distribution
104104+ /// and avoids issues with special characters in handles.
199105 fn make_key(&self, handle: &str) -> String {
200106 let mut h = MetroHash64::default();
201107 h.write(handle.as_bytes());
202108 format!("{}{}", self.key_prefix, h.finish())
203109 }
204110205205- /// Get the TTL in seconds
111111+ /// Get the TTL in seconds.
206112 fn ttl_seconds(&self) -> u64 {
207113 self.ttl_seconds
208114 }
···445351 let _: Result<(), _> = conn.del(key).await;
446352 }
447353 }
448448-}
354354+}
+59
src/handle_resolver/base.rs
···11+//! Base handle resolver implementation.
22+//!
33+//! This module provides the fundamental handle resolution implementation that
44+//! performs actual DNS and HTTP lookups to resolve AT Protocol handles to DIDs.
55+66+use super::errors::HandleResolverError;
77+use super::traits::HandleResolver;
88+use async_trait::async_trait;
99+use atproto_identity::resolve::{DnsResolver, resolve_subject};
1010+use reqwest::Client;
1111+use std::sync::Arc;
1212+1313+/// Base handle resolver that performs actual resolution via DNS and HTTP.
1414+///
1515+/// This resolver implements the core AT Protocol handle resolution logic:
1616+/// 1. DNS TXT record lookup for `_atproto.{handle}`
1717+/// 2. HTTP well-known endpoint query at `https://{handle}/.well-known/atproto-did`
1818+/// 3. DID document retrieval from PLC directory or web DIDs
1919+///
2020+/// # Example
2121+///
2222+/// ```no_run
2323+/// use std::sync::Arc;
2424+/// use reqwest::Client;
2525+/// use atproto_identity::resolve::HickoryDnsResolver;
2626+/// use quickdid::handle_resolver::{BaseHandleResolver, HandleResolver};
2727+///
2828+/// # async fn example() {
2929+/// let dns_resolver = Arc::new(HickoryDnsResolver::create_resolver(&[]));
3030+/// let http_client = Client::new();
3131+///
3232+/// let resolver = BaseHandleResolver {
3333+/// dns_resolver,
3434+/// http_client,
3535+/// plc_hostname: "plc.directory".to_string(),
3636+/// };
3737+///
3838+/// let did = resolver.resolve("alice.bsky.social").await.unwrap();
3939+/// # }
4040+/// ```
4141+pub struct BaseHandleResolver {
4242+ /// DNS resolver for handle-to-DID resolution via TXT records.
4343+ pub dns_resolver: Arc<dyn DnsResolver>,
4444+4545+ /// HTTP client for DID document retrieval and well-known endpoint queries.
4646+ pub http_client: Client,
4747+4848+ /// Hostname of the PLC directory server for `did:plc` resolution.
4949+ pub plc_hostname: String,
5050+}
5151+5252+#[async_trait]
5353+impl HandleResolver for BaseHandleResolver {
5454+ async fn resolve(&self, s: &str) -> Result<String, HandleResolverError> {
5555+ resolve_subject(&self.http_client, &*self.dns_resolver, s)
5656+ .await
5757+ .map_err(|e| HandleResolverError::ResolutionFailed(e.to_string()))
5858+ }
5959+}
+26
src/handle_resolver/errors.rs
···11+//! Error types for handle resolution operations.
22+//!
33+//! This module defines all error types used throughout the handle resolver components,
44+//! following the QuickDID error format conventions.
55+66+use thiserror::Error;
77+88+/// Errors that can occur during handle resolution
99+#[derive(Error, Debug)]
1010+pub enum HandleResolverError {
1111+ /// Failed to resolve subject through DNS or HTTP
1212+ #[error("error-quickdid-resolve-1 Failed to resolve subject: {0}")]
1313+ ResolutionFailed(String),
1414+1515+ /// Handle not found in cache with specific error message
1616+ #[error("error-quickdid-resolve-2 Handle not found (cached): {0}")]
1717+ HandleNotFoundCached(String),
1818+1919+ /// Handle not found in cache (generic)
2020+ #[error("error-quickdid-resolve-3 Handle not found (cached)")]
2121+ HandleNotFound,
2222+2323+ /// Mock resolver failure for testing
2424+ #[error("error-quickdid-resolve-4 Mock resolution failure")]
2525+ MockResolutionFailure,
2626+}
+145
src/handle_resolver/memory.rs
···11+//! In-memory caching handle resolver.
22+//!
33+//! This module provides a handle resolver that caches resolution results in memory
44+//! with a configurable TTL. This is useful for reducing DNS/HTTP lookups and
55+//! improving performance when Redis is not available.
66+77+use super::errors::HandleResolverError;
88+use super::traits::HandleResolver;
99+use async_trait::async_trait;
1010+use chrono::Utc;
1111+use std::collections::HashMap;
1212+use std::sync::Arc;
1313+use tokio::sync::RwLock;
1414+1515+/// Result of a handle resolution cached in memory.
1616+#[derive(Clone, Debug)]
1717+pub enum ResolveHandleResult {
1818+ /// Handle was successfully resolved to a DID
1919+ Found(u64, String),
2020+ /// Handle resolution failed
2121+ NotFound(u64, String),
2222+}
2323+2424+/// In-memory caching wrapper for handle resolvers.
2525+///
2626+/// This resolver wraps another resolver and caches results in memory with
2727+/// a configurable TTL. Both successful and failed resolutions are cached
2828+/// to avoid repeated lookups.
2929+///
3030+/// # Example
3131+///
3232+/// ```no_run
3333+/// use std::sync::Arc;
3434+/// use quickdid::handle_resolver::{CachingHandleResolver, BaseHandleResolver, HandleResolver};
3535+///
3636+/// # async fn example() {
3737+/// # let base_resolver: BaseHandleResolver = todo!();
3838+/// let caching_resolver = CachingHandleResolver::new(
3939+/// Arc::new(base_resolver),
4040+/// 300 // 5 minute TTL
4141+/// );
4242+///
4343+/// // First call hits the underlying resolver
4444+/// let did1 = caching_resolver.resolve("alice.bsky.social").await.unwrap();
4545+///
4646+/// // Second call returns cached result
4747+/// let did2 = caching_resolver.resolve("alice.bsky.social").await.unwrap();
4848+/// # }
4949+/// ```
5050+pub struct CachingHandleResolver {
5151+ inner: Arc<dyn HandleResolver>,
5252+ cache: Arc<RwLock<HashMap<String, ResolveHandleResult>>>,
5353+ ttl_seconds: u64,
5454+}
5555+5656+impl CachingHandleResolver {
5757+ /// Create a new caching handle resolver.
5858+ ///
5959+ /// # Arguments
6060+ ///
6161+ /// * `inner` - The underlying resolver to use for actual resolution
6262+ /// * `ttl_seconds` - How long to cache results in seconds
6363+ pub fn new(inner: Arc<dyn HandleResolver>, ttl_seconds: u64) -> Self {
6464+ Self {
6565+ inner,
6666+ cache: Arc::new(RwLock::new(HashMap::new())),
6767+ ttl_seconds,
6868+ }
6969+ }
7070+7171+ fn current_timestamp() -> u64 {
7272+ Utc::now().timestamp() as u64
7373+ }
7474+7575+ fn is_expired(&self, timestamp: u64) -> bool {
7676+ let current = Self::current_timestamp();
7777+ current > timestamp && (current - timestamp) > self.ttl_seconds
7878+ }
7979+}
8080+8181+#[async_trait]
8282+impl HandleResolver for CachingHandleResolver {
8383+ async fn resolve(&self, s: &str) -> Result<String, HandleResolverError> {
8484+ let handle = s.to_string();
8585+8686+ // Check cache first
8787+ {
8888+ let cache = self.cache.read().await;
8989+ if let Some(cached) = cache.get(&handle) {
9090+ match cached {
9191+ ResolveHandleResult::Found(timestamp, did) => {
9292+ if !self.is_expired(*timestamp) {
9393+ tracing::debug!("Cache hit for handle {}: {}", handle, did);
9494+ return Ok(did.clone());
9595+ }
9696+ tracing::debug!("Cache entry expired for handle {}", handle);
9797+ }
9898+ ResolveHandleResult::NotFound(timestamp, error) => {
9999+ if !self.is_expired(*timestamp) {
100100+ tracing::debug!(
101101+ "Cache hit (not found) for handle {}: {}",
102102+ handle,
103103+ error
104104+ );
105105+ return Err(HandleResolverError::HandleNotFoundCached(error.clone()));
106106+ }
107107+ tracing::debug!("Cache entry expired for handle {}", handle);
108108+ }
109109+ }
110110+ }
111111+ }
112112+113113+ // Not in cache or expired, resolve through inner resolver
114114+ tracing::debug!("Cache miss for handle {}, resolving...", handle);
115115+ let result = self.inner.resolve(s).await;
116116+ let timestamp = Self::current_timestamp();
117117+118118+ // Store in cache
119119+ {
120120+ let mut cache = self.cache.write().await;
121121+ match &result {
122122+ Ok(did) => {
123123+ cache.insert(
124124+ handle.clone(),
125125+ ResolveHandleResult::Found(timestamp, did.clone()),
126126+ );
127127+ tracing::debug!(
128128+ "Cached successful resolution for handle {}: {}",
129129+ handle,
130130+ did
131131+ );
132132+ }
133133+ Err(e) => {
134134+ cache.insert(
135135+ handle.clone(),
136136+ ResolveHandleResult::NotFound(timestamp, e.to_string()),
137137+ );
138138+ tracing::debug!("Cached failed resolution for handle {}: {}", handle, e);
139139+ }
140140+ }
141141+ }
142142+143143+ result
144144+ }
145145+}
+54
src/handle_resolver/mod.rs
···11+//! Handle resolution module for AT Protocol identity resolution.
22+//!
33+//! This module provides various implementations of handle-to-DID resolution
44+//! with different caching strategies.
55+//!
66+//! # Architecture
77+//!
88+//! The module is structured around the [`HandleResolver`] trait with multiple
99+//! implementations:
1010+//!
1111+//! - [`BaseHandleResolver`]: Core resolver that performs actual DNS/HTTP lookups
1212+//! - [`CachingHandleResolver`]: In-memory caching wrapper with configurable TTL
1313+//! - [`RedisHandleResolver`]: Redis-backed persistent caching with binary serialization
1414+//!
1515+//! # Example Usage
1616+//!
1717+//! ```no_run
1818+//! use std::sync::Arc;
1919+//! use quickdid::handle_resolver::{BaseHandleResolver, CachingHandleResolver, HandleResolver};
2020+//!
2121+//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
2222+//! # use atproto_identity::resolve::HickoryDnsResolver;
2323+//! # use reqwest::Client;
2424+//! # let dns_resolver = Arc::new(HickoryDnsResolver::create_resolver(&[]));
2525+//! # let http_client = Client::new();
2626+//! // Create base resolver
2727+//! let base = Arc::new(BaseHandleResolver {
2828+//! dns_resolver,
2929+//! http_client,
3030+//! plc_hostname: "plc.directory".to_string(),
3131+//! });
3232+//!
3333+//! // Wrap with in-memory caching
3434+//! let resolver = CachingHandleResolver::new(base, 300);
3535+//!
3636+//! // Resolve a handle
3737+//! let did = resolver.resolve("alice.bsky.social").await?;
3838+//! # Ok(())
3939+//! # }
4040+//! ```
4141+4242+// Module structure
4343+mod errors;
4444+mod traits;
4545+mod base;
4646+mod memory;
4747+mod redis;
4848+4949+// Re-export public API
5050+pub use errors::HandleResolverError;
5151+pub use traits::HandleResolver;
5252+pub use base::BaseHandleResolver;
5353+pub use memory::{CachingHandleResolver, ResolveHandleResult};
5454+pub use redis::RedisHandleResolver;
+50
src/handle_resolver/traits.rs
···11+//! Core traits for handle resolution.
22+//!
33+//! This module defines the fundamental `HandleResolver` trait that all resolver
44+//! implementations must satisfy.
55+66+use super::errors::HandleResolverError;
77+use async_trait::async_trait;
88+99+/// Core trait for handle-to-DID resolution.
1010+///
1111+/// Implementations of this trait provide different strategies for resolving
1212+/// AT Protocol handles (like `alice.bsky.social`) to their corresponding
1313+/// DID identifiers (like `did:plc:xyz123`).
1414+///
1515+/// # Examples
1616+///
1717+/// ```no_run
1818+/// use async_trait::async_trait;
1919+/// use quickdid::handle_resolver::{HandleResolver, HandleResolverError};
2020+///
2121+/// struct MyResolver;
2222+///
2323+/// #[async_trait]
2424+/// impl HandleResolver for MyResolver {
2525+/// async fn resolve(&self, s: &str) -> Result<String, HandleResolverError> {
2626+/// // Custom resolution logic
2727+/// Ok(format!("did:plc:{}", s.replace('.', "")))
2828+/// }
2929+/// }
3030+/// ```
3131+#[async_trait]
3232+pub trait HandleResolver: Send + Sync {
3333+ /// Resolve a handle to its DID.
3434+ ///
3535+ /// # Arguments
3636+ ///
3737+ /// * `s` - The handle to resolve (e.g., "alice.bsky.social")
3838+ ///
3939+ /// # Returns
4040+ ///
4141+ /// The resolved DID on success, or an error if resolution fails.
4242+ ///
4343+ /// # Errors
4444+ ///
4545+ /// Returns [`HandleResolverError`] if:
4646+ /// - The handle cannot be resolved
4747+ /// - Network errors occur during resolution
4848+ /// - The handle is invalid or doesn't exist
4949+ async fn resolve(&self, s: &str) -> Result<String, HandleResolverError>;
5050+}