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.
···1+# QuickDID - Development Guide for Claude
2+3+## Overview
4+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.
5+6+## Common Commands
7+8+### Building and Running
9+```bash
10+# Build the project
11+cargo build
12+13+# Run in debug mode
14+cargo run
15+16+# Run tests
17+cargo test
18+19+# Type checking
20+cargo check
21+22+# Run with environment variables
23+HTTP_EXTERNAL=localhost:3007 SERVICE_KEY=did:key:z42tmZxD2mi1TfMKSFrsRfednwdaaPNZiiWHP4MPgcvXkDWK cargo run
24+```
25+26+### Development with VS Code
27+The project includes a `.vscode/launch.json` configuration for debugging with Redis integration. Use the "Debug executable 'quickdid'" launch configuration.
28+29+## Architecture
30+31+### Core Components
32+33+1. **Handle Resolution** (`src/handle_resolver.rs`)
34+ - `BaseHandleResolver`: Core resolution using DNS and HTTP
35+ - `CachingHandleResolver`: In-memory caching layer
36+ - `RedisHandleResolver`: Redis-backed persistent caching with 90-day TTL
37+ - Uses binary serialization via `HandleResolutionResult` for space efficiency
38+39+2. **Binary Serialization** (`src/handle_resolution_result.rs`)
40+ - Compact storage format using bincode
41+ - Strips DID prefixes for did:web and did:plc methods
42+ - Stores: timestamp (u64), method type (i16), payload (String)
43+44+3. **Queue System** (`src/queue_adapter.rs`)
45+ - Supports MPSC (in-process) and Redis adapters
46+ - `HandleResolutionWork` items processed asynchronously
47+ - Redis uses reliable queue pattern (LPUSH/RPOPLPUSH/LREM)
48+49+4. **HTTP Server** (`src/http/`)
50+ - XRPC endpoints for AT Protocol compatibility
51+ - Health check endpoint
52+ - DID document serving via .well-known
53+54+## Key Technical Details
55+56+### DID Method Types
57+- `did:web`: Web-based DIDs, prefix stripped for storage
58+- `did:plc`: PLC directory DIDs, prefix stripped for storage
59+- Other DID methods stored with full identifier
60+61+### Redis Integration
62+- **Caching**: Uses MetroHash64 for key generation, stores binary data
63+- **Queuing**: Reliable queue with processing/dead letter queues
64+- **Key Prefixes**: Configurable via `QUEUE_REDIS_PREFIX` environment variable
65+66+### Handle Resolution Flow
67+1. Check Redis cache (if configured)
68+2. Fall back to in-memory cache
69+3. Perform DNS TXT lookup or HTTP well-known query
70+4. Cache result with appropriate TTL
71+5. Return DID or error
72+73+## Environment Variables
74+75+### Required
76+- `HTTP_EXTERNAL`: External hostname for service endpoints (e.g., `localhost:3007`)
77+- `SERVICE_KEY`: Private key for service identity (DID format)
78+79+### Optional
80+- `HTTP_PORT`: Server port (default: 8080)
81+- `PLC_HOSTNAME`: PLC directory hostname (default: plc.directory)
82+- `REDIS_URL`: Redis connection URL for caching
83+- `QUEUE_ADAPTER`: Queue type - 'mpsc' or 'redis' (default: mpsc)
84+- `QUEUE_REDIS_PREFIX`: Redis key prefix for queues (default: queue:handleresolver:)
85+- `QUEUE_WORKER_ID`: Worker ID for Redis queue (auto-generated if not set)
86+- `RUST_LOG`: Logging level (e.g., debug, info)
87+88+## Error Handling
89+90+All error strings must use this format:
91+92+ error-quickdid-<domain>-<number> <message>: <details>
93+94+Example errors:
95+96+* error-quickdid-resolve-1 Multiple DIDs resolved for method
97+* error-quickdid-plc-1 HTTP request failed: https://google.com/ Not Found
98+* error-quickdid-key-1 Error decoding key: invalid
99+100+Errors should be represented as enums using the `thiserror` library.
101+102+Avoid creating new errors with the `anyhow!(...)` or `bail!(...)` macro.
103+104+## Testing
105+106+### Running Tests
107+```bash
108+# Run all tests
109+cargo test
110+111+# Run with Redis integration tests
112+TEST_REDIS_URL=redis://localhost:6379 cargo test
113+114+# Run specific test module
115+cargo test handle_resolver::tests
116+```
117+118+### Test Coverage Areas
119+- Handle resolution with various DID methods
120+- Binary serialization/deserialization
121+- Redis caching and expiration
122+- Queue processing logic
123+- HTTP endpoint responses
124+125+## Development Patterns
126+127+### Error Handling
128+- Uses `anyhow::Result` for error propagation
129+- Graceful fallbacks when Redis is unavailable
130+- Detailed tracing for debugging
131+132+### Performance Optimizations
133+- Binary serialization reduces storage by ~40%
134+- MetroHash64 for fast key generation
135+- Connection pooling for Redis
136+- Configurable TTLs for cache entries
137+138+### Code Style
139+- Follow existing Rust idioms and patterns
140+- Use `tracing` for logging, not `println!`
141+- Prefer `Arc` for shared state across async tasks
142+- Handle errors explicitly, avoid `.unwrap()` in production code
143+144+## Common Tasks
145+146+### Adding a New DID Method
147+1. Update `DidMethodType` enum in `handle_resolution_result.rs`
148+2. Modify `parse_did()` and `to_did()` methods
149+3. Add test cases for the new method type
150+151+### Modifying Cache TTL
152+- For in-memory: Pass TTL to `CachingHandleResolver::new()`
153+- For Redis: Modify `RedisHandleResolver::ttl_seconds()`
154+155+### Debugging Resolution Issues
156+1. Enable debug logging: `RUST_LOG=debug`
157+2. Check Redis cache: `redis-cli GET "handle:<hash>"`
158+3. Monitor queue processing in logs
159+4. Verify DNS/HTTP connectivity to AT Protocol infrastructure
160+161+## Dependencies
162+- `atproto-identity`: Core AT Protocol identity resolution
163+- `bincode`: Binary serialization
164+- `deadpool-redis`: Redis connection pooling
165+- `metrohash`: Fast non-cryptographic hashing
166+- `tokio`: Async runtime
167+- `axum`: Web framework
···1+# QuickDID
2+3+QuickDID is a high-performance AT Protocol identity resolution service written in Rust. It provides blazing-fast handle-to-DID resolution with intelligent caching strategies, supporting both in-memory and Redis-backed persistent caching with binary serialization for optimal storage efficiency.
4+5+## Features
6+7+- **Fast Handle Resolution**: Resolves AT Protocol handles to DIDs using DNS TXT records and HTTP well-known endpoints
8+- **Multi-Layer Caching**: In-memory caching with configurable TTL and Redis-backed persistent caching (90-day TTL)
9+- **Binary Serialization**: Compact storage format reduces cache size by ~40% compared to JSON
10+- **Queue Processing**: Asynchronous handle resolution with support for MPSC and Redis queue adapters
11+- **AT Protocol Compatible**: Implements XRPC endpoints for seamless integration with AT Protocol infrastructure
12+- **Production Ready**: Comprehensive error handling, health checks, and graceful shutdown support
13+14+## Building
15+16+### Prerequisites
17+18+- Rust 1.70 or later
19+- Redis (optional, for persistent caching and distributed queuing)
20+21+### Build Commands
22+23+```bash
24+# Clone the repository
25+git clone https://github.com/yourusername/quickdid.git
26+cd quickdid
27+28+# Build the project
29+cargo build --release
30+31+# Run tests
32+cargo test
33+34+# Run with debug logging
35+RUST_LOG=debug cargo run
36+```
37+38+## Minimum Configuration
39+40+QuickDID requires the following environment variables to run:
41+42+### Required
43+44+- `HTTP_EXTERNAL`: External hostname for service endpoints (e.g., `localhost:3007`)
45+- `SERVICE_KEY`: Private key for service identity in DID format (e.g., `did:key:z42tmZxD2mi1TfMKSFrsRfednwdaaPNZiiWHP4MPgcvXkDWK`)
46+47+### Example Minimal Setup
48+49+```bash
50+HTTP_EXTERNAL=localhost:3007 \
51+SERVICE_KEY=did:key:z42tmZxD2mi1TfMKSFrsRfednwdaaPNZiiWHP4MPgcvXkDWK \
52+cargo run
53+```
54+55+This will start QuickDID with:
56+- HTTP server on port 8080 (default)
57+- In-memory caching only
58+- MPSC queue adapter for async processing
59+- Connection to plc.directory for DID resolution
60+61+### Optional Configuration
62+63+For production deployments, consider these additional environment variables:
64+65+- `HTTP_PORT`: Server port (default: 8080)
66+- `REDIS_URL`: Redis connection URL for persistent caching (e.g., `redis://localhost:6379`)
67+- `QUEUE_ADAPTER`: Queue type - 'mpsc' or 'redis' (default: mpsc)
68+- `PLC_HOSTNAME`: PLC directory hostname (default: plc.directory)
69+- `RUST_LOG`: Logging level (e.g., debug, info, warn, error)
70+71+### Production Example
72+73+```bash
74+HTTP_EXTERNAL=quickdid.example.com \
75+SERVICE_KEY=did:key:yourkeyhere \
76+HTTP_PORT=3000 \
77+REDIS_URL=redis://localhost:6379 \
78+QUEUE_ADAPTER=redis \
79+RUST_LOG=info \
80+./target/release/quickdid
81+```
82+83+## API Endpoints
84+85+- `GET /_health` - Health check endpoint
86+- `GET /xrpc/com.atproto.identity.resolveHandle` - Resolve handle to DID
87+- `GET /.well-known/atproto-did` - Serve DID document for the service
88+89+## License
90+91+This project is open source and available under the MIT License.
92+93+Copyright (c) 2025 Nick Gerakines
94+95+Permission is hereby granted, free of charge, to any person obtaining a copy
96+of this software and associated documentation files (the "Software"), to deal
97+in the Software without restriction, including without limitation the rights
98+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
99+copies of the Software, and to permit persons to whom the Software is
100+furnished to do so, subject to the following conditions:
101+102+The above copyright notice and this permission notice shall be included in all
103+copies or substantial portions of the Software.
104+105+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
106+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
107+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
108+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
109+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
110+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
111+SOFTWARE.
···1+use anyhow::Result;
2+use async_trait::async_trait;
3+use atproto_identity::{
4+ config::{CertificateBundles, DnsNameservers},
5+ key::{identify_key, to_public, KeyData, KeyProvider},
6+ resolve::HickoryDnsResolver,
7+};
8+use clap::Parser;
9+use quickdid::{
10+ cache::create_redis_pool,
11+ config::{Args, Config},
12+ handle_resolver::{BaseHandleResolver, CachingHandleResolver, RedisHandleResolver},
13+ handle_resolver_task::{HandleResolverTask, HandleResolverTaskConfig},
14+ http::{create_router, server::AppContext, server::InnerAppContext},
15+ queue_adapter::{
16+ HandleResolutionWork, MpscQueueAdapter, NoopQueueAdapter, QueueAdapter, RedisQueueAdapter,
17+ },
18+ task_manager::spawn_cancellable_task,
19+};
20+use serde_json::json;
21+use std::{collections::HashMap, sync::Arc};
22+use tokio::signal;
23+use tokio_util::{sync::CancellationToken, task::TaskTracker};
24+use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
25+26+#[derive(Clone)]
27+pub struct SimpleKeyProvider {
28+ keys: HashMap<String, KeyData>,
29+}
30+31+impl SimpleKeyProvider {
32+ pub fn new() -> Self {
33+ Self {
34+ keys: HashMap::new(),
35+ }
36+ }
37+}
38+39+#[async_trait]
40+impl KeyProvider for SimpleKeyProvider {
41+ async fn get_private_key_by_id(&self, key_id: &str) -> anyhow::Result<Option<KeyData>> {
42+ Ok(self.keys.get(key_id).cloned())
43+ }
44+}
45+46+#[tokio::main]
47+async fn main() -> Result<()> {
48+ // Initialize tracing
49+ tracing_subscriber::registry()
50+ .with(
51+ tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| {
52+ "quickdid=info,atproto_identity=debug,atproto_xrpcs=debug".into()
53+ }),
54+ )
55+ .with(tracing_subscriber::fmt::layer())
56+ .init();
57+58+ let args = Args::parse();
59+ let config = Config::from_args(args)?;
60+61+ tracing::info!("Starting QuickDID service on port {}", config.http_port);
62+ tracing::info!("Service DID: {}", config.service_did);
63+64+ // Parse certificate bundles if provided
65+ let certificate_bundles: CertificateBundles = config
66+ .certificate_bundles
67+ .clone()
68+ .unwrap_or_default()
69+ .try_into()?;
70+71+ // Parse DNS nameservers if provided
72+ let dns_nameservers: DnsNameservers = config
73+ .dns_nameservers
74+ .clone()
75+ .unwrap_or_default()
76+ .try_into()?;
77+78+ // Build HTTP client
79+ let mut client_builder = reqwest::Client::builder();
80+ for ca_certificate in certificate_bundles.as_ref() {
81+ let cert = std::fs::read(ca_certificate)?;
82+ let cert = reqwest::Certificate::from_pem(&cert)?;
83+ client_builder = client_builder.add_root_certificate(cert);
84+ }
85+ client_builder = client_builder.user_agent(&config.user_agent);
86+ let http_client = client_builder.build()?;
87+88+ // Create DNS resolver
89+ let dns_resolver = HickoryDnsResolver::create_resolver(dns_nameservers.as_ref());
90+91+ // Process service key
92+ let private_service_key_data = identify_key(&config.service_key)?;
93+ let public_service_key_data = to_public(&private_service_key_data)?;
94+ let public_service_key = public_service_key_data.to_string();
95+96+ // Create service DID document
97+ let service_document = json!({
98+ "@context": vec!["https://www.w3.org/ns/did/v1", "https://w3id.org/security/multikey/v1"],
99+ "id": config.service_did.clone(),
100+ "verificationMethod": [{
101+ "id": format!("{}#atproto", config.service_did),
102+ "type": "Multikey",
103+ "controller": config.service_did.clone(),
104+ "publicKeyMultibase": public_service_key
105+ }],
106+ "service": []
107+ });
108+109+ // Create DNS resolver Arc for sharing
110+ let dns_resolver_arc = Arc::new(dns_resolver);
111+112+ // Create base handle resolver
113+ let base_handle_resolver = Arc::new(BaseHandleResolver {
114+ dns_resolver: dns_resolver_arc.clone(),
115+ http_client: http_client.clone(),
116+ plc_hostname: config.plc_hostname.clone(),
117+ });
118+119+ // Create Redis pool if configured
120+ let redis_pool = if let Some(redis_url) = &config.redis_url {
121+ match create_redis_pool(redis_url) {
122+ Ok(pool) => {
123+ tracing::info!("Redis pool created for handle resolver cache");
124+ Some(pool)
125+ }
126+ Err(e) => {
127+ tracing::warn!("Failed to create Redis pool for handle resolver: {}", e);
128+ None
129+ }
130+ }
131+ } else {
132+ None
133+ };
134+135+ // Create handle resolver with Redis caching if available, otherwise use in-memory caching
136+ let handle_resolver: Arc<dyn quickdid::handle_resolver::HandleResolver> =
137+ if let Some(pool) = redis_pool {
138+ tracing::info!("Using Redis-backed handle resolver with 90-day cache TTL");
139+ Arc::new(RedisHandleResolver::new(base_handle_resolver, pool))
140+ } else {
141+ tracing::info!("Using in-memory handle resolver with 10-minute cache TTL");
142+ Arc::new(CachingHandleResolver::new(
143+ base_handle_resolver,
144+ 600, // 10 minutes TTL for in-memory cache
145+ ))
146+ };
147+148+ // Create task tracker and cancellation token
149+ let tracker = TaskTracker::new();
150+ let token = CancellationToken::new();
151+152+ // Setup background handle resolution task and get the queue adapter
153+ let handle_queue: Arc<dyn QueueAdapter<HandleResolutionWork>> = {
154+ // Create queue adapter based on configuration
155+ let adapter: Arc<dyn QueueAdapter<HandleResolutionWork>> = match config
156+ .queue_adapter
157+ .as_str()
158+ {
159+ "redis" => {
160+ // Use queue-specific Redis URL, fall back to general Redis URL
161+ let queue_redis_url = config
162+ .queue_redis_url
163+ .as_ref()
164+ .or(config.redis_url.as_ref());
165+166+ match queue_redis_url {
167+ Some(url) => match create_redis_pool(url) {
168+ Ok(pool) => {
169+ tracing::info!(
170+ "Creating Redis queue adapter with prefix: {}",
171+ config.queue_redis_prefix
172+ );
173+ Arc::new(RedisQueueAdapter::<HandleResolutionWork>::with_config(
174+ pool,
175+ config.queue_worker_id.clone(),
176+ config.queue_redis_prefix.clone(),
177+ 5, // 5 second timeout for blocking operations
178+ ))
179+ }
180+ Err(e) => {
181+ tracing::error!("Failed to create Redis pool for queue adapter: {}", e);
182+ tracing::warn!("Falling back to MPSC queue adapter");
183+ // Fall back to MPSC if Redis fails
184+ let (handle_sender, handle_receiver) =
185+ tokio::sync::mpsc::channel::<HandleResolutionWork>(
186+ config.queue_buffer_size,
187+ );
188+ Arc::new(MpscQueueAdapter::from_channel(
189+ handle_sender,
190+ handle_receiver,
191+ ))
192+ }
193+ },
194+ None => {
195+ tracing::warn!("Redis queue adapter requested but no Redis URL configured, using no-op adapter");
196+ Arc::new(NoopQueueAdapter::<HandleResolutionWork>::new())
197+ }
198+ }
199+ }
200+ "mpsc" => {
201+ // Use MPSC adapter
202+ tracing::info!(
203+ "Using MPSC queue adapter with buffer size: {}",
204+ config.queue_buffer_size
205+ );
206+ let (handle_sender, handle_receiver) =
207+ tokio::sync::mpsc::channel::<HandleResolutionWork>(config.queue_buffer_size);
208+ Arc::new(MpscQueueAdapter::from_channel(
209+ handle_sender,
210+ handle_receiver,
211+ ))
212+ }
213+ "noop" | "none" => {
214+ // Use no-op adapter
215+ tracing::info!("Using no-op queue adapter (queuing disabled)");
216+ Arc::new(NoopQueueAdapter::<HandleResolutionWork>::new())
217+ }
218+ _ => {
219+ // Default to no-op adapter for unknown types
220+ tracing::warn!(
221+ "Unknown queue adapter type '{}', using no-op adapter",
222+ config.queue_adapter
223+ );
224+ Arc::new(NoopQueueAdapter::<HandleResolutionWork>::new())
225+ }
226+ };
227+228+ // Keep a reference to the adapter for the AppContext
229+ let adapter_for_context = adapter.clone();
230+231+ // Only spawn handle resolver task if not using noop adapter
232+ if !matches!(config.queue_adapter.as_str(), "noop" | "none") {
233+ // Create handle resolver task configuration
234+ let handle_task_config = HandleResolverTaskConfig {
235+ default_timeout_ms: 10000,
236+ };
237+238+ // Create and start handle resolver task
239+ let handle_task = HandleResolverTask::with_config(
240+ adapter,
241+ handle_resolver.clone(),
242+ token.clone(),
243+ handle_task_config,
244+ );
245+246+ // Spawn the handle resolver task
247+ spawn_cancellable_task(
248+ &tracker,
249+ token.clone(),
250+ "handle_resolver",
251+ |cancel_token| async move {
252+ tokio::select! {
253+ result = handle_task.run() => {
254+ if let Err(e) = result {
255+ tracing::error!(error = ?e, "Handle resolver task failed");
256+ Err(anyhow::anyhow!(e))
257+ } else {
258+ Ok(())
259+ }
260+ }
261+ _ = cancel_token.cancelled() => {
262+ tracing::info!("Handle resolver task cancelled");
263+ Ok(())
264+ }
265+ }
266+ },
267+ );
268+269+ tracing::info!(
270+ "Background handle resolution task started with {} adapter",
271+ config.queue_adapter
272+ );
273+ } else {
274+ tracing::info!("Background handle resolution task disabled (using no-op adapter)");
275+ }
276+277+ // Return the adapter to be used in AppContext
278+ adapter_for_context
279+ };
280+281+ // Create app context with the queue adapter
282+ let app_context = AppContext(Arc::new(InnerAppContext {
283+ http_client: http_client.clone(),
284+ service_document,
285+ service_did: config.service_did.clone(),
286+ handle_resolver: handle_resolver.clone(),
287+ handle_queue,
288+ }));
289+290+ // Create router
291+ let router = create_router(app_context);
292+293+ // Setup signal handler
294+ {
295+ let signal_tracker = tracker.clone();
296+ let signal_token = token.clone();
297+298+ // Spawn signal handler without using the managed task helper since it's special
299+ tracing::info!("Starting signal handler task");
300+ tokio::spawn(async move {
301+ let ctrl_c = async {
302+ signal::ctrl_c()
303+ .await
304+ .expect("failed to install Ctrl+C handler");
305+ };
306+307+ #[cfg(unix)]
308+ let terminate = async {
309+ signal::unix::signal(signal::unix::SignalKind::terminate())
310+ .expect("failed to install signal handler")
311+ .recv()
312+ .await;
313+ };
314+315+ #[cfg(not(unix))]
316+ let terminate = std::future::pending::<()>();
317+318+ tokio::select! {
319+ () = signal_token.cancelled() => {
320+ tracing::info!("Signal handler task shutting down gracefully");
321+ },
322+ _ = terminate => {
323+ tracing::info!("Received SIGTERM signal, initiating shutdown");
324+ },
325+ _ = ctrl_c => {
326+ tracing::info!("Received Ctrl+C signal, initiating shutdown");
327+ },
328+ }
329+330+ signal_tracker.close();
331+ signal_token.cancel();
332+ tracing::info!("Signal handler task completed");
333+ });
334+ }
335+336+ // Start HTTP server with cancellation support
337+ let bind_address = format!("0.0.0.0:{}", config.http_port);
338+ spawn_cancellable_task(
339+ &tracker,
340+ token.clone(),
341+ "http",
342+ move |cancel_token| async move {
343+ let listener = tokio::net::TcpListener::bind(&bind_address)
344+ .await
345+ .map_err(|e| anyhow::anyhow!("Failed to bind to {}: {}", bind_address, e))?;
346+347+ tracing::info!("QuickDID service listening on {}", bind_address);
348+349+ let shutdown_token = cancel_token.clone();
350+ axum::serve(listener, router)
351+ .with_graceful_shutdown(async move {
352+ shutdown_token.cancelled().await;
353+ })
354+ .await
355+ .map_err(|e| anyhow::anyhow!("HTTP server error: {}", e))?;
356+357+ Ok(())
358+ },
359+ );
360+361+ // Wait for all tasks to complete
362+ tracing::info!("Waiting for all tasks to complete...");
363+ tracker.wait().await;
364+365+ tracing::info!("All tasks completed, application shutting down");
366+367+ Ok(())
368+}
+11
src/cache.rs
···00000000000
···1+//! Redis cache utilities for QuickDID
2+3+use anyhow::Result;
4+use deadpool_redis::{Config, Pool, Runtime};
5+6+/// Create a Redis connection pool from a Redis URL
7+pub fn create_redis_pool(redis_url: &str) -> Result<Pool> {
8+ let config = Config::from_url(redis_url);
9+ let pool = config.create_pool(Some(Runtime::Tokio1))?;
10+ Ok(pool)
11+}
···1+pub mod cache;
2+pub mod config;
3+pub mod handle_resolution_result;
4+pub mod handle_resolver;
5+pub mod handle_resolver_task;
6+pub mod http;
7+pub mod queue_adapter;
8+pub mod task_manager;