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.
···179 async fn set(&self, handle: &str, did: &str) -> Result<(), HandleResolverError> {
180 // Normalize the handle to lowercase
181 let handle = handle.to_lowercase();
182-183 // Update the in-memory cache
184 {
185 let mut cache = self.cache.write().await;
···190 );
191 self.metrics.incr("resolver.memory.set").await;
192 tracing::debug!("Set handle {} -> DID {} in memory cache", handle, did);
193-194 // Track cache size
195 let cache_size = cache.len() as u64;
196 self.metrics
197 .gauge("resolver.memory.cache_entries", cache_size)
198 .await;
199 }
200-201 // Chain to inner resolver
202 self.inner.set(&handle, did).await
203 }
···179 async fn set(&self, handle: &str, did: &str) -> Result<(), HandleResolverError> {
180 // Normalize the handle to lowercase
181 let handle = handle.to_lowercase();
182+183 // Update the in-memory cache
184 {
185 let mut cache = self.cache.write().await;
···190 );
191 self.metrics.incr("resolver.memory.set").await;
192 tracing::debug!("Set handle {} -> DID {} in memory cache", handle, did);
193+194 // Track cache size
195 let cache_size = cache.len() as u64;
196 self.metrics
197 .gauge("resolver.memory.cache_entries", cache_size)
198 .await;
199 }
200+201 // Chain to inner resolver
202 self.inner.set(&handle, did).await
203 }
+124-63
src/handle_resolver/redis.rs
···9use crate::handle_resolution_result::HandleResolutionResult;
10use crate::metrics::SharedMetricsPublisher;
11use async_trait::async_trait;
12-use atproto_identity::resolve::{parse_input, InputType};
13use deadpool_redis::{Pool as RedisPool, redis::AsyncCommands};
14use metrohash::MetroHash64;
15use std::hash::Hasher as _;
···124 /// This method removes both the handle->DID mapping and the reverse DID->handle mapping.
125 async fn purge_handle(&self, handle: &str) -> Result<(), HandleResolverError> {
126 let handle_key = self.make_key(handle);
127-128 match self.pool.get().await {
129 Ok(mut conn) => {
130 // First, try to get the cached result to find the associated DID
···142 if let Ok(cached_result) = HandleResolutionResult::from_bytes(&cached_bytes) {
143 if let Some(did) = cached_result.to_did() {
144 let did_key = self.make_key(&did);
145-146 // Delete both the handle key and the DID key
147 let _: Result<(), _> = conn.del(&[&handle_key, &did_key]).await;
148-149 tracing::debug!("Purged handle {} and associated DID {}", handle, did);
150- self.metrics.incr("resolver.redis.purge_handle_success").await;
00151 } else {
152 // Just delete the handle key if no DID was resolved
153 let _: Result<(), _> = conn.del(&handle_key).await;
154 tracing::debug!("Purged unresolved handle {}", handle);
155- self.metrics.incr("resolver.redis.purge_handle_unresolved").await;
00156 }
157 } else {
158 // If we can't deserialize, just delete the handle key
159 let _: Result<(), _> = conn.del(&handle_key).await;
160 tracing::warn!("Purged handle {} with undeserializable data", handle);
161- self.metrics.incr("resolver.redis.purge_handle_corrupt").await;
00162 }
163 } else {
164 tracing::debug!("Handle {} not found in cache for purging", handle);
165- self.metrics.incr("resolver.redis.purge_handle_not_found").await;
00166 }
167-168 Ok(())
169 }
170 Err(e) => {
171 tracing::warn!("Failed to get Redis connection for purging: {}", e);
172- self.metrics.incr("resolver.redis.purge_connection_error").await;
173- Err(HandleResolverError::ResolutionFailed(format!("Redis connection error: {}", e)))
00000174 }
175 }
176 }
···180 /// This method removes both the DID->handle mapping and the handle->DID mapping.
181 async fn purge_did(&self, did: &str) -> Result<(), HandleResolverError> {
182 let did_key = self.make_key(did);
183-184 match self.pool.get().await {
185 Ok(mut conn) => {
186 // First, try to get the associated handle from the reverse mapping
···197 if let Some(handle_bytes) = handle_bytes {
198 if let Ok(handle) = String::from_utf8(handle_bytes) {
199 let handle_key = self.make_key(&handle);
200-201 // Delete both the DID key and the handle key
202 let _: Result<(), _> = conn.del(&[&did_key, &handle_key]).await;
203-204 tracing::debug!("Purged DID {} and associated handle {}", did, handle);
205 self.metrics.incr("resolver.redis.purge_did_success").await;
206 } else {
···211 }
212 } else {
213 tracing::debug!("DID {} not found in cache for purging", did);
214- self.metrics.incr("resolver.redis.purge_did_not_found").await;
00215 }
216-217 Ok(())
218 }
219 Err(e) => {
220 tracing::warn!("Failed to get Redis connection for purging: {}", e);
221- self.metrics.incr("resolver.redis.purge_connection_error").await;
222- Err(HandleResolverError::ResolutionFailed(format!("Redis connection error: {}", e)))
00000223 }
224 }
225 }
···323 self.metrics.incr("resolver.redis.cache_set_error").await;
324 } else {
325 self.metrics.incr("resolver.redis.cache_set").await;
326-327 // For successful resolutions, also store reverse DID -> handle mapping
328 if let Ok((did, _)) = &result {
329 let did_key = self.make_key(did);
330 if let Err(e) = conn
331- .set_ex::<_, _, ()>(&did_key, handle.as_bytes(), self.ttl_seconds())
0000332 .await
333 {
334- tracing::warn!("Failed to cache reverse DID->handle mapping in Redis: {}", e);
335- self.metrics.incr("resolver.redis.reverse_cache_set_error").await;
00000336 } else {
337- tracing::debug!("Cached reverse mapping for DID {}: {}", did, handle);
0000338 self.metrics.incr("resolver.redis.reverse_cache_set").await;
339 }
340 }
···366367 async fn purge(&self, subject: &str) -> Result<(), HandleResolverError> {
368 // Use atproto_identity's parse_input to properly identify the input type
369- let parsed_input = parse_input(subject).map_err(|_| HandleResolverError::InvalidSubject(subject.to_string()))?;
0370 match parsed_input {
371 InputType::Handle(handle) => {
372 // It's a handle, purge using the lowercase version
···391 let resolution_result = match HandleResolutionResult::success(did) {
392 Ok(res) => res,
393 Err(e) => {
394- tracing::warn!("Failed to create resolution result for set operation: {}", e);
395- self.metrics.incr("resolver.redis.set_result_create_error").await;
396- return Err(HandleResolverError::InvalidSubject(
397- format!("Failed to create resolution result: {}", e)
398- ));
000000399 }
400 };
401···409 {
410 tracing::warn!("Failed to set handle->DID mapping in Redis: {}", e);
411 self.metrics.incr("resolver.redis.set_cache_error").await;
412- return Err(HandleResolverError::ResolutionFailed(
413- format!("Failed to set cache: {}", e)
414- ));
0415 }
416417 // Set the reverse DID -> handle mapping
···420 .await
421 {
422 tracing::warn!("Failed to set DID->handle mapping in Redis: {}", e);
423- self.metrics.incr("resolver.redis.set_reverse_cache_error").await;
00424 // Don't fail the operation, but log the warning
425 }
426···429 Ok(())
430 }
431 Err(e) => {
432- tracing::warn!("Failed to serialize resolution result for set operation: {}", e);
433- self.metrics.incr("resolver.redis.set_serialize_error").await;
434- Err(HandleResolverError::InvalidSubject(
435- format!("Failed to serialize: {}", e)
436- ))
000000437 }
438 }
439 }
440 Err(e) => {
441 tracing::warn!("Failed to get Redis connection for set operation: {}", e);
442- self.metrics.incr("resolver.redis.set_connection_error").await;
443- Err(HandleResolverError::ResolutionFailed(
444- format!("Redis connection error: {}", e)
445- ))
000446 }
447 }
448 }
···634 let mut h = MetroHash64::default();
635 h.write(test_handle.as_bytes());
636 let handle_key = format!("{}{}", test_prefix, h.finish());
637-638 let mut h2 = MetroHash64::default();
639 h2.write(expected_did.as_bytes());
640 let did_key = format!("{}{}", test_prefix, h2.finish());
641-642 // Check handle -> DID mapping exists
643 let handle_exists: bool = conn.exists(&handle_key).await.unwrap();
644 assert!(handle_exists, "Handle key should exist in cache");
645-646 // Check DID -> handle mapping exists
647 let did_exists: bool = conn.exists(&did_key).await.unwrap();
648 assert!(did_exists, "DID key should exist in cache");
649-650 // Test purge by handle using the trait method
651 redis_resolver.purge(test_handle).await.unwrap();
652-653 // Verify both keys were deleted
654 let handle_exists_after: bool = conn.exists(&handle_key).await.unwrap();
655- assert!(!handle_exists_after, "Handle key should be deleted after purge");
656-000657 let did_exists_after: bool = conn.exists(&did_key).await.unwrap();
658 assert!(!did_exists_after, "DID key should be deleted after purge");
659 }
···664665 // Test purge by DID using the trait method
666 redis_resolver.purge(expected_did).await.unwrap();
667-668 // Verify both keys were deleted again
669 if let Ok(mut conn) = pool.get().await {
670 let mut h = MetroHash64::default();
671 h.write(test_handle.as_bytes());
672 let handle_key = format!("{}{}", test_prefix, h.finish());
673-674 let mut h2 = MetroHash64::default();
675 h2.write(expected_did.as_bytes());
676 let did_key = format!("{}{}", test_prefix, h2.finish());
677-678 let handle_exists: bool = conn.exists(&handle_key).await.unwrap();
679- assert!(!handle_exists, "Handle key should be deleted after DID purge");
680-000681 let did_exists: bool = conn.exists(&did_key).await.unwrap();
682 assert!(!did_exists, "DID key should be deleted after DID purge");
683 }
···717718 // Test different input formats
719 let test_cases = vec![
720- ("alice.bsky.social", "alice.bsky.social"), // Handle
721- ("ALICE.BSKY.SOCIAL", "alice.bsky.social"), // Handle (uppercase)
722- ("did:plc:abc123", "did:plc:abc123"), // PLC DID
723 ("did:web:example.com", "did:web:example.com"), // Web DID
724 ];
725···738 let mut h = MetroHash64::default();
739 h.write(expected_key.as_bytes());
740 let key = format!("{}{}", test_prefix, h.finish());
741-742 // After purge, key should not exist
743 let exists: bool = conn.exists(&key).await.unwrap_or(false);
744 assert!(!exists, "Key for {} should not exist after purge", input);
···789 assert_eq!(resolved_did, test_did);
790791 // Test that uppercase handles are normalized
792- redis_resolver.set("DAVE.BSKY.SOCIAL", "did:plc:dave456").await.unwrap();
000793 let (resolved_did2, _) = redis_resolver.resolve("dave.bsky.social").await.unwrap();
794 assert_eq!(resolved_did2, "did:plc:dave456");
795···798 let mut h = MetroHash64::default();
799 h.write(test_handle.as_bytes());
800 let handle_key = format!("{}{}", test_prefix, h.finish());
801-802 let mut h2 = MetroHash64::default();
803 h2.write(test_did.as_bytes());
804 let did_key = format!("{}{}", test_prefix, h2.finish());
805-806 // Check both keys exist
807 let handle_exists: bool = conn.exists(&handle_key).await.unwrap();
808 assert!(handle_exists, "Handle key should exist after set");
809-810 let did_exists: bool = conn.exists(&did_key).await.unwrap();
811 assert!(did_exists, "DID key should exist after set");
812-813 // Clean up test data
814 let _: Result<(), _> = conn.del(&[&handle_key, &did_key]).await;
815 }
···9use crate::handle_resolution_result::HandleResolutionResult;
10use crate::metrics::SharedMetricsPublisher;
11use async_trait::async_trait;
12+use atproto_identity::resolve::{InputType, parse_input};
13use deadpool_redis::{Pool as RedisPool, redis::AsyncCommands};
14use metrohash::MetroHash64;
15use std::hash::Hasher as _;
···124 /// This method removes both the handle->DID mapping and the reverse DID->handle mapping.
125 async fn purge_handle(&self, handle: &str) -> Result<(), HandleResolverError> {
126 let handle_key = self.make_key(handle);
127+128 match self.pool.get().await {
129 Ok(mut conn) => {
130 // First, try to get the cached result to find the associated DID
···142 if let Ok(cached_result) = HandleResolutionResult::from_bytes(&cached_bytes) {
143 if let Some(did) = cached_result.to_did() {
144 let did_key = self.make_key(&did);
145+146 // Delete both the handle key and the DID key
147 let _: Result<(), _> = conn.del(&[&handle_key, &did_key]).await;
148+149 tracing::debug!("Purged handle {} and associated DID {}", handle, did);
150+ self.metrics
151+ .incr("resolver.redis.purge_handle_success")
152+ .await;
153 } else {
154 // Just delete the handle key if no DID was resolved
155 let _: Result<(), _> = conn.del(&handle_key).await;
156 tracing::debug!("Purged unresolved handle {}", handle);
157+ self.metrics
158+ .incr("resolver.redis.purge_handle_unresolved")
159+ .await;
160 }
161 } else {
162 // If we can't deserialize, just delete the handle key
163 let _: Result<(), _> = conn.del(&handle_key).await;
164 tracing::warn!("Purged handle {} with undeserializable data", handle);
165+ self.metrics
166+ .incr("resolver.redis.purge_handle_corrupt")
167+ .await;
168 }
169 } else {
170 tracing::debug!("Handle {} not found in cache for purging", handle);
171+ self.metrics
172+ .incr("resolver.redis.purge_handle_not_found")
173+ .await;
174 }
175+176 Ok(())
177 }
178 Err(e) => {
179 tracing::warn!("Failed to get Redis connection for purging: {}", e);
180+ self.metrics
181+ .incr("resolver.redis.purge_connection_error")
182+ .await;
183+ Err(HandleResolverError::ResolutionFailed(format!(
184+ "Redis connection error: {}",
185+ e
186+ )))
187 }
188 }
189 }
···193 /// This method removes both the DID->handle mapping and the handle->DID mapping.
194 async fn purge_did(&self, did: &str) -> Result<(), HandleResolverError> {
195 let did_key = self.make_key(did);
196+197 match self.pool.get().await {
198 Ok(mut conn) => {
199 // First, try to get the associated handle from the reverse mapping
···210 if let Some(handle_bytes) = handle_bytes {
211 if let Ok(handle) = String::from_utf8(handle_bytes) {
212 let handle_key = self.make_key(&handle);
213+214 // Delete both the DID key and the handle key
215 let _: Result<(), _> = conn.del(&[&did_key, &handle_key]).await;
216+217 tracing::debug!("Purged DID {} and associated handle {}", did, handle);
218 self.metrics.incr("resolver.redis.purge_did_success").await;
219 } else {
···224 }
225 } else {
226 tracing::debug!("DID {} not found in cache for purging", did);
227+ self.metrics
228+ .incr("resolver.redis.purge_did_not_found")
229+ .await;
230 }
231+232 Ok(())
233 }
234 Err(e) => {
235 tracing::warn!("Failed to get Redis connection for purging: {}", e);
236+ self.metrics
237+ .incr("resolver.redis.purge_connection_error")
238+ .await;
239+ Err(HandleResolverError::ResolutionFailed(format!(
240+ "Redis connection error: {}",
241+ e
242+ )))
243 }
244 }
245 }
···343 self.metrics.incr("resolver.redis.cache_set_error").await;
344 } else {
345 self.metrics.incr("resolver.redis.cache_set").await;
346+347 // For successful resolutions, also store reverse DID -> handle mapping
348 if let Ok((did, _)) = &result {
349 let did_key = self.make_key(did);
350 if let Err(e) = conn
351+ .set_ex::<_, _, ()>(
352+ &did_key,
353+ handle.as_bytes(),
354+ self.ttl_seconds(),
355+ )
356 .await
357 {
358+ tracing::warn!(
359+ "Failed to cache reverse DID->handle mapping in Redis: {}",
360+ e
361+ );
362+ self.metrics
363+ .incr("resolver.redis.reverse_cache_set_error")
364+ .await;
365 } else {
366+ tracing::debug!(
367+ "Cached reverse mapping for DID {}: {}",
368+ did,
369+ handle
370+ );
371 self.metrics.incr("resolver.redis.reverse_cache_set").await;
372 }
373 }
···399400 async fn purge(&self, subject: &str) -> Result<(), HandleResolverError> {
401 // Use atproto_identity's parse_input to properly identify the input type
402+ let parsed_input = parse_input(subject)
403+ .map_err(|_| HandleResolverError::InvalidSubject(subject.to_string()))?;
404 match parsed_input {
405 InputType::Handle(handle) => {
406 // It's a handle, purge using the lowercase version
···425 let resolution_result = match HandleResolutionResult::success(did) {
426 Ok(res) => res,
427 Err(e) => {
428+ tracing::warn!(
429+ "Failed to create resolution result for set operation: {}",
430+ e
431+ );
432+ self.metrics
433+ .incr("resolver.redis.set_result_create_error")
434+ .await;
435+ return Err(HandleResolverError::InvalidSubject(format!(
436+ "Failed to create resolution result: {}",
437+ e
438+ )));
439 }
440 };
441···449 {
450 tracing::warn!("Failed to set handle->DID mapping in Redis: {}", e);
451 self.metrics.incr("resolver.redis.set_cache_error").await;
452+ return Err(HandleResolverError::ResolutionFailed(format!(
453+ "Failed to set cache: {}",
454+ e
455+ )));
456 }
457458 // Set the reverse DID -> handle mapping
···461 .await
462 {
463 tracing::warn!("Failed to set DID->handle mapping in Redis: {}", e);
464+ self.metrics
465+ .incr("resolver.redis.set_reverse_cache_error")
466+ .await;
467 // Don't fail the operation, but log the warning
468 }
469···472 Ok(())
473 }
474 Err(e) => {
475+ tracing::warn!(
476+ "Failed to serialize resolution result for set operation: {}",
477+ e
478+ );
479+ self.metrics
480+ .incr("resolver.redis.set_serialize_error")
481+ .await;
482+ Err(HandleResolverError::InvalidSubject(format!(
483+ "Failed to serialize: {}",
484+ e
485+ )))
486 }
487 }
488 }
489 Err(e) => {
490 tracing::warn!("Failed to get Redis connection for set operation: {}", e);
491+ self.metrics
492+ .incr("resolver.redis.set_connection_error")
493+ .await;
494+ Err(HandleResolverError::ResolutionFailed(format!(
495+ "Redis connection error: {}",
496+ e
497+ )))
498 }
499 }
500 }
···686 let mut h = MetroHash64::default();
687 h.write(test_handle.as_bytes());
688 let handle_key = format!("{}{}", test_prefix, h.finish());
689+690 let mut h2 = MetroHash64::default();
691 h2.write(expected_did.as_bytes());
692 let did_key = format!("{}{}", test_prefix, h2.finish());
693+694 // Check handle -> DID mapping exists
695 let handle_exists: bool = conn.exists(&handle_key).await.unwrap();
696 assert!(handle_exists, "Handle key should exist in cache");
697+698 // Check DID -> handle mapping exists
699 let did_exists: bool = conn.exists(&did_key).await.unwrap();
700 assert!(did_exists, "DID key should exist in cache");
701+702 // Test purge by handle using the trait method
703 redis_resolver.purge(test_handle).await.unwrap();
704+705 // Verify both keys were deleted
706 let handle_exists_after: bool = conn.exists(&handle_key).await.unwrap();
707+ assert!(
708+ !handle_exists_after,
709+ "Handle key should be deleted after purge"
710+ );
711+712 let did_exists_after: bool = conn.exists(&did_key).await.unwrap();
713 assert!(!did_exists_after, "DID key should be deleted after purge");
714 }
···719720 // Test purge by DID using the trait method
721 redis_resolver.purge(expected_did).await.unwrap();
722+723 // Verify both keys were deleted again
724 if let Ok(mut conn) = pool.get().await {
725 let mut h = MetroHash64::default();
726 h.write(test_handle.as_bytes());
727 let handle_key = format!("{}{}", test_prefix, h.finish());
728+729 let mut h2 = MetroHash64::default();
730 h2.write(expected_did.as_bytes());
731 let did_key = format!("{}{}", test_prefix, h2.finish());
732+733 let handle_exists: bool = conn.exists(&handle_key).await.unwrap();
734+ assert!(
735+ !handle_exists,
736+ "Handle key should be deleted after DID purge"
737+ );
738+739 let did_exists: bool = conn.exists(&did_key).await.unwrap();
740 assert!(!did_exists, "DID key should be deleted after DID purge");
741 }
···775776 // Test different input formats
777 let test_cases = vec![
778+ ("alice.bsky.social", "alice.bsky.social"), // Handle
779+ ("ALICE.BSKY.SOCIAL", "alice.bsky.social"), // Handle (uppercase)
780+ ("did:plc:abc123", "did:plc:abc123"), // PLC DID
781 ("did:web:example.com", "did:web:example.com"), // Web DID
782 ];
783···796 let mut h = MetroHash64::default();
797 h.write(expected_key.as_bytes());
798 let key = format!("{}{}", test_prefix, h.finish());
799+800 // After purge, key should not exist
801 let exists: bool = conn.exists(&key).await.unwrap_or(false);
802 assert!(!exists, "Key for {} should not exist after purge", input);
···847 assert_eq!(resolved_did, test_did);
848849 // Test that uppercase handles are normalized
850+ redis_resolver
851+ .set("DAVE.BSKY.SOCIAL", "did:plc:dave456")
852+ .await
853+ .unwrap();
854 let (resolved_did2, _) = redis_resolver.resolve("dave.bsky.social").await.unwrap();
855 assert_eq!(resolved_did2, "did:plc:dave456");
856···859 let mut h = MetroHash64::default();
860 h.write(test_handle.as_bytes());
861 let handle_key = format!("{}{}", test_prefix, h.finish());
862+863 let mut h2 = MetroHash64::default();
864 h2.write(test_did.as_bytes());
865 let did_key = format!("{}{}", test_prefix, h2.finish());
866+867 // Check both keys exist
868 let handle_exists: bool = conn.exists(&handle_key).await.unwrap();
869 assert!(handle_exists, "Handle key should exist after set");
870+871 let did_exists: bool = conn.exists(&did_key).await.unwrap();
872 assert!(did_exists, "DID key should exist after set");
873+874 // Clean up test data
875 let _: Result<(), _> = conn.del(&[&handle_key, &did_key]).await;
876 }
+21-9
src/handle_resolver/sqlite.rs
···259 async fn set(&self, handle: &str, did: &str) -> Result<(), HandleResolverError> {
260 // Normalize the handle to lowercase
261 let handle = handle.to_lowercase();
262-263 // Update the SQLite cache
264 if let Ok(mut conn) = self.pool.acquire().await {
265 // Create a resolution result for the successful mapping
266 let resolution_result = match HandleResolutionResult::success(did) {
267 Ok(res) => res,
268 Err(e) => {
269- tracing::warn!("Failed to create resolution result for set operation: {}", e);
270- self.metrics.incr("resolver.sqlite.set_result_create_error").await;
00000271 // Still chain to inner resolver even if we can't cache
272 return self.inner.set(&handle, did).await;
273 }
···281 .duration_since(std::time::UNIX_EPOCH)
282 .unwrap_or_default()
283 .as_secs() as i64;
284-285 let expires_at = timestamp + self.ttl_seconds as i64;
286-287 match sqlx::query(
288 "INSERT OR REPLACE INTO handle_resolution_cache (handle, resolved_value, created_at, expires_at) VALUES (?, ?, ?, ?)"
289 )
···306 }
307 }
308 Err(e) => {
309- tracing::warn!("Failed to serialize resolution result for set operation: {}", e);
310- self.metrics.incr("resolver.sqlite.set_serialize_error").await;
00000311 // Still chain to inner resolver even if serialization fails
312 }
313 }
314 } else {
315 tracing::warn!("Failed to get SQLite connection for set operation");
316- self.metrics.incr("resolver.sqlite.set_connection_error").await;
00317 }
318-319 // Chain to inner resolver
320 self.inner.set(&handle, did).await
321 }
···259 async fn set(&self, handle: &str, did: &str) -> Result<(), HandleResolverError> {
260 // Normalize the handle to lowercase
261 let handle = handle.to_lowercase();
262+263 // Update the SQLite cache
264 if let Ok(mut conn) = self.pool.acquire().await {
265 // Create a resolution result for the successful mapping
266 let resolution_result = match HandleResolutionResult::success(did) {
267 Ok(res) => res,
268 Err(e) => {
269+ tracing::warn!(
270+ "Failed to create resolution result for set operation: {}",
271+ e
272+ );
273+ self.metrics
274+ .incr("resolver.sqlite.set_result_create_error")
275+ .await;
276 // Still chain to inner resolver even if we can't cache
277 return self.inner.set(&handle, did).await;
278 }
···286 .duration_since(std::time::UNIX_EPOCH)
287 .unwrap_or_default()
288 .as_secs() as i64;
289+290 let expires_at = timestamp + self.ttl_seconds as i64;
291+292 match sqlx::query(
293 "INSERT OR REPLACE INTO handle_resolution_cache (handle, resolved_value, created_at, expires_at) VALUES (?, ?, ?, ?)"
294 )
···311 }
312 }
313 Err(e) => {
314+ tracing::warn!(
315+ "Failed to serialize resolution result for set operation: {}",
316+ e
317+ );
318+ self.metrics
319+ .incr("resolver.sqlite.set_serialize_error")
320+ .await;
321 // Still chain to inner resolver even if serialization fails
322 }
323 }
324 } else {
325 tracing::warn!("Failed to get SQLite connection for set operation");
326+ self.metrics
327+ .incr("resolver.sqlite.set_connection_error")
328+ .await;
329 }
330+331 // Chain to inner resolver
332 self.inner.set(&handle, did).await
333 }
···282 // Verify the set method was called
283 let set_calls = resolver.get_set_calls();
284 assert_eq!(set_calls.len(), 1);
285- assert_eq!(set_calls[0], ("alice.bsky.social".to_string(), "did:plc:testuser".to_string()));
000000286287 // Verify no purge was called
288 let purge_calls = resolver.get_purge_calls();
···351352 assert_eq!(handler.handler_id(), "quickdid_handler");
353 }
354-}
···282 // Verify the set method was called
283 let set_calls = resolver.get_set_calls();
284 assert_eq!(set_calls.len(), 1);
285+ assert_eq!(
286+ set_calls[0],
287+ (
288+ "alice.bsky.social".to_string(),
289+ "did:plc:testuser".to_string()
290+ )
291+ );
292293 // Verify no purge was called
294 let purge_calls = resolver.get_purge_calls();
···357358 assert_eq!(handler.handler_id(), "quickdid_handler");
359 }
360+}