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 Configuration Reference
2+3+This document provides a comprehensive reference for all configuration options available in QuickDID.
4+5+## Table of Contents
6+7+- [Required Configuration](#required-configuration)
8+- [Network Configuration](#network-configuration)
9+- [Caching Configuration](#caching-configuration)
10+- [Queue Configuration](#queue-configuration)
11+- [Security Configuration](#security-configuration)
12+- [Advanced Configuration](#advanced-configuration)
13+- [Configuration Examples](#configuration-examples)
14+- [Validation Rules](#validation-rules)
15+16+## Required Configuration
17+18+These environment variables MUST be set for QuickDID to start.
19+20+### `HTTP_EXTERNAL`
21+22+**Required**: Yes
23+**Type**: String
24+**Format**: Hostname with optional port
25+26+The external hostname where this service will be accessible. This is used to generate the service DID and for AT Protocol identity resolution.
27+28+**Examples**:
29+```bash
30+# Production domain
31+HTTP_EXTERNAL=quickdid.example.com
32+33+# With non-standard port
34+HTTP_EXTERNAL=quickdid.example.com:8080
35+36+# Development/testing
37+HTTP_EXTERNAL=localhost:3007
38+```
39+40+**Constraints**:
41+- Must be a valid hostname or hostname:port combination
42+- Port (if specified) must be between 1-65535
43+- Used to generate service DID (did:web:{HTTP_EXTERNAL})
44+45+### `SERVICE_KEY`
46+47+**Required**: Yes
48+**Type**: String
49+**Format**: DID private key
50+**Security**: SENSITIVE - Never commit to version control
51+52+The private key for the service's AT Protocol identity. This key is used to sign responses and authenticate the service.
53+54+**Examples**:
55+```bash
56+# did:key format (Ed25519)
57+SERVICE_KEY=did:key:z42tmZxD2mi1TfMKSFrsRfednwdaaPNZiiWHP4MPgcvXkDWK
58+59+# did:plc format
60+SERVICE_KEY=did:plc:xyz123abc456def789
61+```
62+63+**Constraints**:
64+- Must be a valid DID format
65+- Must include the private key component
66+- Should be stored securely (e.g., secrets manager, encrypted storage)
67+68+## Network Configuration
69+70+### `HTTP_PORT`
71+72+**Required**: No
73+**Type**: String
74+**Default**: `8080`
75+**Range**: 1-65535
76+77+The port number for the HTTP server to bind to.
78+79+**Examples**:
80+```bash
81+HTTP_PORT=8080 # Default
82+HTTP_PORT=3000 # Common alternative
83+HTTP_PORT=80 # Standard HTTP (requires root/privileges)
84+```
85+86+### `PLC_HOSTNAME`
87+88+**Required**: No
89+**Type**: String
90+**Default**: `plc.directory`
91+92+The hostname of the PLC directory service for DID resolution.
93+94+**Examples**:
95+```bash
96+PLC_HOSTNAME=plc.directory # Production (default)
97+PLC_HOSTNAME=test.plc.directory # Testing environment
98+PLC_HOSTNAME=localhost:2582 # Local PLC server
99+```
100+101+### `DNS_NAMESERVERS`
102+103+**Required**: No
104+**Type**: String (comma-separated IP addresses)
105+**Default**: System DNS
106+107+Custom DNS nameservers for handle resolution via TXT records.
108+109+**Examples**:
110+```bash
111+# Google DNS
112+DNS_NAMESERVERS=8.8.8.8,8.8.4.4
113+114+# Cloudflare DNS
115+DNS_NAMESERVERS=1.1.1.1,1.0.0.1
116+117+# Multiple providers
118+DNS_NAMESERVERS=8.8.8.8,1.1.1.1
119+120+# Local DNS
121+DNS_NAMESERVERS=192.168.1.1
122+```
123+124+### `USER_AGENT`
125+126+**Required**: No
127+**Type**: String
128+**Default**: `quickdid/{version} (+https://github.com/smokesignal.events/quickdid)`
129+130+HTTP User-Agent header for outgoing requests.
131+132+**Examples**:
133+```bash
134+# Custom agent
135+USER_AGENT="MyService/1.0.0 (+https://myservice.com)"
136+137+# With contact info
138+USER_AGENT="quickdid/1.0.0 (+https://quickdid.example.com; admin@example.com)"
139+```
140+141+### `CERTIFICATE_BUNDLES`
142+143+**Required**: No
144+**Type**: String (comma-separated file paths)
145+**Default**: System CA certificates
146+147+Additional CA certificate bundles for TLS connections.
148+149+**Examples**:
150+```bash
151+# Single certificate
152+CERTIFICATE_BUNDLES=/etc/ssl/certs/custom-ca.pem
153+154+# Multiple certificates
155+CERTIFICATE_BUNDLES=/certs/ca1.pem,/certs/ca2.pem
156+157+# Corporate CA
158+CERTIFICATE_BUNDLES=/usr/local/share/ca-certificates/corporate-ca.crt
159+```
160+161+## Caching Configuration
162+163+### `REDIS_URL`
164+165+**Required**: No (but highly recommended for production)
166+**Type**: String
167+**Format**: Redis connection URL
168+169+Redis connection URL for persistent caching. Enables distributed caching and better performance.
170+171+**Examples**:
172+```bash
173+# Local Redis (no auth)
174+REDIS_URL=redis://localhost:6379/0
175+176+# With authentication
177+REDIS_URL=redis://user:password@redis.example.com:6379/0
178+179+# Using database 1
180+REDIS_URL=redis://localhost:6379/1
181+182+# Redis Sentinel
183+REDIS_URL=redis-sentinel://sentinel1:26379,sentinel2:26379/mymaster/0
184+185+# TLS connection
186+REDIS_URL=rediss://secure-redis.example.com:6380/0
187+```
188+189+### `CACHE_TTL_MEMORY`
190+191+**Required**: No
192+**Type**: Integer (seconds)
193+**Default**: `600` (10 minutes)
194+**Range**: 60-3600 (recommended)
195+**Constraints**: Must be > 0
196+197+Time-to-live for in-memory cache entries in seconds. Used when Redis is not available.
198+199+**Examples**:
200+```bash
201+CACHE_TTL_MEMORY=300 # 5 minutes (aggressive refresh)
202+CACHE_TTL_MEMORY=600 # 10 minutes (default, balanced)
203+CACHE_TTL_MEMORY=1800 # 30 minutes (less frequent updates)
204+CACHE_TTL_MEMORY=3600 # 1 hour (stable data)
205+```
206+207+**Recommendations**:
208+- Lower values: Fresher data, more DNS/HTTP lookups, higher load
209+- Higher values: Better performance, potentially stale data
210+- Production with Redis: Can use lower values (300-600)
211+- Production without Redis: Use higher values (1800-3600)
212+213+### `CACHE_TTL_REDIS`
214+215+**Required**: No
216+**Type**: Integer (seconds)
217+**Default**: `7776000` (90 days)
218+**Range**: 3600-31536000 (1 hour to 1 year)
219+**Constraints**: Must be > 0
220+221+Time-to-live for Redis cache entries in seconds.
222+223+**Examples**:
224+```bash
225+CACHE_TTL_REDIS=3600 # 1 hour (frequently changing data)
226+CACHE_TTL_REDIS=86400 # 1 day (recommended for active handles)
227+CACHE_TTL_REDIS=604800 # 1 week (balanced)
228+CACHE_TTL_REDIS=2592000 # 30 days (stable handles)
229+CACHE_TTL_REDIS=7776000 # 90 days (default, maximum stability)
230+```
231+232+**Recommendations**:
233+- Social media handles: 1-7 days
234+- Corporate/stable handles: 30-90 days
235+- Test environments: 1 hour
236+237+## Queue Configuration
238+239+### `QUEUE_ADAPTER`
240+241+**Required**: No
242+**Type**: String
243+**Default**: `mpsc`
244+**Values**: `mpsc`, `redis`, `noop`
245+246+The type of queue adapter for background handle resolution.
247+248+**Options**:
249+- `mpsc`: In-memory multi-producer single-consumer queue (default)
250+- `redis`: Redis-backed distributed queue
251+- `noop`: Disable queue processing (testing only)
252+253+**Examples**:
254+```bash
255+# Single instance deployment
256+QUEUE_ADAPTER=mpsc
257+258+# Multi-instance or high availability
259+QUEUE_ADAPTER=redis
260+261+# Testing without background processing
262+QUEUE_ADAPTER=noop
263+```
264+265+### `QUEUE_REDIS_URL`
266+267+**Required**: No
268+**Type**: String
269+**Default**: Falls back to `REDIS_URL`
270+271+Dedicated Redis URL for queue operations. Use when separating cache and queue Redis instances.
272+273+**Examples**:
274+```bash
275+# Separate Redis for queues
276+QUEUE_REDIS_URL=redis://queue-redis:6379/2
277+278+# With different credentials
279+QUEUE_REDIS_URL=redis://queue_user:queue_pass@redis.example.com:6379/1
280+```
281+282+### `QUEUE_REDIS_PREFIX`
283+284+**Required**: No
285+**Type**: String
286+**Default**: `queue:handleresolver:`
287+288+Redis key prefix for queue operations. Use to namespace queues when sharing Redis.
289+290+**Examples**:
291+```bash
292+# Default
293+QUEUE_REDIS_PREFIX=queue:handleresolver:
294+295+# Environment-specific
296+QUEUE_REDIS_PREFIX=prod:queue:hr:
297+QUEUE_REDIS_PREFIX=staging:queue:hr:
298+299+# Version-specific
300+QUEUE_REDIS_PREFIX=quickdid:v1:queue:
301+302+# Instance-specific
303+QUEUE_REDIS_PREFIX=us-east-1:queue:hr:
304+```
305+306+### `QUEUE_REDIS_TIMEOUT`
307+308+**Required**: No
309+**Type**: Integer (seconds)
310+**Default**: `5`
311+**Range**: 1-60 (recommended)
312+**Constraints**: Must be > 0
313+314+Redis blocking timeout for queue operations in seconds. Controls how long to wait for new items.
315+316+**Examples**:
317+```bash
318+QUEUE_REDIS_TIMEOUT=1 # Very responsive, more polling
319+QUEUE_REDIS_TIMEOUT=5 # Default, balanced
320+QUEUE_REDIS_TIMEOUT=10 # Less polling, slower shutdown
321+QUEUE_REDIS_TIMEOUT=30 # Minimal polling, slow shutdown
322+```
323+324+### `QUEUE_WORKER_ID`
325+326+**Required**: No
327+**Type**: String
328+**Default**: Auto-generated UUID
329+330+Worker identifier for queue operations. Used in logs and monitoring.
331+332+**Examples**:
333+```bash
334+# Simple numbering
335+QUEUE_WORKER_ID=worker-001
336+337+# Environment-based
338+QUEUE_WORKER_ID=prod-us-east-1
339+QUEUE_WORKER_ID=staging-worker-2
340+341+# Hostname-based
342+QUEUE_WORKER_ID=$(hostname)
343+344+# Pod name in Kubernetes
345+QUEUE_WORKER_ID=$HOSTNAME
346+```
347+348+### `QUEUE_BUFFER_SIZE`
349+350+**Required**: No
351+**Type**: Integer
352+**Default**: `1000`
353+**Range**: 100-100000 (recommended)
354+355+Buffer size for the MPSC queue adapter. Only used when `QUEUE_ADAPTER=mpsc`.
356+357+**Examples**:
358+```bash
359+QUEUE_BUFFER_SIZE=100 # Minimal memory, may block
360+QUEUE_BUFFER_SIZE=1000 # Default, balanced
361+QUEUE_BUFFER_SIZE=5000 # High traffic
362+QUEUE_BUFFER_SIZE=10000 # Very high traffic
363+```
364+365+## Configuration Examples
366+367+### Minimal Development Configuration
368+369+```bash
370+# .env.development
371+HTTP_EXTERNAL=localhost:3007
372+SERVICE_KEY=did:key:z42tmZxD2mi1TfMKSFrsRfednwdaaPNZiiWHP4MPgcvXkDWK
373+RUST_LOG=debug
374+```
375+376+### Standard Production Configuration
377+378+```bash
379+# .env.production
380+# Required
381+HTTP_EXTERNAL=quickdid.example.com
382+SERVICE_KEY=${SECRET_SERVICE_KEY} # From secrets manager
383+384+# Network
385+HTTP_PORT=8080
386+USER_AGENT=quickdid/1.0.0 (+https://quickdid.example.com)
387+388+# Caching
389+REDIS_URL=redis://redis:6379/0
390+CACHE_TTL_MEMORY=600
391+CACHE_TTL_REDIS=86400 # 1 day
392+393+# Queue
394+QUEUE_ADAPTER=redis
395+QUEUE_REDIS_TIMEOUT=5
396+QUEUE_BUFFER_SIZE=5000
397+398+# Logging
399+RUST_LOG=info
400+```
401+402+### High-Availability Configuration
403+404+```bash
405+# .env.ha
406+# Required
407+HTTP_EXTERNAL=quickdid.example.com
408+SERVICE_KEY=${SECRET_SERVICE_KEY}
409+410+# Network
411+HTTP_PORT=8080
412+DNS_NAMESERVERS=8.8.8.8,8.8.4.4,1.1.1.1,1.0.0.1
413+414+# Caching (separate Redis instances)
415+REDIS_URL=redis://cache-redis:6379/0
416+CACHE_TTL_MEMORY=300
417+CACHE_TTL_REDIS=3600
418+419+# Queue (dedicated Redis)
420+QUEUE_ADAPTER=redis
421+QUEUE_REDIS_URL=redis://queue-redis:6379/0
422+QUEUE_REDIS_PREFIX=prod:queue:
423+QUEUE_WORKER_ID=${HOSTNAME}
424+QUEUE_REDIS_TIMEOUT=10
425+426+# Performance
427+QUEUE_BUFFER_SIZE=10000
428+429+# Logging
430+RUST_LOG=warn
431+```
432+433+### Docker Compose Configuration
434+435+```yaml
436+version: '3.8'
437+438+services:
439+ quickdid:
440+ image: quickdid:latest
441+ environment:
442+ HTTP_EXTERNAL: quickdid.example.com
443+ SERVICE_KEY: ${SERVICE_KEY}
444+ HTTP_PORT: 8080
445+ REDIS_URL: redis://redis:6379/0
446+ CACHE_TTL_MEMORY: 600
447+ CACHE_TTL_REDIS: 86400
448+ QUEUE_ADAPTER: redis
449+ QUEUE_REDIS_TIMEOUT: 5
450+ RUST_LOG: info
451+ ports:
452+ - "8080:8080"
453+ depends_on:
454+ - redis
455+456+ redis:
457+ image: redis:7-alpine
458+ command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru
459+```
460+461+## Validation Rules
462+463+QuickDID validates configuration at startup. The following rules are enforced:
464+465+### Required Fields
466+467+1. **HTTP_EXTERNAL**: Must be provided
468+2. **SERVICE_KEY**: Must be provided
469+470+### Value Constraints
471+472+1. **TTL Values** (`CACHE_TTL_MEMORY`, `CACHE_TTL_REDIS`):
473+ - Must be positive integers (> 0)
474+ - Recommended minimum: 60 seconds
475+476+2. **Timeout Values** (`QUEUE_REDIS_TIMEOUT`):
477+ - Must be positive integers (> 0)
478+ - Recommended range: 1-60 seconds
479+480+3. **Queue Adapter** (`QUEUE_ADAPTER`):
481+ - Must be one of: `mpsc`, `redis`, `noop`, `none`
482+ - Case-sensitive
483+484+4. **Port** (`HTTP_PORT`):
485+ - Must be valid port number (1-65535)
486+ - Ports < 1024 require elevated privileges
487+488+### Validation Errors
489+490+If validation fails, QuickDID will exit with one of these error codes:
491+492+- `error-quickdid-config-1`: Missing required environment variable
493+- `error-quickdid-config-2`: Invalid configuration value
494+- `error-quickdid-config-3`: Invalid TTL value (must be positive)
495+- `error-quickdid-config-4`: Invalid timeout value (must be positive)
496+497+### Testing Configuration
498+499+Test your configuration without starting the service:
500+501+```bash
502+# Validate configuration
503+HTTP_EXTERNAL=test SERVICE_KEY=test quickdid --help
504+505+# Test with specific values
506+CACHE_TTL_MEMORY=0 quickdid --help # Will fail validation
507+508+# Check parsed configuration (with debug logging)
509+RUST_LOG=debug HTTP_EXTERNAL=test SERVICE_KEY=test quickdid
510+```
511+512+## Best Practices
513+514+### Security
515+516+1. **Never commit SERVICE_KEY** to version control
517+2. Use environment-specific key management (Vault, AWS Secrets, etc.)
518+3. Rotate SERVICE_KEY regularly
519+4. Use TLS for Redis connections in production (`rediss://`)
520+5. Implement network segmentation for Redis access
521+522+### Performance
523+524+1. **With Redis**: Use lower memory cache TTL (300-600s)
525+2. **Without Redis**: Use higher memory cache TTL (1800-3600s)
526+3. **High traffic**: Increase QUEUE_BUFFER_SIZE (5000-10000)
527+4. **Multi-region**: Use region-specific QUEUE_WORKER_ID
528+529+### Monitoring
530+531+1. Set descriptive QUEUE_WORKER_ID for log correlation
532+2. Use structured logging with appropriate RUST_LOG levels
533+3. Monitor Redis memory usage and adjust TTLs accordingly
534+4. Track cache hit rates to optimize TTL values
535+536+### Deployment
537+538+1. Use `.env` files for local development
539+2. Use secrets management for production SERVICE_KEY
540+3. Set resource limits in container orchestration
541+4. Use health checks to monitor service availability
542+5. Implement gradual rollouts with feature flags
···1+# QuickDID Production Deployment Guide
2+3+This guide provides comprehensive instructions for deploying QuickDID in a production environment using Docker.
4+5+## Table of Contents
6+7+- [Prerequisites](#prerequisites)
8+- [Environment Configuration](#environment-configuration)
9+- [Docker Deployment](#docker-deployment)
10+- [Docker Compose Setup](#docker-compose-setup)
11+- [Health Monitoring](#health-monitoring)
12+- [Security Considerations](#security-considerations)
13+- [Troubleshooting](#troubleshooting)
14+15+## Prerequisites
16+17+- Docker 20.10.0 or higher
18+- Docker Compose 2.0.0 or higher (optional, for multi-container setup)
19+- Redis 6.0 or higher (optional, for persistent caching and queue management)
20+- Valid SSL certificates for HTTPS (recommended for production)
21+- Domain name configured with appropriate DNS records
22+23+## Environment Configuration
24+25+Create a `.env` file in your deployment directory with the following configuration:
26+27+```bash
28+# ============================================================================
29+# QuickDID Production Environment Configuration
30+# ============================================================================
31+32+# ----------------------------------------------------------------------------
33+# REQUIRED CONFIGURATION
34+# ----------------------------------------------------------------------------
35+36+# External hostname for service endpoints
37+# This should be your public domain name with port if non-standard
38+# Examples:
39+# - quickdid.example.com
40+# - quickdid.example.com:8080
41+# - localhost:3007 (for testing only)
42+HTTP_EXTERNAL=quickdid.example.com
43+44+# Private key for service identity (DID format)
45+# Generate a new key for production using atproto-identity tools
46+# SECURITY: Keep this key secure and never commit to version control
47+# Example formats:
48+# - did:key:z42tmZxD2mi1TfMKSFrsRfednwdaaPNZiiWHP4MPgcvXkDWK
49+# - did:plc:xyz123abc456
50+SERVICE_KEY=did:key:YOUR_PRODUCTION_KEY_HERE
51+52+# ----------------------------------------------------------------------------
53+# NETWORK CONFIGURATION
54+# ----------------------------------------------------------------------------
55+56+# HTTP server port (default: 8080)
57+# This is the port the service will bind to inside the container
58+# Map this to your desired external port in docker-compose.yml
59+HTTP_PORT=8080
60+61+# PLC directory hostname (default: plc.directory)
62+# Change this if using a custom PLC directory or testing environment
63+PLC_HOSTNAME=plc.directory
64+65+# ----------------------------------------------------------------------------
66+# CACHING CONFIGURATION
67+# ----------------------------------------------------------------------------
68+69+# Redis connection URL for caching (highly recommended for production)
70+# Format: redis://[username:password@]host:port/database
71+# Examples:
72+# - redis://localhost:6379/0 (local Redis, no auth)
73+# - redis://user:pass@redis.example.com:6379/0 (remote with auth)
74+# - redis://redis:6379/0 (Docker network)
75+# - rediss://secure-redis.example.com:6380/0 (TLS)
76+# Benefits: Persistent cache, distributed caching, better performance
77+REDIS_URL=redis://redis:6379/0
78+79+# TTL for in-memory cache in seconds (default: 600 = 10 minutes)
80+# Range: 60-3600 recommended
81+# Lower = fresher data, more DNS/HTTP lookups
82+# Higher = better performance, potentially stale data
83+CACHE_TTL_MEMORY=600
84+85+# TTL for Redis cache in seconds (default: 7776000 = 90 days)
86+# Range: 3600-31536000 (1 hour to 1 year)
87+# Recommendations:
88+# - 86400 (1 day) for frequently changing data
89+# - 604800 (1 week) for balanced performance
90+# - 7776000 (90 days) for stable data
91+CACHE_TTL_REDIS=86400
92+93+# ----------------------------------------------------------------------------
94+# QUEUE CONFIGURATION
95+# ----------------------------------------------------------------------------
96+97+# Queue adapter type: 'mpsc', 'redis', or 'noop' (default: mpsc)
98+# - 'mpsc': In-memory queue for single-instance deployments
99+# - 'redis': Distributed queue for multi-instance or HA deployments
100+# - 'noop': Disable queue processing (testing only)
101+QUEUE_ADAPTER=redis
102+103+# Redis URL for queue adapter (uses REDIS_URL if not set)
104+# Set this if you want to use a separate Redis instance for queuing
105+# QUEUE_REDIS_URL=redis://queue-redis:6379/1
106+107+# Redis key prefix for queues (default: queue:handleresolver:)
108+# Useful when sharing Redis instance with other services
109+QUEUE_REDIS_PREFIX=queue:quickdid:prod:
110+111+# Redis blocking timeout for queue operations in seconds (default: 5)
112+# Range: 1-60 recommended
113+# Lower = more responsive to shutdown, more polling
114+# Higher = less polling overhead, slower shutdown
115+QUEUE_REDIS_TIMEOUT=5
116+117+# Worker ID for Redis queue (auto-generated UUID if not set)
118+# Set this for predictable worker identification in multi-instance deployments
119+# Examples: worker-001, prod-us-east-1, $(hostname)
120+# QUEUE_WORKER_ID=worker-001
121+122+# Buffer size for MPSC queue (default: 1000)
123+# Range: 100-100000
124+# Increase for high-traffic deployments using MPSC adapter
125+QUEUE_BUFFER_SIZE=5000
126+127+# ----------------------------------------------------------------------------
128+# HTTP CLIENT CONFIGURATION
129+# ----------------------------------------------------------------------------
130+131+# HTTP User-Agent header
132+# Identifies your service to other AT Protocol services
133+# Default: Auto-generated with current version from Cargo.toml
134+# Format: quickdid/{version} (+https://github.com/smokesignal.events/quickdid)
135+USER_AGENT=quickdid/1.0.0-rc.1 (+https://quickdid.example.com)
136+137+# Custom DNS nameservers (comma-separated)
138+# Use for custom DNS resolution or to bypass local DNS
139+# Examples:
140+# - 8.8.8.8,8.8.4.4 (Google DNS)
141+# - 1.1.1.1,1.0.0.1 (Cloudflare DNS)
142+# DNS_NAMESERVERS=1.1.1.1,1.0.0.1
143+144+# Additional CA certificates (comma-separated file paths)
145+# Use when connecting to services with custom CA certificates
146+# CERTIFICATE_BUNDLES=/certs/custom-ca.pem,/certs/internal-ca.pem
147+148+# ----------------------------------------------------------------------------
149+# LOGGING AND MONITORING
150+# ----------------------------------------------------------------------------
151+152+# Logging level (debug, info, warn, error)
153+# Use 'info' for production, 'debug' for troubleshooting
154+RUST_LOG=info
155+156+# Structured logging format (optional)
157+# Set to 'json' for machine-readable logs
158+# RUST_LOG_FORMAT=json
159+160+# ----------------------------------------------------------------------------
161+# PERFORMANCE TUNING
162+# ----------------------------------------------------------------------------
163+164+# Tokio runtime worker threads (defaults to CPU count)
165+# Adjust based on your container's CPU allocation
166+# TOKIO_WORKER_THREADS=4
167+168+# Maximum concurrent connections (optional)
169+# Helps prevent resource exhaustion
170+# MAX_CONNECTIONS=10000
171+172+# ----------------------------------------------------------------------------
173+# DOCKER-SPECIFIC CONFIGURATION
174+# ----------------------------------------------------------------------------
175+176+# Container restart policy (for docker-compose)
177+# Options: no, always, on-failure, unless-stopped
178+RESTART_POLICY=unless-stopped
179+180+# Resource limits (for docker-compose)
181+# Adjust based on your available resources
182+MEMORY_LIMIT=512M
183+CPU_LIMIT=1.0
184+```
185+186+## Docker Deployment
187+188+### Building the Docker Image
189+190+Create a `Dockerfile` in your project root:
191+192+```dockerfile
193+# Build stage
194+FROM rust:1.75-slim AS builder
195+196+# Install build dependencies
197+RUN apt-get update && apt-get install -y \
198+ pkg-config \
199+ libssl-dev \
200+ && rm -rf /var/lib/apt/lists/*
201+202+# Create app directory
203+WORKDIR /app
204+205+# Copy source files
206+COPY Cargo.toml Cargo.lock ./
207+COPY src ./src
208+209+# Build the application
210+RUN cargo build --release
211+212+# Runtime stage
213+FROM debian:bookworm-slim
214+215+# Install runtime dependencies
216+RUN apt-get update && apt-get install -y \
217+ ca-certificates \
218+ libssl3 \
219+ curl \
220+ && rm -rf /var/lib/apt/lists/*
221+222+# Create non-root user
223+RUN useradd -m -u 1000 quickdid
224+225+# Copy binary from builder
226+COPY --from=builder /app/target/release/quickdid /usr/local/bin/quickdid
227+228+# Set ownership and permissions
229+RUN chown quickdid:quickdid /usr/local/bin/quickdid
230+231+# Switch to non-root user
232+USER quickdid
233+234+# Expose default port
235+EXPOSE 8080
236+237+# Health check
238+HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
239+ CMD curl -f http://localhost:8080/health || exit 1
240+241+# Run the application
242+ENTRYPOINT ["quickdid"]
243+```
244+245+Build the image:
246+247+```bash
248+docker build -t quickdid:latest .
249+```
250+251+### Running a Single Instance
252+253+```bash
254+# Run with environment file
255+docker run -d \
256+ --name quickdid \
257+ --env-file .env \
258+ -p 8080:8080 \
259+ --restart unless-stopped \
260+ quickdid:latest
261+```
262+263+## Docker Compose Setup
264+265+Create a `docker-compose.yml` file for a complete production setup:
266+267+```yaml
268+version: '3.8'
269+270+services:
271+ quickdid:
272+ image: quickdid:latest
273+ container_name: quickdid
274+ env_file: .env
275+ ports:
276+ - "8080:8080"
277+ depends_on:
278+ redis:
279+ condition: service_healthy
280+ networks:
281+ - quickdid-network
282+ restart: ${RESTART_POLICY:-unless-stopped}
283+ deploy:
284+ resources:
285+ limits:
286+ memory: ${MEMORY_LIMIT:-512M}
287+ cpus: ${CPU_LIMIT:-1.0}
288+ reservations:
289+ memory: 256M
290+ cpus: '0.5'
291+ healthcheck:
292+ test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
293+ interval: 30s
294+ timeout: 3s
295+ retries: 3
296+ start_period: 10s
297+ logging:
298+ driver: "json-file"
299+ options:
300+ max-size: "10m"
301+ max-file: "3"
302+303+ redis:
304+ image: redis:7-alpine
305+ container_name: quickdid-redis
306+ command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
307+ volumes:
308+ - redis-data:/data
309+ networks:
310+ - quickdid-network
311+ restart: unless-stopped
312+ healthcheck:
313+ test: ["CMD", "redis-cli", "ping"]
314+ interval: 10s
315+ timeout: 3s
316+ retries: 3
317+ logging:
318+ driver: "json-file"
319+ options:
320+ max-size: "10m"
321+ max-file: "3"
322+323+ # Optional: Nginx reverse proxy with SSL
324+ nginx:
325+ image: nginx:alpine
326+ container_name: quickdid-nginx
327+ ports:
328+ - "80:80"
329+ - "443:443"
330+ volumes:
331+ - ./nginx.conf:/etc/nginx/nginx.conf:ro
332+ - ./certs:/etc/nginx/certs:ro
333+ - ./acme-challenge:/var/www/acme:ro
334+ depends_on:
335+ - quickdid
336+ networks:
337+ - quickdid-network
338+ restart: unless-stopped
339+ logging:
340+ driver: "json-file"
341+ options:
342+ max-size: "10m"
343+ max-file: "3"
344+345+networks:
346+ quickdid-network:
347+ driver: bridge
348+349+volumes:
350+ redis-data:
351+ driver: local
352+```
353+354+### Nginx Configuration (nginx.conf)
355+356+```nginx
357+events {
358+ worker_connections 1024;
359+}
360+361+http {
362+ upstream quickdid {
363+ server quickdid:8080;
364+ }
365+366+ server {
367+ listen 80;
368+ server_name quickdid.example.com;
369+370+ # ACME challenge for Let's Encrypt
371+ location /.well-known/acme-challenge/ {
372+ root /var/www/acme;
373+ }
374+375+ # Redirect HTTP to HTTPS
376+ location / {
377+ return 301 https://$server_name$request_uri;
378+ }
379+ }
380+381+ server {
382+ listen 443 ssl http2;
383+ server_name quickdid.example.com;
384+385+ ssl_certificate /etc/nginx/certs/fullchain.pem;
386+ ssl_certificate_key /etc/nginx/certs/privkey.pem;
387+ ssl_protocols TLSv1.2 TLSv1.3;
388+ ssl_ciphers HIGH:!aNULL:!MD5;
389+390+ location / {
391+ proxy_pass http://quickdid;
392+ proxy_set_header Host $host;
393+ proxy_set_header X-Real-IP $remote_addr;
394+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
395+ proxy_set_header X-Forwarded-Proto $scheme;
396+397+ # WebSocket support (if needed)
398+ proxy_http_version 1.1;
399+ proxy_set_header Upgrade $http_upgrade;
400+ proxy_set_header Connection "upgrade";
401+402+ # Timeouts
403+ proxy_connect_timeout 60s;
404+ proxy_send_timeout 60s;
405+ proxy_read_timeout 60s;
406+ }
407+408+ # Health check endpoint
409+ location /health {
410+ proxy_pass http://quickdid/health;
411+ access_log off;
412+ }
413+ }
414+}
415+```
416+417+### Starting the Stack
418+419+```bash
420+# Start all services
421+docker-compose up -d
422+423+# View logs
424+docker-compose logs -f
425+426+# Check service status
427+docker-compose ps
428+429+# Stop all services
430+docker-compose down
431+```
432+433+## Health Monitoring
434+435+QuickDID provides health check endpoints for monitoring:
436+437+### Basic Health Check
438+439+```bash
440+curl http://quickdid.example.com/health
441+```
442+443+Expected response:
444+```json
445+{
446+ "status": "healthy",
447+ "version": "1.0.0",
448+ "uptime_seconds": 3600
449+}
450+```
451+452+### Monitoring with Prometheus (Optional)
453+454+Add to your `docker-compose.yml`:
455+456+```yaml
457+ prometheus:
458+ image: prom/prometheus:latest
459+ container_name: quickdid-prometheus
460+ volumes:
461+ - ./prometheus.yml:/etc/prometheus/prometheus.yml
462+ - prometheus-data:/prometheus
463+ command:
464+ - '--config.file=/etc/prometheus/prometheus.yml'
465+ - '--storage.tsdb.path=/prometheus'
466+ ports:
467+ - "9090:9090"
468+ networks:
469+ - quickdid-network
470+ restart: unless-stopped
471+472+volumes:
473+ prometheus-data:
474+ driver: local
475+```
476+477+## Security Considerations
478+479+### 1. Service Key Protection
480+481+- **Never commit** the `SERVICE_KEY` to version control
482+- Store keys in a secure secret management system (e.g., HashiCorp Vault, AWS Secrets Manager)
483+- Rotate keys regularly
484+- Use different keys for different environments
485+486+### 2. Network Security
487+488+- Use HTTPS in production with valid SSL certificates
489+- Implement rate limiting at the reverse proxy level
490+- Use firewall rules to restrict access to Redis
491+- Enable Redis authentication in production
492+493+### 3. Container Security
494+495+- Run containers as non-root user (already configured in Dockerfile)
496+- Keep base images updated
497+- Scan images for vulnerabilities regularly
498+- Use read-only filesystems where possible
499+500+### 4. Redis Security
501+502+```bash
503+# Add to Redis configuration for production
504+requirepass your_strong_password_here
505+maxclients 10000
506+timeout 300
507+```
508+509+### 5. Environment Variables
510+511+- Use Docker secrets or external secret management
512+- Avoid logging sensitive environment variables
513+- Implement proper access controls
514+515+## Troubleshooting
516+517+### Common Issues and Solutions
518+519+#### 1. Container Won't Start
520+521+```bash
522+# Check logs
523+docker logs quickdid
524+525+# Verify environment variables
526+docker exec quickdid env | grep -E "HTTP_EXTERNAL|SERVICE_KEY"
527+528+# Test Redis connectivity
529+docker exec quickdid redis-cli -h redis ping
530+```
531+532+#### 2. Handle Resolution Failures
533+534+```bash
535+# Enable debug logging
536+docker exec quickdid sh -c "export RUST_LOG=debug"
537+538+# Check DNS resolution
539+docker exec quickdid nslookup plc.directory
540+541+# Verify Redis cache
542+docker exec -it quickdid-redis redis-cli
543+> KEYS handle:*
544+> TTL handle:example_key
545+```
546+547+#### 3. Performance Issues
548+549+```bash
550+# Monitor Redis memory usage
551+docker exec quickdid-redis redis-cli INFO memory
552+553+# Check container resource usage
554+docker stats quickdid
555+556+# Analyze slow queries (with debug logging)
557+docker logs quickdid | grep "resolution took"
558+```
559+560+#### 4. Health Check Failures
561+562+```bash
563+# Manual health check
564+docker exec quickdid curl -v http://localhost:8080/health
565+566+# Check service binding
567+docker exec quickdid netstat -tlnp | grep 8080
568+```
569+570+### Debugging Commands
571+572+```bash
573+# Interactive shell in container
574+docker exec -it quickdid /bin/bash
575+576+# Test handle resolution
577+curl "http://localhost:8080/xrpc/com.atproto.identity.resolveHandle?handle=example.bsky.social"
578+579+# Check Redis keys
580+docker exec quickdid-redis redis-cli --scan --pattern "handle:*" | head -20
581+582+# Monitor real-time logs
583+docker-compose logs -f quickdid | grep -E "ERROR|WARN"
584+```
585+586+## Maintenance
587+588+### Backup and Restore
589+590+```bash
591+# Backup Redis data
592+docker exec quickdid-redis redis-cli BGSAVE
593+docker cp quickdid-redis:/data/dump.rdb ./backups/redis-$(date +%Y%m%d).rdb
594+595+# Restore Redis data
596+docker cp ./backups/redis-backup.rdb quickdid-redis:/data/dump.rdb
597+docker restart quickdid-redis
598+```
599+600+### Updates and Rollbacks
601+602+```bash
603+# Update to new version
604+docker pull quickdid:new-version
605+docker-compose down
606+docker-compose up -d
607+608+# Rollback if needed
609+docker-compose down
610+docker tag quickdid:previous quickdid:latest
611+docker-compose up -d
612+```
613+614+### Log Rotation
615+616+Configure Docker's built-in log rotation in `/etc/docker/daemon.json`:
617+618+```json
619+{
620+ "log-driver": "json-file",
621+ "log-opts": {
622+ "max-size": "10m",
623+ "max-file": "3"
624+ }
625+}
626+```
627+628+## Performance Optimization
629+630+### Redis Optimization
631+632+```redis
633+# Add to redis.conf or pass as command arguments
634+maxmemory 2gb
635+maxmemory-policy allkeys-lru
636+save "" # Disable persistence for cache-only usage
637+tcp-keepalive 300
638+timeout 0
639+```
640+641+### System Tuning
642+643+```bash
644+# Add to host system's /etc/sysctl.conf
645+net.core.somaxconn = 1024
646+net.ipv4.tcp_tw_reuse = 1
647+net.ipv4.ip_local_port_range = 10000 65000
648+fs.file-max = 100000
649+```
650+651+## Configuration Validation
652+653+QuickDID validates all configuration at startup. The following rules are enforced:
654+655+### Required Fields
656+657+- **HTTP_EXTERNAL**: Must be provided
658+- **SERVICE_KEY**: Must be provided
659+660+### Value Constraints
661+662+1. **TTL Values** (`CACHE_TTL_MEMORY`, `CACHE_TTL_REDIS`):
663+ - Must be positive integers (> 0)
664+ - Recommended minimum: 60 seconds
665+666+2. **Timeout Values** (`QUEUE_REDIS_TIMEOUT`):
667+ - Must be positive integers (> 0)
668+ - Recommended range: 1-60 seconds
669+670+3. **Queue Adapter** (`QUEUE_ADAPTER`):
671+ - Must be one of: `mpsc`, `redis`, `noop`, `none`
672+ - Case-sensitive
673+674+### Validation Errors
675+676+If validation fails, QuickDID will exit with one of these error codes:
677+678+- `error-quickdid-config-1`: Missing required environment variable
679+- `error-quickdid-config-2`: Invalid configuration value
680+- `error-quickdid-config-3`: Invalid TTL value (must be positive)
681+- `error-quickdid-config-4`: Invalid timeout value (must be positive)
682+683+### Testing Configuration
684+685+```bash
686+# Validate configuration without starting service
687+HTTP_EXTERNAL=test SERVICE_KEY=test quickdid --help
688+689+# Test with specific values (will fail validation)
690+CACHE_TTL_MEMORY=0 quickdid --help
691+692+# Debug configuration parsing
693+RUST_LOG=debug HTTP_EXTERNAL=test SERVICE_KEY=test quickdid
694+```
695+696+## Support and Resources
697+698+- **Documentation**: [QuickDID GitHub Repository](https://github.com/smokesignal.events/quickdid)
699+- **Configuration Reference**: See [configuration-reference.md](./configuration-reference.md) for detailed documentation of all options
700+- **AT Protocol Specs**: [atproto.com](https://atproto.com)
701+- **Issues**: Report bugs via GitHub Issues
702+- **Community**: Join the AT Protocol Discord server
703+704+## License
705+706+QuickDID is licensed under the MIT License. See LICENSE file for details.
+27-7
src/bin/quickdid.rs
···2use async_trait::async_trait;
3use atproto_identity::{
4 config::{CertificateBundles, DnsNameservers},
5- key::{identify_key, to_public, KeyData, KeyProvider},
6 resolve::HickoryDnsResolver,
7};
8use clap::Parser;
···58 let args = Args::parse();
59 let config = Config::from_args(args)?;
6000061 tracing::info!("Starting QuickDID service on port {}", config.http_port);
62 tracing::info!("Service DID: {}", config.service_did);
000006364 // Parse certificate bundles if provided
65 let certificate_bundles: CertificateBundles = config
···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))
0000000140 } else {
141- tracing::info!("Using in-memory handle resolver with 10-minute cache TTL");
000142 Arc::new(CachingHandleResolver::new(
143 base_handle_resolver,
144- 600, // 10 minutes TTL for in-memory cache
145 ))
146 };
147···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) => {
···192 }
193 },
194 None => {
195- tracing::warn!("Redis queue adapter requested but no Redis URL configured, using no-op adapter");
00196 Arc::new(NoopQueueAdapter::<HandleResolutionWork>::new())
197 }
198 }
···2use async_trait::async_trait;
3use atproto_identity::{
4 config::{CertificateBundles, DnsNameservers},
5+ key::{KeyData, KeyProvider, identify_key, to_public},
6 resolve::HickoryDnsResolver,
7};
8use clap::Parser;
···58 let args = Args::parse();
59 let config = Config::from_args(args)?;
6061+ // Validate configuration
62+ config.validate()?;
63+64 tracing::info!("Starting QuickDID service on port {}", config.http_port);
65 tracing::info!("Service DID: {}", config.service_did);
66+ tracing::info!(
67+ "Cache TTL - Memory: {}s, Redis: {}s",
68+ config.cache_ttl_memory,
69+ config.cache_ttl_redis
70+ );
7172 // Parse certificate bundles if provided
73 let certificate_bundles: CertificateBundles = config
···143 // Create handle resolver with Redis caching if available, otherwise use in-memory caching
144 let handle_resolver: Arc<dyn quickdid::handle_resolver::HandleResolver> =
145 if let Some(pool) = redis_pool {
146+ tracing::info!(
147+ "Using Redis-backed handle resolver with {}-second cache TTL",
148+ config.cache_ttl_redis
149+ );
150+ Arc::new(RedisHandleResolver::with_ttl(
151+ base_handle_resolver,
152+ pool,
153+ config.cache_ttl_redis,
154+ ))
155 } else {
156+ tracing::info!(
157+ "Using in-memory handle resolver with {}-second cache TTL",
158+ config.cache_ttl_memory
159+ );
160 Arc::new(CachingHandleResolver::new(
161 base_handle_resolver,
162+ config.cache_ttl_memory,
163 ))
164 };
165···192 pool,
193 config.queue_worker_id.clone(),
194 config.queue_redis_prefix.clone(),
195+ config.queue_redis_timeout, // Configurable timeout for blocking operations
196 ))
197 }
198 Err(e) => {
···210 }
211 },
212 None => {
213+ tracing::warn!(
214+ "Redis queue adapter requested but no Redis URL configured, using no-op adapter"
215+ );
216 Arc::new(NoopQueueAdapter::<HandleResolutionWork>::new())
217 }
218 }
+18-3
src/cache.rs
···1//! Redis cache utilities for QuickDID
23-use anyhow::Result;
4use deadpool_redis::{Config, Pool, Runtime};
0000000000000056/// 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))?;
0010 Ok(pool)
11}
···1//! Redis cache utilities for QuickDID
203use deadpool_redis::{Config, Pool, Runtime};
4+use thiserror::Error;
5+6+/// Cache-specific errors following the QuickDID error format
7+#[derive(Debug, Error)]
8+pub enum CacheError {
9+ #[error("error-quickdid-cache-1 Redis pool creation failed: {0}")]
10+ PoolCreationFailed(String),
11+12+ #[error("error-quickdid-cache-2 Invalid Redis URL: {0}")]
13+ InvalidRedisUrl(String),
14+15+ #[error("error-quickdid-cache-3 Redis connection failed: {0}")]
16+ ConnectionFailed(String),
17+}
1819/// Create a Redis connection pool from a Redis URL
20+pub fn create_redis_pool(redis_url: &str) -> Result<Pool, CacheError> {
21 let config = Config::from_url(redis_url);
22+ let pool = config
23+ .create_pool(Some(Runtime::Tokio1))
24+ .map_err(|e| CacheError::PoolCreationFailed(e.to_string()))?;
25 Ok(pool)
26}
···1+//! Configuration management for QuickDID service
2+//!
3+//! This module handles all configuration parsing, validation, and error handling
4+//! for the QuickDID AT Protocol identity resolution service.
5+//!
6+//! ## Configuration Sources
7+//!
8+//! Configuration can be provided through:
9+//! - Environment variables (highest priority)
10+//! - Command-line arguments
11+//! - Default values (lowest priority)
12+//!
13+//! ## Example
14+//!
15+//! ```bash
16+//! # Minimal configuration
17+//! HTTP_EXTERNAL=quickdid.example.com \
18+//! SERVICE_KEY=did:key:z42tmZxD2mi1TfMKSFrsRfednwdaaPNZiiWHP4MPgcvXkDWK \
19+//! quickdid
20+//!
21+//! # Full configuration with Redis and custom settings
22+//! HTTP_EXTERNAL=quickdid.example.com \
23+//! SERVICE_KEY=did:key:z42tmZxD2mi1TfMKSFrsRfednwdaaPNZiiWHP4MPgcvXkDWK \
24+//! HTTP_PORT=3000 \
25+//! REDIS_URL=redis://localhost:6379 \
26+//! CACHE_TTL_MEMORY=300 \
27+//! CACHE_TTL_REDIS=86400 \
28+//! QUEUE_ADAPTER=redis \
29+//! QUEUE_REDIS_TIMEOUT=10 \
30+//! quickdid
31+//! ```
32+33use atproto_identity::config::optional_env;
34use clap::Parser;
35+use thiserror::Error;
36+37+/// Configuration-specific errors following the QuickDID error format
38+///
39+/// All errors follow the pattern: `error-quickdid-config-{number} {message}: {details}`
40+#[derive(Debug, Error)]
41+pub enum ConfigError {
42+ /// Missing required environment variable or command-line argument
43+ ///
44+ /// Example: When SERVICE_KEY or HTTP_EXTERNAL are not provided
45+ #[error("error-quickdid-config-1 Missing required environment variable: {0}")]
46+ MissingRequired(String),
47+48+ /// Invalid configuration value that doesn't meet expected format or constraints
49+ ///
50+ /// Example: Invalid QUEUE_ADAPTER value (must be 'mpsc', 'redis', or 'noop')
51+ #[error("error-quickdid-config-2 Invalid configuration value: {0}")]
52+ InvalidValue(String),
53+54+ /// Invalid TTL (Time To Live) value
55+ ///
56+ /// TTL values must be positive integers representing seconds
57+ #[error("error-quickdid-config-3 Invalid TTL value (must be positive): {0}")]
58+ InvalidTtl(String),
59+60+ /// Invalid timeout value
61+ ///
62+ /// Timeout values must be positive integers representing seconds
63+ #[error("error-quickdid-config-4 Invalid timeout value (must be positive): {0}")]
64+ InvalidTimeout(String),
65+}
6667#[derive(Parser, Clone)]
68#[command(
···84 HTTP_EXTERNAL External hostname for service endpoints (required)
85 HTTP_PORT HTTP server port (default: 8080)
86 PLC_HOSTNAME PLC directory hostname (default: plc.directory)
87+ USER_AGENT HTTP User-Agent header (auto-generated with version)
88+ DNS_NAMESERVERS Custom DNS nameservers (comma-separated IPs)
89+ CERTIFICATE_BUNDLES Additional CA certificates (comma-separated paths)
90+91+ CACHING:
92 REDIS_URL Redis URL for handle resolution caching (optional)
93+ CACHE_TTL_MEMORY TTL for in-memory cache in seconds (default: 600)
94+ CACHE_TTL_REDIS TTL for Redis cache in seconds (default: 7776000 = 90 days)
95+96+ QUEUE CONFIGURATION:
97+ QUEUE_ADAPTER Queue adapter: 'mpsc', 'redis', 'noop' (default: mpsc)
98 QUEUE_REDIS_URL Redis URL for queue adapter (uses REDIS_URL if not set)
99 QUEUE_REDIS_PREFIX Redis key prefix for queues (default: queue:handleresolver:)
100+ QUEUE_REDIS_TIMEOUT Queue blocking timeout in seconds (default: 5)
101+ QUEUE_WORKER_ID Worker ID for Redis queue (auto-generated UUID if not set)
102 QUEUE_BUFFER_SIZE Buffer size for MPSC queue (default: 1000)
103"
104)]
105+/// Command-line arguments and environment variables configuration
106pub struct Args {
107+ /// HTTP server port to bind to
108+ ///
109+ /// Examples: "8080", "3000", "80"
110+ /// Constraints: Must be a valid port number (1-65535)
111 #[arg(long, env = "HTTP_PORT", default_value = "8080")]
112 pub http_port: String,
113114+ /// PLC directory hostname for DID resolution
115+ ///
116+ /// Examples: "plc.directory", "test.plc.directory"
117+ /// Use "plc.directory" for production
118 #[arg(long, env = "PLC_HOSTNAME", default_value = "plc.directory")]
119 pub plc_hostname: String,
120121+ /// External hostname for service endpoints (REQUIRED)
122+ ///
123+ /// Examples:
124+ /// - "quickdid.example.com" (standard)
125+ /// - "quickdid.example.com:8080" (with port)
126+ /// - "localhost:3007" (development)
127 #[arg(long, env = "HTTP_EXTERNAL")]
128 pub http_external: Option<String>,
129130+ /// Private key for service identity in DID format (REQUIRED)
131+ ///
132+ /// Examples:
133+ /// - "did:key:z42tmZxD2mi1TfMKSFrsRfednwdaaPNZiiWHP4MPgcvXkDWK"
134+ /// - "did:plc:xyz123abc456"
135+ ///
136+ /// SECURITY: Keep this key secure and never commit to version control
137 #[arg(long, env = "SERVICE_KEY")]
138 pub service_key: Option<String>,
139140+ /// HTTP User-Agent header for outgoing requests
141+ ///
142+ /// Example: `quickdid/1.0.0 (+https://quickdid.example.com)`
143+ /// Default: Auto-generated with current version
144 #[arg(long, env = "USER_AGENT")]
145 pub user_agent: Option<String>,
146147+ /// Custom DNS nameservers (comma-separated IP addresses)
148+ ///
149+ /// Examples:
150+ /// - "8.8.8.8,8.8.4.4" (Google DNS)
151+ /// - "1.1.1.1,1.0.0.1" (Cloudflare DNS)
152+ /// - "192.168.1.1" (Local DNS)
153 #[arg(long, env = "DNS_NAMESERVERS")]
154 pub dns_nameservers: Option<String>,
155156+ /// Additional CA certificates (comma-separated file paths)
157+ ///
158+ /// Examples:
159+ /// - "/etc/ssl/certs/custom-ca.pem"
160+ /// - "/certs/ca1.pem,/certs/ca2.pem"
161+ ///
162+ /// Use for custom or internal certificate authorities
163 #[arg(long, env = "CERTIFICATE_BUNDLES")]
164 pub certificate_bundles: Option<String>,
165166+ /// Redis connection URL for caching
167+ ///
168+ /// Examples:
169+ /// - "redis://localhost:6379/0" (local, no auth)
170+ /// - "redis://user:pass@redis.example.com:6379/0" (with auth)
171+ /// - "rediss://secure-redis.example.com:6380/0" (TLS)
172+ ///
173+ /// Benefits: Persistent cache, distributed caching, better performance
174 #[arg(long, env = "REDIS_URL")]
175 pub redis_url: Option<String>,
176177+ /// Queue adapter type for background processing
178+ ///
179+ /// Values:
180+ /// - "mpsc": In-memory multi-producer single-consumer queue
181+ /// - "redis": Redis-backed distributed queue
182+ /// - "noop": Disable queue processing (for testing)
183+ ///
184+ /// Default: "mpsc" for single-instance deployments
185 #[arg(long, env = "QUEUE_ADAPTER", default_value = "mpsc")]
186 pub queue_adapter: String,
187188+ /// Redis URL specifically for queue operations
189+ ///
190+ /// Falls back to REDIS_URL if not specified
191+ /// Use when separating cache and queue Redis instances
192 #[arg(long, env = "QUEUE_REDIS_URL")]
193 pub queue_redis_url: Option<String>,
194195+ /// Redis key prefix for queue operations
196+ ///
197+ /// Examples:
198+ /// - "queue:handleresolver:" (default)
199+ /// - "prod:queue:hr:" (environment-specific)
200+ /// - "quickdid:v1:queue:" (version-specific)
201+ ///
202+ /// Use to namespace queues when sharing Redis
203 #[arg(
204 long,
205 env = "QUEUE_REDIS_PREFIX",
···207 )]
208 pub queue_redis_prefix: String,
209210+ /// Worker ID for Redis queue operations
211+ ///
212+ /// Examples: "worker-001", "prod-us-east-1", "quickdid-1"
213+ /// Default: Auto-generated UUID
214+ ///
215+ /// Use for identifying specific workers in logs
216 #[arg(long, env = "QUEUE_WORKER_ID")]
217 pub queue_worker_id: Option<String>,
218219+ /// Buffer size for MPSC queue
220+ ///
221+ /// Range: 100-100000 (recommended)
222+ /// Default: 1000
223+ ///
224+ /// Increase for high-traffic deployments
225 #[arg(long, env = "QUEUE_BUFFER_SIZE", default_value = "1000")]
226 pub queue_buffer_size: usize,
227+228+ /// TTL for in-memory cache in seconds
229+ ///
230+ /// Range: 60-3600 (recommended)
231+ /// Default: 600 (10 minutes)
232+ ///
233+ /// Lower values = fresher data, more resolution requests
234+ /// Higher values = better performance, potentially stale data
235+ #[arg(long, env = "CACHE_TTL_MEMORY", default_value = "600")]
236+ pub cache_ttl_memory: u64,
237+238+ /// TTL for Redis cache in seconds
239+ ///
240+ /// Range: 3600-31536000 (1 hour to 1 year)
241+ /// Default: 7776000 (90 days)
242+ ///
243+ /// Recommendation: 86400 (1 day) for frequently changing data
244+ #[arg(long, env = "CACHE_TTL_REDIS", default_value = "7776000")]
245+ pub cache_ttl_redis: u64,
246+247+ /// Redis blocking timeout for queue operations in seconds
248+ ///
249+ /// Range: 1-60 (recommended)
250+ /// Default: 5
251+ ///
252+ /// Lower values = more responsive to shutdown
253+ /// Higher values = less Redis polling overhead
254+ #[arg(long, env = "QUEUE_REDIS_TIMEOUT", default_value = "5")]
255+ pub queue_redis_timeout: u64,
256}
257258+/// Validated configuration for QuickDID service
259+///
260+/// This struct contains all configuration after validation and processing.
261+/// Use `Config::from_args()` to create from command-line arguments and environment variables.
262+///
263+/// ## Example
264+///
265+/// ```rust,no_run
266+/// use quickdid::config::{Args, Config};
267+/// use clap::Parser;
268+///
269+/// let args = Args::parse();
270+/// let config = Config::from_args(args)?;
271+/// config.validate()?;
272+///
273+/// println!("Service running at: {}", config.http_external);
274+/// println!("Service DID: {}", config.service_did);
275+/// ```
276#[derive(Clone)]
277pub struct Config {
278+ /// HTTP server port (e.g., "8080", "3000")
279 pub http_port: String,
280+281+ /// PLC directory hostname (e.g., "plc.directory")
282 pub plc_hostname: String,
283+284+ /// External hostname for service endpoints (e.g., "quickdid.example.com")
285 pub http_external: String,
286+287+ /// Private key for service identity (e.g., "did:key:z42tm...")
288 pub service_key: String,
289+290+ /// HTTP User-Agent for outgoing requests (e.g., "quickdid/1.0.0 (+https://...)")
291 pub user_agent: String,
292+293+ /// Derived service DID (e.g., "did:web:quickdid.example.com")
294+ /// Automatically generated from http_external with proper encoding
295 pub service_did: String,
296+297+ /// Custom DNS nameservers, comma-separated (e.g., "8.8.8.8,8.8.4.4")
298 pub dns_nameservers: Option<String>,
299+300+ /// Additional CA certificate bundles, comma-separated paths
301 pub certificate_bundles: Option<String>,
302+303+ /// Redis URL for caching (e.g., "redis://localhost:6379/0")
304 pub redis_url: Option<String>,
305+306+ /// Queue adapter type: "mpsc", "redis", or "noop"
307 pub queue_adapter: String,
308+309+ /// Redis URL for queue operations (falls back to redis_url)
310 pub queue_redis_url: Option<String>,
311+312+ /// Redis key prefix for queues (e.g., "queue:handleresolver:")
313 pub queue_redis_prefix: String,
314+315+ /// Worker ID for queue operations (auto-generated if not set)
316 pub queue_worker_id: Option<String>,
317+318+ /// Buffer size for MPSC queue (e.g., 1000)
319 pub queue_buffer_size: usize,
320+321+ /// TTL for in-memory cache in seconds (e.g., 600 = 10 minutes)
322+ pub cache_ttl_memory: u64,
323+324+ /// TTL for Redis cache in seconds (e.g., 7776000 = 90 days)
325+ pub cache_ttl_redis: u64,
326+327+ /// Redis blocking timeout for queue operations in seconds (e.g., 5)
328+ pub queue_redis_timeout: u64,
329}
330331impl Config {
332+ /// Create a validated Config from command-line arguments and environment variables
333+ ///
334+ /// This method:
335+ /// 1. Processes command-line arguments with environment variable fallbacks
336+ /// 2. Validates required fields (HTTP_EXTERNAL and SERVICE_KEY)
337+ /// 3. Generates derived values (service_did from http_external)
338+ /// 4. Applies defaults where appropriate
339+ ///
340+ /// ## Priority Order
341+ ///
342+ /// 1. Command-line arguments (highest priority)
343+ /// 2. Environment variables
344+ /// 3. Default values (lowest priority)
345+ ///
346+ /// ## Example
347+ ///
348+ /// ```rust,no_run
349+ /// use quickdid::config::{Args, Config};
350+ /// use clap::Parser;
351+ ///
352+ /// // Parse from environment and command-line
353+ /// let args = Args::parse();
354+ /// let config = Config::from_args(args)?;
355+ ///
356+ /// // The service DID is automatically generated from HTTP_EXTERNAL
357+ /// assert!(config.service_did.starts_with("did:web:"));
358+ /// ```
359+ ///
360+ /// ## Errors
361+ ///
362+ /// Returns `ConfigError::MissingRequired` if:
363+ /// - HTTP_EXTERNAL is not provided
364+ /// - SERVICE_KEY is not provided
365+ pub fn from_args(args: Args) -> Result<Self, ConfigError> {
366 let http_external = args
367 .http_external
368 .or_else(|| {
···373 Some(env_val)
374 }
375 })
376+ .ok_or_else(|| ConfigError::MissingRequired("HTTP_EXTERNAL".to_string()))?;
377378 let service_key = args
379 .service_key
···385 Some(env_val)
386 }
387 })
388+ .ok_or_else(|| ConfigError::MissingRequired("SERVICE_KEY".to_string()))?;
389390+ let default_user_agent = format!(
391+ "quickdid/{} (+https://github.com/smokesignal.events/quickdid)",
392+ env!("CARGO_PKG_VERSION")
393+ );
394395 let user_agent = args
396 .user_agent
···461 }
462 }),
463 queue_buffer_size: args.queue_buffer_size,
464+ cache_ttl_memory: args.cache_ttl_memory,
465+ cache_ttl_redis: args.cache_ttl_redis,
466+ queue_redis_timeout: args.queue_redis_timeout,
467 })
468+ }
469+470+ /// Validate the configuration for correctness and consistency
471+ ///
472+ /// Checks:
473+ /// - Cache TTL values are positive (> 0)
474+ /// - Queue timeout is positive (> 0)
475+ /// - Queue adapter is a valid value ('mpsc', 'redis', 'noop', 'none')
476+ ///
477+ /// ## Example
478+ ///
479+ /// ```rust,no_run
480+ /// let config = Config::from_args(args)?;
481+ /// config.validate()?; // Ensures all values are valid
482+ /// ```
483+ ///
484+ /// ## Errors
485+ ///
486+ /// Returns `ConfigError::InvalidTtl` if TTL values are 0 or negative
487+ /// Returns `ConfigError::InvalidTimeout` if timeout values are 0 or negative
488+ /// Returns `ConfigError::InvalidValue` if queue adapter is invalid
489+ pub fn validate(&self) -> Result<(), ConfigError> {
490+ if self.cache_ttl_memory == 0 {
491+ return Err(ConfigError::InvalidTtl(
492+ "CACHE_TTL_MEMORY must be > 0".to_string(),
493+ ));
494+ }
495+ if self.cache_ttl_redis == 0 {
496+ return Err(ConfigError::InvalidTtl(
497+ "CACHE_TTL_REDIS must be > 0".to_string(),
498+ ));
499+ }
500+ if self.queue_redis_timeout == 0 {
501+ return Err(ConfigError::InvalidTimeout(
502+ "QUEUE_REDIS_TIMEOUT must be > 0".to_string(),
503+ ));
504+ }
505+ match self.queue_adapter.as_str() {
506+ "mpsc" | "redis" | "noop" | "none" => {}
507+ _ => {
508+ return Err(ConfigError::InvalidValue(format!(
509+ "Invalid QUEUE_ADAPTER '{}', must be 'mpsc', 'redis', or 'noop'",
510+ self.queue_adapter
511+ )));
512+ }
513+ }
514+ Ok(())
515 }
516}
+61-13
src/handle_resolution_result.rs
···67use serde::{Deserialize, Serialize};
8use std::time::{SystemTime, UNIX_EPOCH};
00000000000000910/// Represents the type of DID method from a resolution
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
···3435impl HandleResolutionResult {
36 /// Create a new resolution result for a successfully resolved handle
37- pub fn success(did: &str) -> Self {
0000000000000000038 let timestamp = SystemTime::now()
39 .duration_since(UNIX_EPOCH)
40 .expect("Time went backwards")
···50 }
5152 /// Create a new resolution result for a failed resolution
53- pub fn not_resolved() -> Self {
00000000000000054 let timestamp = SystemTime::now()
55 .duration_since(UNIX_EPOCH)
56 .expect("Time went backwards")
···108 }
109110 /// Serialize the result to bytes using bincode
111- pub fn to_bytes(&self) -> Result<Vec<u8>, bincode::error::EncodeError> {
112 bincode::serde::encode_to_vec(self, bincode::config::standard())
0113 }
114115 /// Deserialize from bytes using bincode
116- pub fn from_bytes(bytes: &[u8]) -> Result<Self, bincode::error::DecodeError> {
117 bincode::serde::decode_from_slice(bytes, bincode::config::standard())
118 .map(|(result, _)| result)
0119 }
120}
121···126 #[test]
127 fn test_parse_did_web() {
128 let did = "did:web:example.com";
129- let result = HandleResolutionResult::success(did);
130 assert_eq!(result.method_type, DidMethodType::Web);
131 assert_eq!(result.payload, "example.com");
132 assert_eq!(result.to_did(), Some(did.to_string()));
···135 #[test]
136 fn test_parse_did_plc() {
137 let did = "did:plc:abcdef123456";
138- let result = HandleResolutionResult::success(did);
139 assert_eq!(result.method_type, DidMethodType::Plc);
140 assert_eq!(result.payload, "abcdef123456");
141 assert_eq!(result.to_did(), Some(did.to_string()));
···144 #[test]
145 fn test_parse_did_other() {
146 let did = "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK";
147- let result = HandleResolutionResult::success(did);
148 assert_eq!(result.method_type, DidMethodType::Other);
149 assert_eq!(result.payload, did);
150 assert_eq!(result.to_did(), Some(did.to_string()));
···152153 #[test]
154 fn test_not_resolved() {
155- let result = HandleResolutionResult::not_resolved();
156 assert_eq!(result.method_type, DidMethodType::NotResolved);
157 assert_eq!(result.payload, "");
158 assert_eq!(result.to_did(), None);
···209 );
210211 // Verify the size for not resolved (should be minimal)
212- let not_resolved = HandleResolutionResult::not_resolved();
213 let bytes = not_resolved.to_bytes().unwrap();
214 assert!(
215 bytes.len() < 50,
···221 #[test]
222 fn test_did_web_with_port() {
223 let did = "did:web:localhost:3000";
224- let result = HandleResolutionResult::success(did);
225 assert_eq!(result.method_type, DidMethodType::Web);
226 assert_eq!(result.payload, "localhost:3000");
227 assert_eq!(result.to_did(), Some(did.to_string()));
···230 #[test]
231 fn test_did_web_with_path() {
232 let did = "did:web:example.com:path:to:did";
233- let result = HandleResolutionResult::success(did);
234 assert_eq!(result.method_type, DidMethodType::Web);
235 assert_eq!(result.payload, "example.com:path:to:did");
236 assert_eq!(result.to_did(), Some(did.to_string()));
···239 #[test]
240 fn test_invalid_did_format() {
241 let did = "not-a-did";
242- let result = HandleResolutionResult::success(did);
243 assert_eq!(result.method_type, DidMethodType::Other);
244 assert_eq!(result.payload, did);
245 assert_eq!(result.to_did(), Some(did.to_string()));
···258 ];
259260 for (did, expected_type, expected_payload) in test_cases {
261- let result = HandleResolutionResult::success(did);
262 assert_eq!(result.method_type, expected_type);
263 assert_eq!(result.payload, expected_payload);
264
···67use serde::{Deserialize, Serialize};
8use std::time::{SystemTime, UNIX_EPOCH};
9+use thiserror::Error;
10+11+/// Errors that can occur during handle resolution result operations
12+#[derive(Debug, Error)]
13+pub enum HandleResolutionError {
14+ #[error("error-quickdid-resolution-1 System time error: {0}")]
15+ SystemTimeError(String),
16+17+ #[error("error-quickdid-serialization-1 Failed to serialize resolution result: {0}")]
18+ SerializationError(String),
19+20+ #[error("error-quickdid-serialization-2 Failed to deserialize resolution result: {0}")]
21+ DeserializationError(String),
22+}
2324/// Represents the type of DID method from a resolution
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
···4849impl HandleResolutionResult {
50 /// Create a new resolution result for a successfully resolved handle
51+ pub fn success(did: &str) -> Result<Self, HandleResolutionError> {
52+ let timestamp = SystemTime::now()
53+ .duration_since(UNIX_EPOCH)
54+ .map_err(|e| HandleResolutionError::SystemTimeError(e.to_string()))?
55+ .as_secs();
56+57+ let (method_type, payload) = Self::parse_did(did);
58+59+ Ok(Self {
60+ timestamp,
61+ method_type,
62+ payload,
63+ })
64+ }
65+66+ /// Create a new resolution result for a successfully resolved handle (unsafe version for compatibility)
67+ /// This version panics if system time is invalid and should only be used in tests
68+ pub fn success_unchecked(did: &str) -> Self {
69 let timestamp = SystemTime::now()
70 .duration_since(UNIX_EPOCH)
71 .expect("Time went backwards")
···81 }
8283 /// Create a new resolution result for a failed resolution
84+ pub fn not_resolved() -> Result<Self, HandleResolutionError> {
85+ let timestamp = SystemTime::now()
86+ .duration_since(UNIX_EPOCH)
87+ .map_err(|e| HandleResolutionError::SystemTimeError(e.to_string()))?
88+ .as_secs();
89+90+ Ok(Self {
91+ timestamp,
92+ method_type: DidMethodType::NotResolved,
93+ payload: String::new(),
94+ })
95+ }
96+97+ /// Create a new resolution result for a failed resolution (unsafe version for compatibility)
98+ /// This version panics if system time is invalid and should only be used in tests
99+ pub fn not_resolved_unchecked() -> Self {
100 let timestamp = SystemTime::now()
101 .duration_since(UNIX_EPOCH)
102 .expect("Time went backwards")
···154 }
155156 /// Serialize the result to bytes using bincode
157+ pub fn to_bytes(&self) -> Result<Vec<u8>, HandleResolutionError> {
158 bincode::serde::encode_to_vec(self, bincode::config::standard())
159+ .map_err(|e| HandleResolutionError::SerializationError(e.to_string()))
160 }
161162 /// Deserialize from bytes using bincode
163+ pub fn from_bytes(bytes: &[u8]) -> Result<Self, HandleResolutionError> {
164 bincode::serde::decode_from_slice(bytes, bincode::config::standard())
165 .map(|(result, _)| result)
166+ .map_err(|e| HandleResolutionError::DeserializationError(e.to_string()))
167 }
168}
169···174 #[test]
175 fn test_parse_did_web() {
176 let did = "did:web:example.com";
177+ let result = HandleResolutionResult::success_unchecked(did);
178 assert_eq!(result.method_type, DidMethodType::Web);
179 assert_eq!(result.payload, "example.com");
180 assert_eq!(result.to_did(), Some(did.to_string()));
···183 #[test]
184 fn test_parse_did_plc() {
185 let did = "did:plc:abcdef123456";
186+ let result = HandleResolutionResult::success_unchecked(did);
187 assert_eq!(result.method_type, DidMethodType::Plc);
188 assert_eq!(result.payload, "abcdef123456");
189 assert_eq!(result.to_did(), Some(did.to_string()));
···192 #[test]
193 fn test_parse_did_other() {
194 let did = "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK";
195+ let result = HandleResolutionResult::success_unchecked(did);
196 assert_eq!(result.method_type, DidMethodType::Other);
197 assert_eq!(result.payload, did);
198 assert_eq!(result.to_did(), Some(did.to_string()));
···200201 #[test]
202 fn test_not_resolved() {
203+ let result = HandleResolutionResult::not_resolved_unchecked();
204 assert_eq!(result.method_type, DidMethodType::NotResolved);
205 assert_eq!(result.payload, "");
206 assert_eq!(result.to_did(), None);
···257 );
258259 // Verify the size for not resolved (should be minimal)
260+ let not_resolved = HandleResolutionResult::not_resolved_unchecked();
261 let bytes = not_resolved.to_bytes().unwrap();
262 assert!(
263 bytes.len() < 50,
···269 #[test]
270 fn test_did_web_with_port() {
271 let did = "did:web:localhost:3000";
272+ let result = HandleResolutionResult::success_unchecked(did);
273 assert_eq!(result.method_type, DidMethodType::Web);
274 assert_eq!(result.payload, "localhost:3000");
275 assert_eq!(result.to_did(), Some(did.to_string()));
···278 #[test]
279 fn test_did_web_with_path() {
280 let did = "did:web:example.com:path:to:did";
281+ let result = HandleResolutionResult::success_unchecked(did);
282 assert_eq!(result.method_type, DidMethodType::Web);
283 assert_eq!(result.payload, "example.com:path:to:did");
284 assert_eq!(result.to_did(), Some(did.to_string()));
···287 #[test]
288 fn test_invalid_did_format() {
289 let did = "not-a-did";
290+ let result = HandleResolutionResult::success_unchecked(did);
291 assert_eq!(result.method_type, DidMethodType::Other);
292 assert_eq!(result.payload, did);
293 assert_eq!(result.to_did(), Some(did.to_string()));
···306 ];
307308 for (did, expected_type, expected_payload) in test_cases {
309+ let result = HandleResolutionResult::success_unchecked(did);
310 assert_eq!(result.method_type, expected_type);
311 assert_eq!(result.payload, expected_payload);
312
+56-18
src/handle_resolver.rs
···1use crate::handle_resolution_result::HandleResolutionResult;
2use async_trait::async_trait;
3-use atproto_identity::resolve::{resolve_subject, DnsResolver};
4use chrono::Utc;
5-use deadpool_redis::{redis::AsyncCommands, Pool as RedisPool};
6use metrohash::MetroHash64;
7use reqwest::Client;
8use std::collections::HashMap;
···16pub enum HandleResolverError {
17 #[error("error-quickdid-resolve-1 Failed to resolve subject: {0}")]
18 ResolutionFailed(String),
19-20 #[error("error-quickdid-resolve-2 Handle not found (cached): {0}")]
21 HandleNotFoundCached(String),
22-23 #[error("error-quickdid-resolve-3 Handle not found (cached)")]
24 HandleNotFound,
25-26 #[error("error-quickdid-resolve-4 Mock resolution failure")]
27 MockResolutionFailure,
28}
···148}
149150/// Redis-backed caching handle resolver that caches resolution results in Redis
151-/// with a 90-day expiration time.
152pub struct RedisHandleResolver {
153 /// Base handle resolver to perform actual resolution
154 inner: Arc<dyn HandleResolver>,
···156 pool: RedisPool,
157 /// Redis key prefix for handle resolution cache
158 key_prefix: String,
00159}
160161impl RedisHandleResolver {
162- /// Create a new Redis-backed handle resolver
163 pub fn new(inner: Arc<dyn HandleResolver>, pool: RedisPool) -> Self {
164- Self::with_prefix(inner, pool, "handle:".to_string())
00000165 }
166167 /// Create a new Redis-backed handle resolver with a custom key prefix
···170 pool: RedisPool,
171 key_prefix: String,
172 ) -> Self {
0000000000173 Self {
174 inner,
175 pool,
176 key_prefix,
0177 }
178 }
179···184 format!("{}{}", self.key_prefix, h.finish())
185 }
186187- /// Get the TTL in seconds (90 days)
188- fn ttl_seconds() -> u64 {
189- 90 * 24 * 60 * 60 // 90 days in seconds
190 }
191}
192···243 handle,
244 did
245 );
246- HandleResolutionResult::success(did)
000000247 }
248 Err(e) => {
249 tracing::debug!("Caching failed resolution for handle {}: {}", handle, e);
250- HandleResolutionResult::not_resolved()
000000251 }
252 };
253···256 Ok(bytes) => {
257 // Set with expiration (ignore errors to not fail the resolution)
258 if let Err(e) = conn
259- .set_ex::<_, _, ()>(&key, bytes, Self::ttl_seconds())
260 .await
261 {
262 tracing::warn!("Failed to cache handle resolution in Redis: {}", e);
···336337 // Create Redis-backed resolver with a unique key prefix for testing
338 let test_prefix = format!("test:handle:{}:", uuid::Uuid::new_v4());
339- let redis_resolver =
340- RedisHandleResolver::with_prefix(mock_resolver, pool.clone(), test_prefix.clone());
0000341342 let test_handle = "alice.bsky.social";
343···385386 // Create Redis-backed resolver with a unique key prefix for testing
387 let test_prefix = format!("test:handle:{}:", uuid::Uuid::new_v4());
388- let redis_resolver =
389- RedisHandleResolver::with_prefix(mock_resolver, pool.clone(), test_prefix.clone());
0000390391 let test_handle = "error.bsky.social";
392
···1use crate::handle_resolution_result::HandleResolutionResult;
2use async_trait::async_trait;
3+use atproto_identity::resolve::{DnsResolver, resolve_subject};
4use chrono::Utc;
5+use deadpool_redis::{Pool as RedisPool, redis::AsyncCommands};
6use metrohash::MetroHash64;
7use reqwest::Client;
8use std::collections::HashMap;
···16pub enum HandleResolverError {
17 #[error("error-quickdid-resolve-1 Failed to resolve subject: {0}")]
18 ResolutionFailed(String),
19+20 #[error("error-quickdid-resolve-2 Handle not found (cached): {0}")]
21 HandleNotFoundCached(String),
22+23 #[error("error-quickdid-resolve-3 Handle not found (cached)")]
24 HandleNotFound,
25+26 #[error("error-quickdid-resolve-4 Mock resolution failure")]
27 MockResolutionFailure,
28}
···148}
149150/// Redis-backed caching handle resolver that caches resolution results in Redis
151+/// with a configurable expiration time.
152pub struct RedisHandleResolver {
153 /// Base handle resolver to perform actual resolution
154 inner: Arc<dyn HandleResolver>,
···156 pool: RedisPool,
157 /// Redis key prefix for handle resolution cache
158 key_prefix: String,
159+ /// TTL for cache entries in seconds
160+ ttl_seconds: u64,
161}
162163impl RedisHandleResolver {
164+ /// Create a new Redis-backed handle resolver with default 90-day TTL
165 pub fn new(inner: Arc<dyn HandleResolver>, pool: RedisPool) -> Self {
166+ Self::with_ttl(inner, pool, 90 * 24 * 60 * 60) // 90 days default
167+ }
168+169+ /// Create a new Redis-backed handle resolver with custom TTL
170+ pub fn with_ttl(inner: Arc<dyn HandleResolver>, pool: RedisPool, ttl_seconds: u64) -> Self {
171+ Self::with_full_config(inner, pool, "handle:".to_string(), ttl_seconds)
172 }
173174 /// Create a new Redis-backed handle resolver with a custom key prefix
···177 pool: RedisPool,
178 key_prefix: String,
179 ) -> Self {
180+ Self::with_full_config(inner, pool, key_prefix, 90 * 24 * 60 * 60)
181+ }
182+183+ /// Create a new Redis-backed handle resolver with full configuration
184+ pub fn with_full_config(
185+ inner: Arc<dyn HandleResolver>,
186+ pool: RedisPool,
187+ key_prefix: String,
188+ ttl_seconds: u64,
189+ ) -> Self {
190 Self {
191 inner,
192 pool,
193 key_prefix,
194+ ttl_seconds,
195 }
196 }
197···202 format!("{}{}", self.key_prefix, h.finish())
203 }
204205+ /// Get the TTL in seconds
206+ fn ttl_seconds(&self) -> u64 {
207+ self.ttl_seconds
208 }
209}
210···261 handle,
262 did
263 );
264+ match HandleResolutionResult::success(did) {
265+ Ok(res) => res,
266+ Err(e) => {
267+ tracing::warn!("Failed to create resolution result: {}", e);
268+ return result;
269+ }
270+ }
271 }
272 Err(e) => {
273 tracing::debug!("Caching failed resolution for handle {}: {}", handle, e);
274+ match HandleResolutionResult::not_resolved() {
275+ Ok(res) => res,
276+ Err(err) => {
277+ tracing::warn!("Failed to create not_resolved result: {}", err);
278+ return result;
279+ }
280+ }
281 }
282 };
283···286 Ok(bytes) => {
287 // Set with expiration (ignore errors to not fail the resolution)
288 if let Err(e) = conn
289+ .set_ex::<_, _, ()>(&key, bytes, self.ttl_seconds())
290 .await
291 {
292 tracing::warn!("Failed to cache handle resolution in Redis: {}", e);
···366367 // Create Redis-backed resolver with a unique key prefix for testing
368 let test_prefix = format!("test:handle:{}:", uuid::Uuid::new_v4());
369+ let redis_resolver = RedisHandleResolver::with_full_config(
370+ mock_resolver,
371+ pool.clone(),
372+ test_prefix.clone(),
373+ 3600,
374+ );
375376 let test_handle = "alice.bsky.social";
377···419420 // Create Redis-backed resolver with a unique key prefix for testing
421 let test_prefix = format!("test:handle:{}:", uuid::Uuid::new_v4());
422+ let redis_resolver = RedisHandleResolver::with_full_config(
423+ mock_resolver,
424+ pool.clone(),
425+ test_prefix.clone(),
426+ 3600,
427+ );
428429 let test_handle = "error.bsky.social";
430
···4//! that can be used with any work type for handle resolution and other tasks.
56use async_trait::async_trait;
7-use deadpool_redis::{redis::AsyncCommands, Pool as RedisPool};
8use serde::{Deserialize, Serialize};
9use std::sync::Arc;
10use thiserror::Error;
11-use tokio::sync::{mpsc, Mutex};
12use tracing::{debug, error, warn};
1314/// Queue operation errors
···4//! that can be used with any work type for handle resolution and other tasks.
56use async_trait::async_trait;
7+use deadpool_redis::{Pool as RedisPool, redis::AsyncCommands};
8use serde::{Deserialize, Serialize};
9use std::sync::Arc;
10use thiserror::Error;
11+use tokio::sync::{Mutex, mpsc};
12use tracing::{debug, error, warn};
1314/// Queue operation errors