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+# QuickDID - Development Guide for Claude
22+33+## Overview
44+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.
55+66+## Common Commands
77+88+### Building and Running
99+```bash
1010+# Build the project
1111+cargo build
1212+1313+# Run in debug mode
1414+cargo run
1515+1616+# Run tests
1717+cargo test
1818+1919+# Type checking
2020+cargo check
2121+2222+# Run with environment variables
2323+HTTP_EXTERNAL=localhost:3007 SERVICE_KEY=did:key:z42tmZxD2mi1TfMKSFrsRfednwdaaPNZiiWHP4MPgcvXkDWK cargo run
2424+```
2525+2626+### Development with VS Code
2727+The project includes a `.vscode/launch.json` configuration for debugging with Redis integration. Use the "Debug executable 'quickdid'" launch configuration.
2828+2929+## Architecture
3030+3131+### Core Components
3232+3333+1. **Handle Resolution** (`src/handle_resolver.rs`)
3434+ - `BaseHandleResolver`: Core resolution using DNS and HTTP
3535+ - `CachingHandleResolver`: In-memory caching layer
3636+ - `RedisHandleResolver`: Redis-backed persistent caching with 90-day TTL
3737+ - Uses binary serialization via `HandleResolutionResult` for space efficiency
3838+3939+2. **Binary Serialization** (`src/handle_resolution_result.rs`)
4040+ - Compact storage format using bincode
4141+ - Strips DID prefixes for did:web and did:plc methods
4242+ - Stores: timestamp (u64), method type (i16), payload (String)
4343+4444+3. **Queue System** (`src/queue_adapter.rs`)
4545+ - Supports MPSC (in-process) and Redis adapters
4646+ - `HandleResolutionWork` items processed asynchronously
4747+ - Redis uses reliable queue pattern (LPUSH/RPOPLPUSH/LREM)
4848+4949+4. **HTTP Server** (`src/http/`)
5050+ - XRPC endpoints for AT Protocol compatibility
5151+ - Health check endpoint
5252+ - DID document serving via .well-known
5353+5454+## Key Technical Details
5555+5656+### DID Method Types
5757+- `did:web`: Web-based DIDs, prefix stripped for storage
5858+- `did:plc`: PLC directory DIDs, prefix stripped for storage
5959+- Other DID methods stored with full identifier
6060+6161+### Redis Integration
6262+- **Caching**: Uses MetroHash64 for key generation, stores binary data
6363+- **Queuing**: Reliable queue with processing/dead letter queues
6464+- **Key Prefixes**: Configurable via `QUEUE_REDIS_PREFIX` environment variable
6565+6666+### Handle Resolution Flow
6767+1. Check Redis cache (if configured)
6868+2. Fall back to in-memory cache
6969+3. Perform DNS TXT lookup or HTTP well-known query
7070+4. Cache result with appropriate TTL
7171+5. Return DID or error
7272+7373+## Environment Variables
7474+7575+### Required
7676+- `HTTP_EXTERNAL`: External hostname for service endpoints (e.g., `localhost:3007`)
7777+- `SERVICE_KEY`: Private key for service identity (DID format)
7878+7979+### Optional
8080+- `HTTP_PORT`: Server port (default: 8080)
8181+- `PLC_HOSTNAME`: PLC directory hostname (default: plc.directory)
8282+- `REDIS_URL`: Redis connection URL for caching
8383+- `QUEUE_ADAPTER`: Queue type - 'mpsc' or 'redis' (default: mpsc)
8484+- `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)
8686+- `RUST_LOG`: Logging level (e.g., debug, info)
8787+8888+## Error Handling
8989+9090+All error strings must use this format:
9191+9292+ error-quickdid-<domain>-<number> <message>: <details>
9393+9494+Example errors:
9595+9696+* error-quickdid-resolve-1 Multiple DIDs resolved for method
9797+* error-quickdid-plc-1 HTTP request failed: https://google.com/ Not Found
9898+* error-quickdid-key-1 Error decoding key: invalid
9999+100100+Errors should be represented as enums using the `thiserror` library.
101101+102102+Avoid creating new errors with the `anyhow!(...)` or `bail!(...)` macro.
103103+104104+## Testing
105105+106106+### Running Tests
107107+```bash
108108+# Run all tests
109109+cargo test
110110+111111+# Run with Redis integration tests
112112+TEST_REDIS_URL=redis://localhost:6379 cargo test
113113+114114+# Run specific test module
115115+cargo test handle_resolver::tests
116116+```
117117+118118+### Test Coverage Areas
119119+- Handle resolution with various DID methods
120120+- Binary serialization/deserialization
121121+- Redis caching and expiration
122122+- Queue processing logic
123123+- HTTP endpoint responses
124124+125125+## Development Patterns
126126+127127+### Error Handling
128128+- Uses `anyhow::Result` for error propagation
129129+- Graceful fallbacks when Redis is unavailable
130130+- Detailed tracing for debugging
131131+132132+### Performance Optimizations
133133+- Binary serialization reduces storage by ~40%
134134+- MetroHash64 for fast key generation
135135+- Connection pooling for Redis
136136+- Configurable TTLs for cache entries
137137+138138+### Code Style
139139+- Follow existing Rust idioms and patterns
140140+- Use `tracing` for logging, not `println!`
141141+- Prefer `Arc` for shared state across async tasks
142142+- Handle errors explicitly, avoid `.unwrap()` in production code
143143+144144+## Common Tasks
145145+146146+### Adding a New DID Method
147147+1. Update `DidMethodType` enum in `handle_resolution_result.rs`
148148+2. Modify `parse_did()` and `to_did()` methods
149149+3. Add test cases for the new method type
150150+151151+### Modifying Cache TTL
152152+- For in-memory: Pass TTL to `CachingHandleResolver::new()`
153153+- For Redis: Modify `RedisHandleResolver::ttl_seconds()`
154154+155155+### Debugging Resolution Issues
156156+1. Enable debug logging: `RUST_LOG=debug`
157157+2. Check Redis cache: `redis-cli GET "handle:<hash>"`
158158+3. Monitor queue processing in logs
159159+4. Verify DNS/HTTP connectivity to AT Protocol infrastructure
160160+161161+## Dependencies
162162+- `atproto-identity`: Core AT Protocol identity resolution
163163+- `bincode`: Binary serialization
164164+- `deadpool-redis`: Redis connection pooling
165165+- `metrohash`: Fast non-cryptographic hashing
166166+- `tokio`: Async runtime
167167+- `axum`: Web framework
···11+# QuickDID
22+33+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.
44+55+## Features
66+77+- **Fast Handle Resolution**: Resolves AT Protocol handles to DIDs using DNS TXT records and HTTP well-known endpoints
88+- **Multi-Layer Caching**: In-memory caching with configurable TTL and Redis-backed persistent caching (90-day TTL)
99+- **Binary Serialization**: Compact storage format reduces cache size by ~40% compared to JSON
1010+- **Queue Processing**: Asynchronous handle resolution with support for MPSC and Redis queue adapters
1111+- **AT Protocol Compatible**: Implements XRPC endpoints for seamless integration with AT Protocol infrastructure
1212+- **Production Ready**: Comprehensive error handling, health checks, and graceful shutdown support
1313+1414+## Building
1515+1616+### Prerequisites
1717+1818+- Rust 1.70 or later
1919+- Redis (optional, for persistent caching and distributed queuing)
2020+2121+### Build Commands
2222+2323+```bash
2424+# Clone the repository
2525+git clone https://github.com/yourusername/quickdid.git
2626+cd quickdid
2727+2828+# Build the project
2929+cargo build --release
3030+3131+# Run tests
3232+cargo test
3333+3434+# Run with debug logging
3535+RUST_LOG=debug cargo run
3636+```
3737+3838+## Minimum Configuration
3939+4040+QuickDID requires the following environment variables to run:
4141+4242+### Required
4343+4444+- `HTTP_EXTERNAL`: External hostname for service endpoints (e.g., `localhost:3007`)
4545+- `SERVICE_KEY`: Private key for service identity in DID format (e.g., `did:key:z42tmZxD2mi1TfMKSFrsRfednwdaaPNZiiWHP4MPgcvXkDWK`)
4646+4747+### Example Minimal Setup
4848+4949+```bash
5050+HTTP_EXTERNAL=localhost:3007 \
5151+SERVICE_KEY=did:key:z42tmZxD2mi1TfMKSFrsRfednwdaaPNZiiWHP4MPgcvXkDWK \
5252+cargo run
5353+```
5454+5555+This will start QuickDID with:
5656+- HTTP server on port 8080 (default)
5757+- In-memory caching only
5858+- MPSC queue adapter for async processing
5959+- Connection to plc.directory for DID resolution
6060+6161+### Optional Configuration
6262+6363+For production deployments, consider these additional environment variables:
6464+6565+- `HTTP_PORT`: Server port (default: 8080)
6666+- `REDIS_URL`: Redis connection URL for persistent caching (e.g., `redis://localhost:6379`)
6767+- `QUEUE_ADAPTER`: Queue type - 'mpsc' or 'redis' (default: mpsc)
6868+- `PLC_HOSTNAME`: PLC directory hostname (default: plc.directory)
6969+- `RUST_LOG`: Logging level (e.g., debug, info, warn, error)
7070+7171+### Production Example
7272+7373+```bash
7474+HTTP_EXTERNAL=quickdid.example.com \
7575+SERVICE_KEY=did:key:yourkeyhere \
7676+HTTP_PORT=3000 \
7777+REDIS_URL=redis://localhost:6379 \
7878+QUEUE_ADAPTER=redis \
7979+RUST_LOG=info \
8080+./target/release/quickdid
8181+```
8282+8383+## API Endpoints
8484+8585+- `GET /_health` - Health check endpoint
8686+- `GET /xrpc/com.atproto.identity.resolveHandle` - Resolve handle to DID
8787+- `GET /.well-known/atproto-did` - Serve DID document for the service
8888+8989+## License
9090+9191+This project is open source and available under the MIT License.
9292+9393+Copyright (c) 2025 Nick Gerakines
9494+9595+Permission is hereby granted, free of charge, to any person obtaining a copy
9696+of this software and associated documentation files (the "Software"), to deal
9797+in the Software without restriction, including without limitation the rights
9898+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9999+copies of the Software, and to permit persons to whom the Software is
100100+furnished to do so, subject to the following conditions:
101101+102102+The above copyright notice and this permission notice shall be included in all
103103+copies or substantial portions of the Software.
104104+105105+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
106106+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
107107+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
108108+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
109109+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
110110+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
111111+SOFTWARE.
+138
docs/redis-handle-resolver.md
···11+# Redis-Backed Handle Resolver
22+33+The QuickDID service now supports Redis-backed caching for handle resolution, providing persistent caching with 90-day expiration times.
44+55+## Features
66+77+- **90-Day Cache TTL**: Resolved handles are cached in Redis for 90 days, significantly reducing resolution latency
88+- **Error Caching**: Failed resolutions are also cached (as empty strings) to prevent repeated failed lookups
99+- **Automatic Fallback**: If Redis is unavailable, the service falls back to in-memory caching
1010+- **Configurable Key Prefix**: Supports custom Redis key prefixes for multi-tenant deployments
1111+1212+## Configuration
1313+1414+Set the `REDIS_URL` environment variable to enable Redis caching:
1515+1616+```bash
1717+export REDIS_URL="redis://localhost:6379"
1818+# or with authentication
1919+export REDIS_URL="redis://username:password@localhost:6379/0"
2020+```
2121+2222+When Redis is configured:
2323+- Handle resolutions are cached with 90-day expiration
2424+- Cache keys use the format: `handle:{handle_name}`
2525+- Failed resolutions are cached as empty strings
2626+2727+When Redis is not configured:
2828+- Falls back to in-memory caching with 10-minute TTL
2929+- Cache is not persisted between service restarts
3030+3131+## Architecture
3232+3333+```
3434+┌─────────────────┐
3535+│ HTTP Request │
3636+└────────┬────────┘
3737+ │
3838+ ▼
3939+┌─────────────────┐
4040+│ Handle Resolver │
4141+│ Endpoint │
4242+└────────┬────────┘
4343+ │
4444+ ▼
4545+┌─────────────────────┐ Cache Miss ┌───────────────────┐
4646+│ RedisHandleResolver │ ─────────────────► │ BaseHandleResolver│
4747+│ │ │ │
4848+│ - Check Redis │ │ - DNS TXT lookup │
4949+│ - 90-day TTL │ ◄───────────────── │ - Well-known │
5050+│ - Cache result │ Resolution │ endpoint │
5151+└─────────┬───────────┘ └───────────────────┘
5252+ │
5353+ │ Cache Hit
5454+ ▼
5555+┌─────────────────┐
5656+│ Redis Cache │
5757+│ │
5858+│ handle:alice -> │
5959+│ did:plc:xyz... │
6060+└─────────────────┘
6161+```
6262+6363+## Implementation Details
6464+6565+### RedisHandleResolver
6666+6767+The `RedisHandleResolver` struct wraps a base handle resolver and adds Redis caching:
6868+6969+```rust
7070+pub struct RedisHandleResolver {
7171+ inner: Arc<dyn HandleResolver>,
7272+ pool: RedisPool,
7373+ key_prefix: String,
7474+}
7575+```
7676+7777+### Cache Behavior
7878+7979+1. **Cache Hit**: Returns DID immediately from Redis
8080+2. **Cache Miss**: Resolves through inner resolver, caches result
8181+3. **Resolution Error**: Caches empty string to prevent repeated failures
8282+4. **Redis Error**: Falls back to inner resolver without caching
8383+8484+### Key Format
8585+8686+Redis keys follow this pattern:
8787+- Success: `handle:alice.bsky.social` → `did:plc:xyz123...`
8888+- Error: `handle:invalid.handle` → `""` (empty string)
8989+9090+## Testing
9191+9292+Tests can be run with a local Redis instance:
9393+9494+```bash
9595+# Start Redis locally
9696+docker run -d -p 6379:6379 redis:latest
9797+9898+# Run tests with Redis
9999+export TEST_REDIS_URL="redis://localhost:6379"
100100+cargo test
101101+102102+# Tests will skip if TEST_REDIS_URL is not set
103103+cargo test
104104+```
105105+106106+## Performance Considerations
107107+108108+- **Cache Hits**: Near-instantaneous (<1ms typical)
109109+- **Cache Misses**: Same as base resolver (DNS/HTTP lookups)
110110+- **Memory Usage**: Minimal, only stores handle → DID mappings
111111+- **Network Overhead**: Single Redis round-trip per resolution
112112+113113+## Monitoring
114114+115115+The service logs cache operations at various levels:
116116+117117+- `DEBUG`: Cache hits/misses
118118+- `INFO`: Redis pool creation and resolver selection
119119+- `WARN`: Redis connection failures and fallbacks
120120+121121+Example logs:
122122+```
123123+INFO Using Redis-backed handle resolver with 90-day cache TTL
124124+DEBUG Cache miss for handle alice.bsky.social, resolving...
125125+DEBUG Caching successful resolution for handle alice.bsky.social: did:plc:xyz123
126126+DEBUG Cache hit for handle alice.bsky.social: did:plc:xyz123
127127+```
128128+129129+## Migration
130130+131131+To migrate from in-memory to Redis caching:
132132+133133+1. Deploy Redis instance
134134+2. Set `REDIS_URL` environment variable
135135+3. Restart QuickDID service
136136+4. Service will automatically use Redis for new resolutions
137137+138138+No data migration needed - cache will populate on first resolution.
+368
src/bin/quickdid.rs
···11+use anyhow::Result;
22+use async_trait::async_trait;
33+use atproto_identity::{
44+ config::{CertificateBundles, DnsNameservers},
55+ key::{identify_key, to_public, KeyData, KeyProvider},
66+ resolve::HickoryDnsResolver,
77+};
88+use clap::Parser;
99+use quickdid::{
1010+ cache::create_redis_pool,
1111+ config::{Args, Config},
1212+ handle_resolver::{BaseHandleResolver, CachingHandleResolver, RedisHandleResolver},
1313+ handle_resolver_task::{HandleResolverTask, HandleResolverTaskConfig},
1414+ http::{create_router, server::AppContext, server::InnerAppContext},
1515+ queue_adapter::{
1616+ HandleResolutionWork, MpscQueueAdapter, NoopQueueAdapter, QueueAdapter, RedisQueueAdapter,
1717+ },
1818+ task_manager::spawn_cancellable_task,
1919+};
2020+use serde_json::json;
2121+use std::{collections::HashMap, sync::Arc};
2222+use tokio::signal;
2323+use tokio_util::{sync::CancellationToken, task::TaskTracker};
2424+use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
2525+2626+#[derive(Clone)]
2727+pub struct SimpleKeyProvider {
2828+ keys: HashMap<String, KeyData>,
2929+}
3030+3131+impl SimpleKeyProvider {
3232+ pub fn new() -> Self {
3333+ Self {
3434+ keys: HashMap::new(),
3535+ }
3636+ }
3737+}
3838+3939+#[async_trait]
4040+impl KeyProvider for SimpleKeyProvider {
4141+ async fn get_private_key_by_id(&self, key_id: &str) -> anyhow::Result<Option<KeyData>> {
4242+ Ok(self.keys.get(key_id).cloned())
4343+ }
4444+}
4545+4646+#[tokio::main]
4747+async fn main() -> Result<()> {
4848+ // Initialize tracing
4949+ tracing_subscriber::registry()
5050+ .with(
5151+ tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| {
5252+ "quickdid=info,atproto_identity=debug,atproto_xrpcs=debug".into()
5353+ }),
5454+ )
5555+ .with(tracing_subscriber::fmt::layer())
5656+ .init();
5757+5858+ let args = Args::parse();
5959+ let config = Config::from_args(args)?;
6060+6161+ tracing::info!("Starting QuickDID service on port {}", config.http_port);
6262+ tracing::info!("Service DID: {}", config.service_did);
6363+6464+ // Parse certificate bundles if provided
6565+ let certificate_bundles: CertificateBundles = config
6666+ .certificate_bundles
6767+ .clone()
6868+ .unwrap_or_default()
6969+ .try_into()?;
7070+7171+ // Parse DNS nameservers if provided
7272+ let dns_nameservers: DnsNameservers = config
7373+ .dns_nameservers
7474+ .clone()
7575+ .unwrap_or_default()
7676+ .try_into()?;
7777+7878+ // Build HTTP client
7979+ let mut client_builder = reqwest::Client::builder();
8080+ for ca_certificate in certificate_bundles.as_ref() {
8181+ let cert = std::fs::read(ca_certificate)?;
8282+ let cert = reqwest::Certificate::from_pem(&cert)?;
8383+ client_builder = client_builder.add_root_certificate(cert);
8484+ }
8585+ client_builder = client_builder.user_agent(&config.user_agent);
8686+ let http_client = client_builder.build()?;
8787+8888+ // Create DNS resolver
8989+ let dns_resolver = HickoryDnsResolver::create_resolver(dns_nameservers.as_ref());
9090+9191+ // Process service key
9292+ let private_service_key_data = identify_key(&config.service_key)?;
9393+ let public_service_key_data = to_public(&private_service_key_data)?;
9494+ let public_service_key = public_service_key_data.to_string();
9595+9696+ // Create service DID document
9797+ let service_document = json!({
9898+ "@context": vec!["https://www.w3.org/ns/did/v1", "https://w3id.org/security/multikey/v1"],
9999+ "id": config.service_did.clone(),
100100+ "verificationMethod": [{
101101+ "id": format!("{}#atproto", config.service_did),
102102+ "type": "Multikey",
103103+ "controller": config.service_did.clone(),
104104+ "publicKeyMultibase": public_service_key
105105+ }],
106106+ "service": []
107107+ });
108108+109109+ // Create DNS resolver Arc for sharing
110110+ let dns_resolver_arc = Arc::new(dns_resolver);
111111+112112+ // Create base handle resolver
113113+ let base_handle_resolver = Arc::new(BaseHandleResolver {
114114+ dns_resolver: dns_resolver_arc.clone(),
115115+ http_client: http_client.clone(),
116116+ plc_hostname: config.plc_hostname.clone(),
117117+ });
118118+119119+ // Create Redis pool if configured
120120+ let redis_pool = if let Some(redis_url) = &config.redis_url {
121121+ match create_redis_pool(redis_url) {
122122+ Ok(pool) => {
123123+ tracing::info!("Redis pool created for handle resolver cache");
124124+ Some(pool)
125125+ }
126126+ Err(e) => {
127127+ tracing::warn!("Failed to create Redis pool for handle resolver: {}", e);
128128+ None
129129+ }
130130+ }
131131+ } else {
132132+ None
133133+ };
134134+135135+ // Create handle resolver with Redis caching if available, otherwise use in-memory caching
136136+ let handle_resolver: Arc<dyn quickdid::handle_resolver::HandleResolver> =
137137+ if let Some(pool) = redis_pool {
138138+ tracing::info!("Using Redis-backed handle resolver with 90-day cache TTL");
139139+ Arc::new(RedisHandleResolver::new(base_handle_resolver, pool))
140140+ } else {
141141+ tracing::info!("Using in-memory handle resolver with 10-minute cache TTL");
142142+ Arc::new(CachingHandleResolver::new(
143143+ base_handle_resolver,
144144+ 600, // 10 minutes TTL for in-memory cache
145145+ ))
146146+ };
147147+148148+ // Create task tracker and cancellation token
149149+ let tracker = TaskTracker::new();
150150+ let token = CancellationToken::new();
151151+152152+ // Setup background handle resolution task and get the queue adapter
153153+ let handle_queue: Arc<dyn QueueAdapter<HandleResolutionWork>> = {
154154+ // Create queue adapter based on configuration
155155+ let adapter: Arc<dyn QueueAdapter<HandleResolutionWork>> = match config
156156+ .queue_adapter
157157+ .as_str()
158158+ {
159159+ "redis" => {
160160+ // Use queue-specific Redis URL, fall back to general Redis URL
161161+ let queue_redis_url = config
162162+ .queue_redis_url
163163+ .as_ref()
164164+ .or(config.redis_url.as_ref());
165165+166166+ match queue_redis_url {
167167+ Some(url) => match create_redis_pool(url) {
168168+ Ok(pool) => {
169169+ tracing::info!(
170170+ "Creating Redis queue adapter with prefix: {}",
171171+ config.queue_redis_prefix
172172+ );
173173+ Arc::new(RedisQueueAdapter::<HandleResolutionWork>::with_config(
174174+ pool,
175175+ config.queue_worker_id.clone(),
176176+ config.queue_redis_prefix.clone(),
177177+ 5, // 5 second timeout for blocking operations
178178+ ))
179179+ }
180180+ Err(e) => {
181181+ tracing::error!("Failed to create Redis pool for queue adapter: {}", e);
182182+ tracing::warn!("Falling back to MPSC queue adapter");
183183+ // Fall back to MPSC if Redis fails
184184+ let (handle_sender, handle_receiver) =
185185+ tokio::sync::mpsc::channel::<HandleResolutionWork>(
186186+ config.queue_buffer_size,
187187+ );
188188+ Arc::new(MpscQueueAdapter::from_channel(
189189+ handle_sender,
190190+ handle_receiver,
191191+ ))
192192+ }
193193+ },
194194+ None => {
195195+ tracing::warn!("Redis queue adapter requested but no Redis URL configured, using no-op adapter");
196196+ Arc::new(NoopQueueAdapter::<HandleResolutionWork>::new())
197197+ }
198198+ }
199199+ }
200200+ "mpsc" => {
201201+ // Use MPSC adapter
202202+ tracing::info!(
203203+ "Using MPSC queue adapter with buffer size: {}",
204204+ config.queue_buffer_size
205205+ );
206206+ let (handle_sender, handle_receiver) =
207207+ tokio::sync::mpsc::channel::<HandleResolutionWork>(config.queue_buffer_size);
208208+ Arc::new(MpscQueueAdapter::from_channel(
209209+ handle_sender,
210210+ handle_receiver,
211211+ ))
212212+ }
213213+ "noop" | "none" => {
214214+ // Use no-op adapter
215215+ tracing::info!("Using no-op queue adapter (queuing disabled)");
216216+ Arc::new(NoopQueueAdapter::<HandleResolutionWork>::new())
217217+ }
218218+ _ => {
219219+ // Default to no-op adapter for unknown types
220220+ tracing::warn!(
221221+ "Unknown queue adapter type '{}', using no-op adapter",
222222+ config.queue_adapter
223223+ );
224224+ Arc::new(NoopQueueAdapter::<HandleResolutionWork>::new())
225225+ }
226226+ };
227227+228228+ // Keep a reference to the adapter for the AppContext
229229+ let adapter_for_context = adapter.clone();
230230+231231+ // Only spawn handle resolver task if not using noop adapter
232232+ if !matches!(config.queue_adapter.as_str(), "noop" | "none") {
233233+ // Create handle resolver task configuration
234234+ let handle_task_config = HandleResolverTaskConfig {
235235+ default_timeout_ms: 10000,
236236+ };
237237+238238+ // Create and start handle resolver task
239239+ let handle_task = HandleResolverTask::with_config(
240240+ adapter,
241241+ handle_resolver.clone(),
242242+ token.clone(),
243243+ handle_task_config,
244244+ );
245245+246246+ // Spawn the handle resolver task
247247+ spawn_cancellable_task(
248248+ &tracker,
249249+ token.clone(),
250250+ "handle_resolver",
251251+ |cancel_token| async move {
252252+ tokio::select! {
253253+ result = handle_task.run() => {
254254+ if let Err(e) = result {
255255+ tracing::error!(error = ?e, "Handle resolver task failed");
256256+ Err(anyhow::anyhow!(e))
257257+ } else {
258258+ Ok(())
259259+ }
260260+ }
261261+ _ = cancel_token.cancelled() => {
262262+ tracing::info!("Handle resolver task cancelled");
263263+ Ok(())
264264+ }
265265+ }
266266+ },
267267+ );
268268+269269+ tracing::info!(
270270+ "Background handle resolution task started with {} adapter",
271271+ config.queue_adapter
272272+ );
273273+ } else {
274274+ tracing::info!("Background handle resolution task disabled (using no-op adapter)");
275275+ }
276276+277277+ // Return the adapter to be used in AppContext
278278+ adapter_for_context
279279+ };
280280+281281+ // Create app context with the queue adapter
282282+ let app_context = AppContext(Arc::new(InnerAppContext {
283283+ http_client: http_client.clone(),
284284+ service_document,
285285+ service_did: config.service_did.clone(),
286286+ handle_resolver: handle_resolver.clone(),
287287+ handle_queue,
288288+ }));
289289+290290+ // Create router
291291+ let router = create_router(app_context);
292292+293293+ // Setup signal handler
294294+ {
295295+ let signal_tracker = tracker.clone();
296296+ let signal_token = token.clone();
297297+298298+ // Spawn signal handler without using the managed task helper since it's special
299299+ tracing::info!("Starting signal handler task");
300300+ tokio::spawn(async move {
301301+ let ctrl_c = async {
302302+ signal::ctrl_c()
303303+ .await
304304+ .expect("failed to install Ctrl+C handler");
305305+ };
306306+307307+ #[cfg(unix)]
308308+ let terminate = async {
309309+ signal::unix::signal(signal::unix::SignalKind::terminate())
310310+ .expect("failed to install signal handler")
311311+ .recv()
312312+ .await;
313313+ };
314314+315315+ #[cfg(not(unix))]
316316+ let terminate = std::future::pending::<()>();
317317+318318+ tokio::select! {
319319+ () = signal_token.cancelled() => {
320320+ tracing::info!("Signal handler task shutting down gracefully");
321321+ },
322322+ _ = terminate => {
323323+ tracing::info!("Received SIGTERM signal, initiating shutdown");
324324+ },
325325+ _ = ctrl_c => {
326326+ tracing::info!("Received Ctrl+C signal, initiating shutdown");
327327+ },
328328+ }
329329+330330+ signal_tracker.close();
331331+ signal_token.cancel();
332332+ tracing::info!("Signal handler task completed");
333333+ });
334334+ }
335335+336336+ // Start HTTP server with cancellation support
337337+ let bind_address = format!("0.0.0.0:{}", config.http_port);
338338+ spawn_cancellable_task(
339339+ &tracker,
340340+ token.clone(),
341341+ "http",
342342+ move |cancel_token| async move {
343343+ let listener = tokio::net::TcpListener::bind(&bind_address)
344344+ .await
345345+ .map_err(|e| anyhow::anyhow!("Failed to bind to {}: {}", bind_address, e))?;
346346+347347+ tracing::info!("QuickDID service listening on {}", bind_address);
348348+349349+ let shutdown_token = cancel_token.clone();
350350+ axum::serve(listener, router)
351351+ .with_graceful_shutdown(async move {
352352+ shutdown_token.cancelled().await;
353353+ })
354354+ .await
355355+ .map_err(|e| anyhow::anyhow!("HTTP server error: {}", e))?;
356356+357357+ Ok(())
358358+ },
359359+ );
360360+361361+ // Wait for all tasks to complete
362362+ tracing::info!("Waiting for all tasks to complete...");
363363+ tracker.wait().await;
364364+365365+ tracing::info!("All tasks completed, application shutting down");
366366+367367+ Ok(())
368368+}
+11
src/cache.rs
···11+//! Redis cache utilities for QuickDID
22+33+use anyhow::Result;
44+use deadpool_redis::{Config, Pool, Runtime};
55+66+/// Create a Redis connection pool from a Redis URL
77+pub fn create_redis_pool(redis_url: &str) -> Result<Pool> {
88+ let config = Config::from_url(redis_url);
99+ let pool = config.create_pool(Some(Runtime::Tokio1))?;
1010+ Ok(pool)
1111+}
···11+pub mod cache;
22+pub mod config;
33+pub mod handle_resolution_result;
44+pub mod handle_resolver;
55+pub mod handle_resolver_task;
66+pub mod http;
77+pub mod queue_adapter;
88+pub mod task_manager;