Select the types of activity you want to include in your feed.
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.
···323233331. **Handle Resolution** (`src/handle_resolver.rs`)
3434 - `BaseHandleResolver`: Core resolution using DNS and HTTP
3535+ - `RateLimitedHandleResolver`: Semaphore-based rate limiting for concurrent resolutions
3536 - `CachingHandleResolver`: In-memory caching layer
3637 - `RedisHandleResolver`: Redis-backed persistent caching with 90-day TTL
3838+ - `SqliteHandleResolver`: SQLite-backed persistent caching
3739 - Uses binary serialization via `HandleResolutionResult` for space efficiency
384039412. **Binary Serialization** (`src/handle_resolution_result.rs`)
···6466- **Key Prefixes**: Configurable via `QUEUE_REDIS_PREFIX` environment variable
65676668### Handle Resolution Flow
6767-1. Check Redis cache (if configured)
6868-2. Fall back to in-memory cache
6969+1. Check cache (Redis/SQLite/in-memory based on configuration)
7070+2. If not cached, acquire rate limit permit (if rate limiting enabled)
69713. Perform DNS TXT lookup or HTTP well-known query
70724. Cache result with appropriate TTL
71735. Return DID or error
···8082- `HTTP_PORT`: Server port (default: 8080)
8183- `PLC_HOSTNAME`: PLC directory hostname (default: plc.directory)
8284- `REDIS_URL`: Redis connection URL for caching
8383-- `QUEUE_ADAPTER`: Queue type - 'mpsc' or 'redis' (default: mpsc)
8585+- `QUEUE_ADAPTER`: Queue type - 'mpsc', 'redis', 'sqlite', or 'noop' (default: mpsc)
8486- `QUEUE_REDIS_PREFIX`: Redis key prefix for queues (default: queue:handleresolver:)
8585-- `QUEUE_WORKER_ID`: Worker ID for Redis queue (auto-generated if not set)
8787+- `QUEUE_WORKER_ID`: Worker ID for Redis queue (default: worker1)
8888+- `RESOLVER_MAX_CONCURRENT`: Maximum concurrent handle resolutions (default: 0 = disabled)
8689- `RUST_LOG`: Logging level (e.g., debug, info)
87908891## Error Handling
···134137- MetroHash64 for fast key generation
135138- Connection pooling for Redis
136139- Configurable TTLs for cache entries
140140+- Rate limiting via semaphore-based concurrency control
137141138142### Code Style
139143- Follow existing Rust idioms and patterns
+119-3
docs/configuration-reference.md
···88- [Network Configuration](#network-configuration)
99- [Caching Configuration](#caching-configuration)
1010- [Queue Configuration](#queue-configuration)
1111-- [Security Configuration](#security-configuration)
1212-- [Advanced Configuration](#advanced-configuration)
1111+- [Rate Limiting Configuration](#rate-limiting-configuration)
1312- [Configuration Examples](#configuration-examples)
1413- [Validation Rules](#validation-rules)
1514···453452- **Disk space concerns**: Lower values (1000-5000)
454453- **High ingestion rate**: Higher values (50000-1000000)
455454455455+## Rate Limiting Configuration
456456+457457+### `RESOLVER_MAX_CONCURRENT`
458458+459459+**Required**: No
460460+**Type**: Integer
461461+**Default**: `0` (disabled)
462462+**Range**: 0-10000
463463+**Constraints**: Must be between 0 and 10000
464464+465465+Maximum concurrent handle resolutions allowed. When set to a value greater than 0, enables semaphore-based rate limiting to protect upstream DNS and HTTP services from being overwhelmed.
466466+467467+**How it works**:
468468+- Uses a semaphore to limit concurrent resolutions
469469+- Applied between the base resolver and caching layers
470470+- Requests wait for an available permit before resolution
471471+- Helps prevent overwhelming upstream services
472472+473473+**Examples**:
474474+```bash
475475+# Disabled (default)
476476+RESOLVER_MAX_CONCURRENT=0
477477+478478+# Light rate limiting
479479+RESOLVER_MAX_CONCURRENT=10
480480+481481+# Moderate rate limiting
482482+RESOLVER_MAX_CONCURRENT=50
483483+484484+# Heavy traffic with rate limiting
485485+RESOLVER_MAX_CONCURRENT=100
486486+487487+# Maximum allowed
488488+RESOLVER_MAX_CONCURRENT=10000
489489+```
490490+491491+**Recommendations**:
492492+- **Development**: 0 (disabled) or 10-50 for testing
493493+- **Production (low traffic)**: 50-100
494494+- **Production (high traffic)**: 100-500
495495+- **Production (very high traffic)**: 500-1000
496496+- **Testing rate limiting**: 1-5 to observe behavior
497497+498498+**Placement in resolver stack**:
499499+```
500500+Request → Cache → RateLimited → Base → DNS/HTTP
501501+```
502502+503503+### `RESOLVER_MAX_CONCURRENT_TIMEOUT_MS`
504504+505505+**Required**: No
506506+**Type**: Integer (milliseconds)
507507+**Default**: `0` (no timeout)
508508+**Range**: 0-60000
509509+**Constraints**: Must be between 0 and 60000 (60 seconds max)
510510+511511+Timeout for acquiring a rate limit permit in milliseconds. When set to a value greater than 0, requests will timeout if they cannot acquire a permit within the specified time, preventing them from waiting indefinitely when the rate limiter is at capacity.
512512+513513+**How it works**:
514514+- Applied when `RESOLVER_MAX_CONCURRENT` is enabled (> 0)
515515+- Uses `tokio::time::timeout` to limit permit acquisition time
516516+- Returns an error if timeout expires before permit is acquired
517517+- Prevents request queue buildup during high load
518518+519519+**Examples**:
520520+```bash
521521+# No timeout (default)
522522+RESOLVER_MAX_CONCURRENT_TIMEOUT_MS=0
523523+524524+# Quick timeout for responsive failures (100ms)
525525+RESOLVER_MAX_CONCURRENT_TIMEOUT_MS=100
526526+527527+# Moderate timeout (1 second)
528528+RESOLVER_MAX_CONCURRENT_TIMEOUT_MS=1000
529529+530530+# Longer timeout for production (5 seconds)
531531+RESOLVER_MAX_CONCURRENT_TIMEOUT_MS=5000
532532+533533+# Maximum allowed (60 seconds)
534534+RESOLVER_MAX_CONCURRENT_TIMEOUT_MS=60000
535535+```
536536+537537+**Recommendations**:
538538+- **Development**: 100-1000ms for quick feedback
539539+- **Production (low latency)**: 1000-5000ms
540540+- **Production (high latency tolerance)**: 5000-30000ms
541541+- **Testing**: 100ms to quickly identify bottlenecks
542542+- **0**: Use when you want requests to wait indefinitely
543543+544544+**Error behavior**:
545545+When a timeout occurs, the request fails with:
546546+```
547547+Rate limit permit acquisition timed out after {timeout}ms
548548+```
549549+456550## Configuration Examples
457551458552### Minimal Development Configuration
···486580QUEUE_REDIS_TIMEOUT=5
487581QUEUE_BUFFER_SIZE=5000
488582583583+# Rate Limiting (optional, recommended for production)
584584+RESOLVER_MAX_CONCURRENT=100
585585+RESOLVER_MAX_CONCURRENT_TIMEOUT_MS=5000 # 5 second timeout
586586+489587# Logging
490588RUST_LOG=info
491589```
···511609QUEUE_ADAPTER=sqlite
512610QUEUE_BUFFER_SIZE=5000
513611QUEUE_SQLITE_MAX_SIZE=10000
612612+613613+# Rate Limiting (optional, recommended for production)
614614+RESOLVER_MAX_CONCURRENT=100
615615+RESOLVER_MAX_CONCURRENT_TIMEOUT_MS=5000 # 5 second timeout
514616515617# Logging
516618RUST_LOG=info
···543645# Performance
544646QUEUE_BUFFER_SIZE=10000
545647648648+# Rate Limiting (important for HA deployments)
649649+RESOLVER_MAX_CONCURRENT=500
650650+RESOLVER_MAX_CONCURRENT_TIMEOUT_MS=10000 # 10 second timeout for HA
651651+546652# Logging
547653RUST_LOG=warn
548654```
···655761 - Must be one of: `mpsc`, `redis`, `sqlite`, `noop`, `none`
656762 - Case-sensitive
657763658658-4. **Port** (`HTTP_PORT`):
764764+4. **Rate Limiting** (`RESOLVER_MAX_CONCURRENT`):
765765+ - Must be between 0 and 10000
766766+ - 0 = disabled (default)
767767+ - Values > 10000 will fail validation
768768+769769+5. **Rate Limiting Timeout** (`RESOLVER_MAX_CONCURRENT_TIMEOUT_MS`):
770770+ - Must be between 0 and 60000 (milliseconds)
771771+ - 0 = no timeout (default)
772772+ - Values > 60000 will fail validation
773773+774774+6. **Port** (`HTTP_PORT`):
659775 - Must be valid port number (1-65535)
660776 - Ports < 1024 require elevated privileges
661777
+26
docs/production-deployment.md
···184184# RUST_LOG_FORMAT=json
185185186186# ----------------------------------------------------------------------------
187187+# RATE LIMITING CONFIGURATION
188188+# ----------------------------------------------------------------------------
189189+190190+# Maximum concurrent handle resolutions (default: 0 = disabled)
191191+# When > 0, enables semaphore-based rate limiting
192192+# Range: 0-10000 (0 = disabled)
193193+# Protects upstream DNS/HTTP services from being overwhelmed
194194+RESOLVER_MAX_CONCURRENT=0
195195+196196+# Timeout for acquiring rate limit permit in milliseconds (default: 0 = no timeout)
197197+# When > 0, requests will timeout if they can't acquire a permit within this time
198198+# Range: 0-60000 (max 60 seconds)
199199+# Prevents requests from waiting indefinitely when rate limiter is at capacity
200200+RESOLVER_MAX_CONCURRENT_TIMEOUT_MS=0
201201+202202+# ----------------------------------------------------------------------------
187203# PERFORMANCE TUNING
188204# ----------------------------------------------------------------------------
189205···8388543. **Queue Adapter** (`QUEUE_ADAPTER`):
839855 - Must be one of: `mpsc`, `redis`, `sqlite`, `noop`, `none`
840856 - Case-sensitive
857857+858858+4. **Rate Limiting** (`RESOLVER_MAX_CONCURRENT`):
859859+ - Must be between 0 and 10000
860860+ - 0 = disabled (default)
861861+ - When > 0, limits concurrent handle resolutions
862862+863863+5. **Rate Limiting Timeout** (`RESOLVER_MAX_CONCURRENT_TIMEOUT_MS`):
864864+ - Must be between 0 and 60000 (milliseconds)
865865+ - 0 = no timeout (default)
866866+ - Maximum: 60000ms (60 seconds)
841867842868### Validation Errors
843869
+26-3
src/bin/quickdid.rs
···88 cache::create_redis_pool,
99 config::Config,
1010 handle_resolver::{
1111- create_base_resolver, create_caching_resolver, create_redis_resolver_with_ttl,
1212- create_sqlite_resolver_with_ttl,
1111+ create_base_resolver, create_caching_resolver, create_rate_limited_resolver_with_timeout,
1212+ create_redis_resolver_with_ttl, create_sqlite_resolver_with_ttl,
1313 },
1414 sqlite_schema::create_sqlite_pool,
1515 handle_resolver_task::{HandleResolverTaskConfig, create_handle_resolver_task_with_config},
···9999 println!(" QUEUE_WORKER_ID Worker ID for Redis queue (default: worker1)");
100100 println!(" QUEUE_BUFFER_SIZE Buffer size for MPSC queue (default: 1000)");
101101 println!(" QUEUE_SQLITE_MAX_SIZE Maximum SQLite queue size (default: 10000)");
102102+ println!();
103103+ println!(" RATE LIMITING:");
104104+ println!(" RESOLVER_MAX_CONCURRENT Maximum concurrent resolutions (default: 0 = disabled)");
105105+ println!(" RESOLVER_MAX_CONCURRENT_TIMEOUT_MS Timeout for acquiring permits in ms (default: 0 = no timeout)");
102106 println!();
103107 println!("For more information, visit: https://github.com/smokesignal.events/quickdid");
104108 return true;
···190194 let dns_resolver_arc = Arc::new(dns_resolver);
191195192196 // Create base handle resolver using factory function
193193- let base_handle_resolver = create_base_resolver(dns_resolver_arc.clone(), http_client.clone());
197197+ let mut base_handle_resolver = create_base_resolver(dns_resolver_arc.clone(), http_client.clone());
198198+199199+ // Apply rate limiting if configured
200200+ if config.resolver_max_concurrent > 0 {
201201+ let timeout_info = if config.resolver_max_concurrent_timeout_ms > 0 {
202202+ format!(", {}ms timeout", config.resolver_max_concurrent_timeout_ms)
203203+ } else {
204204+ String::new()
205205+ };
206206+ tracing::info!(
207207+ "Applying rate limiting to handle resolver (max {} concurrent resolutions{})",
208208+ config.resolver_max_concurrent,
209209+ timeout_info
210210+ );
211211+ base_handle_resolver = create_rate_limited_resolver_with_timeout(
212212+ base_handle_resolver,
213213+ config.resolver_max_concurrent,
214214+ config.resolver_max_concurrent_timeout_ms
215215+ );
216216+ }
194217195218 // Create Redis pool if configured
196219 let redis_pool = config
+11-3
src/cache.rs
···11//! Redis cache utilities for QuickDID
2233-use anyhow::Result;
43use deadpool_redis::{Config, Pool, Runtime};
44+use thiserror::Error;
55+66+/// Cache-specific errors
77+#[derive(Debug, Error)]
88+pub enum CacheError {
99+ /// Redis pool creation failed
1010+ #[error("error-quickdid-cache-1 Redis pool creation failed: {0}")]
1111+ RedisPoolCreationFailed(String),
1212+}
513614/// Create a Redis connection pool from a Redis URL.
715///
···1422/// Returns an error if:
1523/// - The Redis URL is invalid
1624/// - Pool creation fails
1717-pub fn create_redis_pool(redis_url: &str) -> Result<Pool> {
2525+pub fn create_redis_pool(redis_url: &str) -> Result<Pool, CacheError> {
1826 let config = Config::from_url(redis_url);
1927 let pool = config
2028 .create_pool(Some(Runtime::Tokio1))
2121- .map_err(|e| anyhow::anyhow!("error-quickdid-cache-1 Redis pool creation failed: {}", e))?;
2929+ .map_err(|e| CacheError::RedisPoolCreationFailed(e.to_string()))?;
2230 Ok(pool)
2331}
+26-4
src/config.rs
···44444545 /// Invalid configuration value that doesn't meet expected format or constraints
4646 ///
4747- /// Example: Invalid QUEUE_ADAPTER value (must be 'mpsc', 'redis', or 'noop')
4747+ /// Example: Invalid QUEUE_ADAPTER value (must be 'mpsc', 'redis', 'sqlite', 'noop', or 'none')
4848 #[error("error-quickdid-config-2 Invalid configuration value: {0}")]
4949 InvalidValue(String),
5050···8484/// Validated configuration for QuickDID service
8585///
8686/// This struct contains all configuration after validation and processing.
8787-/// Use `Config::from_args()` to create from command-line arguments and environment variables.
8787+/// Use `Config::from_env()` to create from environment variables.
8888///
8989/// ## Example
9090///
···164164 /// When exceeded, oldest entries are deleted to maintain this limit.
165165 /// Set to 0 to disable work shedding (unlimited queue size).
166166 pub queue_sqlite_max_size: u64,
167167+168168+ /// Maximum concurrent handle resolutions allowed (rate limiting).
169169+ /// When set to > 0, enables rate limiting using a semaphore.
170170+ /// Default: 0 (disabled)
171171+ pub resolver_max_concurrent: usize,
172172+173173+ /// Timeout for acquiring rate limit permit in milliseconds.
174174+ /// When set to > 0, requests will timeout if they can't acquire a permit within this time.
175175+ /// Default: 0 (no timeout)
176176+ pub resolver_max_concurrent_timeout_ms: u64,
167177}
168178169179impl Config {
···242252 cache_ttl_sqlite: parse_env("CACHE_TTL_SQLITE", 7776000)?,
243253 queue_redis_timeout: parse_env("QUEUE_REDIS_TIMEOUT", 5)?,
244254 queue_sqlite_max_size: parse_env("QUEUE_SQLITE_MAX_SIZE", 10000)?,
255255+ resolver_max_concurrent: parse_env("RESOLVER_MAX_CONCURRENT", 0)?,
256256+ resolver_max_concurrent_timeout_ms: parse_env("RESOLVER_MAX_CONCURRENT_TIMEOUT_MS", 0)?,
245257 })
246258 }
247259···250262 /// Checks:
251263 /// - Cache TTL values are positive (> 0)
252264 /// - Queue timeout is positive (> 0)
253253- /// - Queue adapter is a valid value ('mpsc', 'redis', 'noop', 'none')
265265+ /// - Queue adapter is a valid value ('mpsc', 'redis', 'sqlite', 'noop', 'none')
254266 ///
255267 /// ## Example
256268 ///
···293305 "mpsc" | "redis" | "sqlite" | "noop" | "none" => {}
294306 _ => {
295307 return Err(ConfigError::InvalidValue(format!(
296296- "Invalid QUEUE_ADAPTER '{}', must be 'mpsc', 'redis', 'sqlite', or 'noop'",
308308+ "Invalid QUEUE_ADAPTER '{}', must be 'mpsc', 'redis', 'sqlite', 'noop', or 'none'",
297309 self.queue_adapter
298310 )));
299311 }
312312+ }
313313+ if self.resolver_max_concurrent > 10000 {
314314+ return Err(ConfigError::InvalidValue(
315315+ "RESOLVER_MAX_CONCURRENT must be between 0 and 10000".to_string(),
316316+ ));
317317+ }
318318+ if self.resolver_max_concurrent_timeout_ms > 60000 {
319319+ return Err(ConfigError::InvalidTimeout(
320320+ "RESOLVER_MAX_CONCURRENT_TIMEOUT_MS must be <= 60000 (60 seconds)".to_string(),
321321+ ));
300322 }
301323 Ok(())
302324 }
+6-3
src/handle_resolution_result.rs
···1111/// Errors that can occur during handle resolution result operations
1212#[derive(Debug, Error)]
1313pub enum HandleResolutionError {
1414- #[error("error-quickdid-resolution-1 System time error: {0}")]
1414+ /// System time error when getting timestamp
1515+ #[error("error-quickdid-result-1 System time error: {0}")]
1516 SystemTime(String),
16171717- #[error("error-quickdid-serialization-1 Failed to serialize resolution result: {0}")]
1818+ /// Failed to serialize resolution result to binary format
1919+ #[error("error-quickdid-result-2 Failed to serialize resolution result: {0}")]
1820 Serialization(String),
19212020- #[error("error-quickdid-serialization-2 Failed to deserialize resolution result: {0}")]
2222+ /// Failed to deserialize resolution result from binary format
2323+ #[error("error-quickdid-result-3 Failed to deserialize resolution result: {0}")]
2124 Deserialization(String),
2225}
2326
+3
src/handle_resolver/mod.rs
···99//! implementations:
1010//!
1111//! - [`BaseHandleResolver`]: Core resolver that performs actual DNS/HTTP lookups
1212+//! - [`RateLimitedHandleResolver`]: Rate limiting wrapper using semaphore-based concurrency control
1213//! - [`CachingHandleResolver`]: In-memory caching wrapper with configurable TTL
1314//! - [`RedisHandleResolver`]: Redis-backed persistent caching with binary serialization
1415//! - [`SqliteHandleResolver`]: SQLite-backed persistent caching for single-instance deployments
···4344mod base;
4445mod errors;
4546mod memory;
4747+mod rate_limited;
4648mod redis;
4749mod sqlite;
4850mod traits;
···5456// Factory functions for creating resolvers
5557pub use base::create_base_resolver;
5658pub use memory::create_caching_resolver;
5959+pub use rate_limited::{create_rate_limited_resolver, create_rate_limited_resolver_with_timeout};
5760pub use redis::{create_redis_resolver, create_redis_resolver_with_ttl};
5861pub use sqlite::{create_sqlite_resolver, create_sqlite_resolver_with_ttl};
+213
src/handle_resolver/rate_limited.rs
···11+//! Rate-limited handle resolver implementation.
22+//!
33+//! This module provides a handle resolver wrapper that limits concurrent
44+//! resolution requests using a semaphore to implement basic rate limiting.
55+66+use super::errors::HandleResolverError;
77+use super::traits::HandleResolver;
88+use async_trait::async_trait;
99+use std::sync::Arc;
1010+use std::time::Duration;
1111+use tokio::sync::Semaphore;
1212+use tokio::time::timeout;
1313+1414+/// Rate-limited handle resolver that constrains concurrent resolutions.
1515+///
1616+/// This resolver wraps an inner resolver and uses a semaphore to limit
1717+/// the number of concurrent resolution requests. This provides basic
1818+/// rate limiting and protects upstream services from being overwhelmed.
1919+///
2020+/// # Architecture
2121+///
2222+/// The rate limiter should be placed between the base resolver and any
2323+/// caching layers:
2424+/// ```text
2525+/// Request -> Cache -> RateLimited -> Base -> DNS/HTTP
2626+/// ```
2727+///
2828+/// # Example
2929+///
3030+/// ```no_run
3131+/// use std::sync::Arc;
3232+/// use quickdid::handle_resolver::{
3333+/// create_base_resolver,
3434+/// create_rate_limited_resolver,
3535+/// HandleResolver,
3636+/// };
3737+///
3838+/// # async fn example() {
3939+/// # use atproto_identity::resolve::HickoryDnsResolver;
4040+/// # use reqwest::Client;
4141+/// # let dns_resolver = Arc::new(HickoryDnsResolver::create_resolver(&[]));
4242+/// # let http_client = Client::new();
4343+/// // Create base resolver
4444+/// let base = create_base_resolver(dns_resolver, http_client);
4545+///
4646+/// // Wrap with rate limiting (max 10 concurrent resolutions)
4747+/// let rate_limited = create_rate_limited_resolver(base, 10);
4848+///
4949+/// // Use the rate-limited resolver
5050+/// let did = rate_limited.resolve("alice.bsky.social").await.unwrap();
5151+/// # }
5252+/// ```
5353+pub(super) struct RateLimitedHandleResolver {
5454+ /// Inner resolver that performs actual resolution.
5555+ inner: Arc<dyn HandleResolver>,
5656+5757+ /// Semaphore for limiting concurrent resolutions.
5858+ semaphore: Arc<Semaphore>,
5959+6060+ /// Optional timeout for acquiring permits (in milliseconds).
6161+ /// When None or 0, no timeout is applied.
6262+ timeout_ms: Option<u64>,
6363+}
6464+6565+impl RateLimitedHandleResolver {
6666+ /// Create a new rate-limited resolver.
6767+ ///
6868+ /// # Arguments
6969+ ///
7070+ /// * `inner` - The inner resolver to wrap
7171+ /// * `max_concurrent` - Maximum number of concurrent resolutions allowed
7272+ pub fn new(inner: Arc<dyn HandleResolver>, max_concurrent: usize) -> Self {
7373+ Self {
7474+ inner,
7575+ semaphore: Arc::new(Semaphore::new(max_concurrent)),
7676+ timeout_ms: None,
7777+ }
7878+ }
7979+8080+ /// Create a new rate-limited resolver with timeout.
8181+ ///
8282+ /// # Arguments
8383+ ///
8484+ /// * `inner` - The inner resolver to wrap
8585+ /// * `max_concurrent` - Maximum number of concurrent resolutions allowed
8686+ /// * `timeout_ms` - Timeout in milliseconds for acquiring permits (0 = no timeout)
8787+ pub fn new_with_timeout(inner: Arc<dyn HandleResolver>, max_concurrent: usize, timeout_ms: u64) -> Self {
8888+ Self {
8989+ inner,
9090+ semaphore: Arc::new(Semaphore::new(max_concurrent)),
9191+ timeout_ms: if timeout_ms > 0 { Some(timeout_ms) } else { None },
9292+ }
9393+ }
9494+}
9595+9696+#[async_trait]
9797+impl HandleResolver for RateLimitedHandleResolver {
9898+ async fn resolve(&self, s: &str) -> Result<String, HandleResolverError> {
9999+ // Acquire a permit from the semaphore, with optional timeout
100100+ let _permit = match self.timeout_ms {
101101+ Some(timeout_ms) if timeout_ms > 0 => {
102102+ // Apply timeout when acquiring permit
103103+ let duration = Duration::from_millis(timeout_ms);
104104+ match timeout(duration, self.semaphore.acquire()).await {
105105+ Ok(Ok(permit)) => permit,
106106+ Ok(Err(e)) => {
107107+ // Semaphore error (e.g., closed)
108108+ return Err(HandleResolverError::ResolutionFailed(
109109+ format!("Failed to acquire rate limit permit: {}", e)
110110+ ));
111111+ }
112112+ Err(_) => {
113113+ // Timeout occurred
114114+ return Err(HandleResolverError::ResolutionFailed(
115115+ format!("Rate limit permit acquisition timed out after {}ms", timeout_ms)
116116+ ));
117117+ }
118118+ }
119119+ }
120120+ _ => {
121121+ // No timeout configured, wait indefinitely
122122+ self.semaphore.acquire().await
123123+ .map_err(|e| HandleResolverError::ResolutionFailed(
124124+ format!("Failed to acquire rate limit permit: {}", e)
125125+ ))?
126126+ }
127127+ };
128128+129129+ // With permit acquired, forward to inner resolver
130130+ self.inner.resolve(s).await
131131+ }
132132+}
133133+134134+/// Create a rate-limited handle resolver.
135135+///
136136+/// This factory function creates a new [`RateLimitedHandleResolver`] that wraps
137137+/// the provided inner resolver with concurrency limiting.
138138+///
139139+/// # Arguments
140140+///
141141+/// * `inner` - The resolver to wrap with rate limiting
142142+/// * `max_concurrent` - Maximum number of concurrent resolutions allowed
143143+///
144144+/// # Returns
145145+///
146146+/// An `Arc<dyn HandleResolver>` that can be used wherever a handle resolver is needed.
147147+///
148148+/// # Example
149149+///
150150+/// ```no_run
151151+/// use std::sync::Arc;
152152+/// use quickdid::handle_resolver::{
153153+/// create_base_resolver,
154154+/// create_rate_limited_resolver,
155155+/// };
156156+///
157157+/// # async fn example() {
158158+/// # use atproto_identity::resolve::HickoryDnsResolver;
159159+/// # use reqwest::Client;
160160+/// # let dns_resolver = Arc::new(HickoryDnsResolver::create_resolver(&[]));
161161+/// # let http_client = Client::new();
162162+/// let base = create_base_resolver(dns_resolver, http_client);
163163+/// let rate_limited = create_rate_limited_resolver(base, 10);
164164+/// # }
165165+/// ```
166166+pub fn create_rate_limited_resolver(
167167+ inner: Arc<dyn HandleResolver>,
168168+ max_concurrent: usize,
169169+) -> Arc<dyn HandleResolver> {
170170+ Arc::new(RateLimitedHandleResolver::new(inner, max_concurrent))
171171+}
172172+173173+/// Create a rate-limited handle resolver with timeout.
174174+///
175175+/// This factory function creates a new [`RateLimitedHandleResolver`] that wraps
176176+/// the provided inner resolver with concurrency limiting and timeout for permit acquisition.
177177+///
178178+/// # Arguments
179179+///
180180+/// * `inner` - The resolver to wrap with rate limiting
181181+/// * `max_concurrent` - Maximum number of concurrent resolutions allowed
182182+/// * `timeout_ms` - Timeout in milliseconds for acquiring permits (0 = no timeout)
183183+///
184184+/// # Returns
185185+///
186186+/// An `Arc<dyn HandleResolver>` that can be used wherever a handle resolver is needed.
187187+///
188188+/// # Example
189189+///
190190+/// ```no_run
191191+/// use std::sync::Arc;
192192+/// use quickdid::handle_resolver::{
193193+/// create_base_resolver,
194194+/// create_rate_limited_resolver_with_timeout,
195195+/// };
196196+///
197197+/// # async fn example() {
198198+/// # use atproto_identity::resolve::HickoryDnsResolver;
199199+/// # use reqwest::Client;
200200+/// # let dns_resolver = Arc::new(HickoryDnsResolver::create_resolver(&[]));
201201+/// # let http_client = Client::new();
202202+/// let base = create_base_resolver(dns_resolver, http_client);
203203+/// // Rate limit with 10 concurrent resolutions and 5 second timeout
204204+/// let rate_limited = create_rate_limited_resolver_with_timeout(base, 10, 5000);
205205+/// # }
206206+/// ```
207207+pub fn create_rate_limited_resolver_with_timeout(
208208+ inner: Arc<dyn HandleResolver>,
209209+ max_concurrent: usize,
210210+ timeout_ms: u64,
211211+) -> Arc<dyn HandleResolver> {
212212+ Arc::new(RateLimitedHandleResolver::new_with_timeout(inner, max_concurrent, timeout_ms))
213213+}
+2-1
src/handle_resolver_task.rs
···1616/// Handle resolver task errors
1717#[derive(Error, Debug)]
1818pub(crate) enum HandleResolverError {
1919- #[error("Queue adapter health check failed: adapter is not healthy")]
1919+ /// Queue adapter health check failed
2020+ #[error("error-quickdid-task-1 Queue adapter health check failed: adapter is not healthy")]
2021 QueueAdapterUnhealthy,
2122}
2223