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
378
}
379
379
}
380
380
}
381
381
+
382
382
+
async fn set(&self, handle: &str, did: &str) -> Result<(), HandleResolverError> {
383
383
+
// Normalize the handle to lowercase
384
384
+
let handle = handle.to_lowercase();
385
385
+
let handle_key = self.make_key(&handle);
386
386
+
let did_key = self.make_key(did);
387
387
+
388
388
+
match self.pool.get().await {
389
389
+
Ok(mut conn) => {
390
390
+
// Create a resolution result for the successful mapping
391
391
+
let resolution_result = match HandleResolutionResult::success(did) {
392
392
+
Ok(res) => res,
393
393
+
Err(e) => {
394
394
+
tracing::warn!("Failed to create resolution result for set operation: {}", e);
395
395
+
self.metrics.incr("resolver.redis.set_result_create_error").await;
396
396
+
return Err(HandleResolverError::InvalidSubject(
397
397
+
format!("Failed to create resolution result: {}", e)
398
398
+
));
399
399
+
}
400
400
+
};
401
401
+
402
402
+
// Serialize to bytes
403
403
+
match resolution_result.to_bytes() {
404
404
+
Ok(bytes) => {
405
405
+
// Set the handle -> DID mapping with expiration
406
406
+
if let Err(e) = conn
407
407
+
.set_ex::<_, _, ()>(&handle_key, bytes, self.ttl_seconds())
408
408
+
.await
409
409
+
{
410
410
+
tracing::warn!("Failed to set handle->DID mapping in Redis: {}", e);
411
411
+
self.metrics.incr("resolver.redis.set_cache_error").await;
412
412
+
return Err(HandleResolverError::ResolutionFailed(
413
413
+
format!("Failed to set cache: {}", e)
414
414
+
));
415
415
+
}
416
416
+
417
417
+
// Set the reverse DID -> handle mapping
418
418
+
if let Err(e) = conn
419
419
+
.set_ex::<_, _, ()>(&did_key, handle.as_bytes(), self.ttl_seconds())
420
420
+
.await
421
421
+
{
422
422
+
tracing::warn!("Failed to set DID->handle mapping in Redis: {}", e);
423
423
+
self.metrics.incr("resolver.redis.set_reverse_cache_error").await;
424
424
+
// Don't fail the operation, but log the warning
425
425
+
}
426
426
+
427
427
+
tracing::debug!("Set handle {} -> DID {} mapping in cache", handle, did);
428
428
+
self.metrics.incr("resolver.redis.set_success").await;
429
429
+
Ok(())
430
430
+
}
431
431
+
Err(e) => {
432
432
+
tracing::warn!("Failed to serialize resolution result for set operation: {}", e);
433
433
+
self.metrics.incr("resolver.redis.set_serialize_error").await;
434
434
+
Err(HandleResolverError::InvalidSubject(
435
435
+
format!("Failed to serialize: {}", e)
436
436
+
))
437
437
+
}
438
438
+
}
439
439
+
}
440
440
+
Err(e) => {
441
441
+
tracing::warn!("Failed to get Redis connection for set operation: {}", e);
442
442
+
self.metrics.incr("resolver.redis.set_connection_error").await;
443
443
+
Err(HandleResolverError::ResolutionFailed(
444
444
+
format!("Redis connection error: {}", e)
445
445
+
))
446
446
+
}
447
447
+
}
448
448
+
}
381
449
}
382
450
383
451
/// Create a new Redis-backed handle resolver with default 90-day TTL.
···
675
743
let exists: bool = conn.exists(&key).await.unwrap_or(false);
676
744
assert!(!exists, "Key for {} should not exist after purge", input);
677
745
}
746
746
+
}
747
747
+
}
748
748
+
749
749
+
#[tokio::test]
750
750
+
async fn test_redis_handle_resolver_set_method() {
751
751
+
let pool = match crate::test_helpers::get_test_redis_pool() {
752
752
+
Some(p) => p,
753
753
+
None => return,
754
754
+
};
755
755
+
756
756
+
// Create mock resolver
757
757
+
let mock_resolver = Arc::new(MockHandleResolver {
758
758
+
should_fail: false,
759
759
+
expected_did: "did:plc:old".to_string(),
760
760
+
});
761
761
+
762
762
+
// Create metrics publisher
763
763
+
let metrics = Arc::new(crate::metrics::NoOpMetricsPublisher);
764
764
+
765
765
+
// Create Redis-backed resolver with a unique key prefix for testing
766
766
+
let test_prefix = format!(
767
767
+
"test:handle:{}:",
768
768
+
std::time::SystemTime::now()
769
769
+
.duration_since(std::time::UNIX_EPOCH)
770
770
+
.unwrap()
771
771
+
.as_nanos()
772
772
+
);
773
773
+
let redis_resolver = RedisHandleResolver::with_full_config(
774
774
+
mock_resolver,
775
775
+
pool.clone(),
776
776
+
test_prefix.clone(),
777
777
+
3600,
778
778
+
metrics,
779
779
+
);
780
780
+
781
781
+
let test_handle = "charlie.bsky.social";
782
782
+
let test_did = "did:plc:newuser123";
783
783
+
784
784
+
// Set the mapping using the trait method
785
785
+
redis_resolver.set(test_handle, test_did).await.unwrap();
786
786
+
787
787
+
// Verify the mapping by resolving the handle
788
788
+
let (resolved_did, _) = redis_resolver.resolve(test_handle).await.unwrap();
789
789
+
assert_eq!(resolved_did, test_did);
790
790
+
791
791
+
// Test that uppercase handles are normalized
792
792
+
redis_resolver.set("DAVE.BSKY.SOCIAL", "did:plc:dave456").await.unwrap();
793
793
+
let (resolved_did2, _) = redis_resolver.resolve("dave.bsky.social").await.unwrap();
794
794
+
assert_eq!(resolved_did2, "did:plc:dave456");
795
795
+
796
796
+
// Verify both forward and reverse mappings exist
797
797
+
if let Ok(mut conn) = pool.get().await {
798
798
+
let mut h = MetroHash64::default();
799
799
+
h.write(test_handle.as_bytes());
800
800
+
let handle_key = format!("{}{}", test_prefix, h.finish());
801
801
+
802
802
+
let mut h2 = MetroHash64::default();
803
803
+
h2.write(test_did.as_bytes());
804
804
+
let did_key = format!("{}{}", test_prefix, h2.finish());
805
805
+
806
806
+
// Check both keys exist
807
807
+
let handle_exists: bool = conn.exists(&handle_key).await.unwrap();
808
808
+
assert!(handle_exists, "Handle key should exist after set");
809
809
+
810
810
+
let did_exists: bool = conn.exists(&did_key).await.unwrap();
811
811
+
assert!(did_exists, "DID key should exist after set");
812
812
+
813
813
+
// Clean up test data
814
814
+
let _: Result<(), _> = conn.del(&[&handle_key, &did_key]).await;
678
815
}
679
816
}
680
817
+34
src/handle_resolver/traits.rs
···
77
77
async fn purge(&self, _subject: &str) -> Result<(), HandleResolverError> {
78
78
Ok(())
79
79
}
80
80
+
81
81
+
/// Set a handle-to-DID mapping in the cache.
82
82
+
///
83
83
+
/// This method allows manually setting or updating a cached mapping between
84
84
+
/// a handle and its corresponding DID. This is useful for pre-populating
85
85
+
/// caches or updating stale entries.
86
86
+
///
87
87
+
/// # Arguments
88
88
+
///
89
89
+
/// * `handle` - The handle to cache (e.g., "alice.bsky.social")
90
90
+
/// * `did` - The DID to associate with the handle (e.g., "did:plc:xyz123")
91
91
+
///
92
92
+
/// # Returns
93
93
+
///
94
94
+
/// Ok(()) if the mapping was successfully set or if the implementation
95
95
+
/// doesn't support manual cache updates. Most implementations will simply
96
96
+
/// return Ok(()) as a no-op.
97
97
+
///
98
98
+
/// # Default Implementation
99
99
+
///
100
100
+
/// The default implementation is a no-op that always returns Ok(()).
101
101
+
async fn set(&self, _handle: &str, _did: &str) -> Result<(), HandleResolverError> {
102
102
+
Ok(())
103
103
+
}
80
104
}
81
105
82
106
#[cfg(test)]
···
102
126
assert!(resolver.purge("alice.bsky.social").await.is_ok());
103
127
assert!(resolver.purge("did:plc:xyz123").await.is_ok());
104
128
assert!(resolver.purge("").await.is_ok());
129
129
+
}
130
130
+
131
131
+
#[tokio::test]
132
132
+
async fn test_default_set_implementation() {
133
133
+
let resolver = NoOpTestResolver;
134
134
+
135
135
+
// Default implementation should always return Ok(())
136
136
+
assert!(resolver.set("alice.bsky.social", "did:plc:xyz123").await.is_ok());
137
137
+
assert!(resolver.set("bob.example.com", "did:web:example.com").await.is_ok());
138
138
+
assert!(resolver.set("", "").await.is_ok());
105
139
}
106
140
}