tangled
alpha
login
or
join now
smokesignal.events
/
quickdid
50
fork
atom
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.
50
fork
atom
overview
issues
pulls
pipelines
feature: set
Nick Gerakines
6 months ago
388c322a
e313deaf
+171
2 changed files
expand all
collapse all
unified
split
src
handle_resolver
redis.rs
traits.rs
+137
src/handle_resolver/redis.rs
···
378
}
379
}
380
}
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
381
}
382
383
/// Create a new Redis-backed handle resolver with default 90-day TTL.
···
675
let exists: bool = conn.exists(&key).await.unwrap_or(false);
676
assert!(!exists, "Key for {} should not exist after purge", input);
677
}
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
678
}
679
}
680
···
378
}
379
}
380
}
381
+
382
+
async fn set(&self, handle: &str, did: &str) -> Result<(), HandleResolverError> {
383
+
// Normalize the handle to lowercase
384
+
let handle = handle.to_lowercase();
385
+
let handle_key = self.make_key(&handle);
386
+
let did_key = self.make_key(did);
387
+
388
+
match self.pool.get().await {
389
+
Ok(mut conn) => {
390
+
// Create a resolution result for the successful mapping
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
+
));
399
+
}
400
+
};
401
+
402
+
// Serialize to bytes
403
+
match resolution_result.to_bytes() {
404
+
Ok(bytes) => {
405
+
// Set the handle -> DID mapping with expiration
406
+
if let Err(e) = conn
407
+
.set_ex::<_, _, ()>(&handle_key, bytes, self.ttl_seconds())
408
+
.await
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
+
));
415
+
}
416
+
417
+
// Set the reverse DID -> handle mapping
418
+
if let Err(e) = conn
419
+
.set_ex::<_, _, ()>(&did_key, handle.as_bytes(), self.ttl_seconds())
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;
424
+
// Don't fail the operation, but log the warning
425
+
}
426
+
427
+
tracing::debug!("Set handle {} -> DID {} mapping in cache", handle, did);
428
+
self.metrics.incr("resolver.redis.set_success").await;
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
+
))
437
+
}
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
+
))
446
+
}
447
+
}
448
+
}
449
}
450
451
/// Create a new Redis-backed handle resolver with default 90-day TTL.
···
743
let exists: bool = conn.exists(&key).await.unwrap_or(false);
744
assert!(!exists, "Key for {} should not exist after purge", input);
745
}
746
+
}
747
+
}
748
+
749
+
#[tokio::test]
750
+
async fn test_redis_handle_resolver_set_method() {
751
+
let pool = match crate::test_helpers::get_test_redis_pool() {
752
+
Some(p) => p,
753
+
None => return,
754
+
};
755
+
756
+
// Create mock resolver
757
+
let mock_resolver = Arc::new(MockHandleResolver {
758
+
should_fail: false,
759
+
expected_did: "did:plc:old".to_string(),
760
+
});
761
+
762
+
// Create metrics publisher
763
+
let metrics = Arc::new(crate::metrics::NoOpMetricsPublisher);
764
+
765
+
// Create Redis-backed resolver with a unique key prefix for testing
766
+
let test_prefix = format!(
767
+
"test:handle:{}:",
768
+
std::time::SystemTime::now()
769
+
.duration_since(std::time::UNIX_EPOCH)
770
+
.unwrap()
771
+
.as_nanos()
772
+
);
773
+
let redis_resolver = RedisHandleResolver::with_full_config(
774
+
mock_resolver,
775
+
pool.clone(),
776
+
test_prefix.clone(),
777
+
3600,
778
+
metrics,
779
+
);
780
+
781
+
let test_handle = "charlie.bsky.social";
782
+
let test_did = "did:plc:newuser123";
783
+
784
+
// Set the mapping using the trait method
785
+
redis_resolver.set(test_handle, test_did).await.unwrap();
786
+
787
+
// Verify the mapping by resolving the handle
788
+
let (resolved_did, _) = redis_resolver.resolve(test_handle).await.unwrap();
789
+
assert_eq!(resolved_did, test_did);
790
+
791
+
// Test that uppercase handles are normalized
792
+
redis_resolver.set("DAVE.BSKY.SOCIAL", "did:plc:dave456").await.unwrap();
793
+
let (resolved_did2, _) = redis_resolver.resolve("dave.bsky.social").await.unwrap();
794
+
assert_eq!(resolved_did2, "did:plc:dave456");
795
+
796
+
// Verify both forward and reverse mappings exist
797
+
if let Ok(mut conn) = pool.get().await {
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
}
816
}
817
+34
src/handle_resolver/traits.rs
···
77
async fn purge(&self, _subject: &str) -> Result<(), HandleResolverError> {
78
Ok(())
79
}
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
80
}
81
82
#[cfg(test)]
···
102
assert!(resolver.purge("alice.bsky.social").await.is_ok());
103
assert!(resolver.purge("did:plc:xyz123").await.is_ok());
104
assert!(resolver.purge("").await.is_ok());
0
0
0
0
0
0
0
0
0
0
105
}
106
}
···
77
async fn purge(&self, _subject: &str) -> Result<(), HandleResolverError> {
78
Ok(())
79
}
80
+
81
+
/// Set a handle-to-DID mapping in the cache.
82
+
///
83
+
/// This method allows manually setting or updating a cached mapping between
84
+
/// a handle and its corresponding DID. This is useful for pre-populating
85
+
/// caches or updating stale entries.
86
+
///
87
+
/// # Arguments
88
+
///
89
+
/// * `handle` - The handle to cache (e.g., "alice.bsky.social")
90
+
/// * `did` - The DID to associate with the handle (e.g., "did:plc:xyz123")
91
+
///
92
+
/// # Returns
93
+
///
94
+
/// Ok(()) if the mapping was successfully set or if the implementation
95
+
/// doesn't support manual cache updates. Most implementations will simply
96
+
/// return Ok(()) as a no-op.
97
+
///
98
+
/// # Default Implementation
99
+
///
100
+
/// The default implementation is a no-op that always returns Ok(()).
101
+
async fn set(&self, _handle: &str, _did: &str) -> Result<(), HandleResolverError> {
102
+
Ok(())
103
+
}
104
}
105
106
#[cfg(test)]
···
126
assert!(resolver.purge("alice.bsky.social").await.is_ok());
127
assert!(resolver.purge("did:plc:xyz123").await.is_ok());
128
assert!(resolver.purge("").await.is_ok());
129
+
}
130
+
131
+
#[tokio::test]
132
+
async fn test_default_set_implementation() {
133
+
let resolver = NoOpTestResolver;
134
+
135
+
// Default implementation should always return Ok(())
136
+
assert!(resolver.set("alice.bsky.social", "did:plc:xyz123").await.is_ok());
137
+
assert!(resolver.set("bob.example.com", "did:web:example.com").await.is_ok());
138
+
assert!(resolver.set("", "").await.is_ok());
139
}
140
}