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.

refactor: reducing log level for soft lookup failures

+98 -13
+98 -13
src/handle_resolver_task.rs
··· 44 44 pub total_succeeded: std::sync::atomic::AtomicU64, 45 45 pub total_failed: std::sync::atomic::AtomicU64, 46 46 pub total_cached: std::sync::atomic::AtomicU64, 47 + pub total_not_found: std::sync::atomic::AtomicU64, 47 48 } 48 49 49 50 /// Handle resolver task processor ··· 140 141 .metrics 141 142 .total_cached 142 143 .load(std::sync::atomic::Ordering::Relaxed), 144 + total_not_found = self 145 + .metrics 146 + .total_not_found 147 + .load(std::sync::atomic::Ordering::Relaxed), 143 148 "Handle resolver task processor stopped" 144 149 ); 145 150 146 151 Ok(()) 152 + } 153 + 154 + /// Check if an error represents a soft failure (handle not found) 155 + /// rather than a real error condition. 156 + /// 157 + /// These atproto_identity library errors indicate the handle doesn't support 158 + /// the specific resolution method, which is normal and expected: 159 + /// - error-atproto-identity-resolve-4: DNS resolution failed (no records) 160 + /// - error-atproto-identity-resolve-5: HTTP resolution failed (hostname not found) 161 + fn is_soft_failure(error_str: &str) -> bool { 162 + // Check for specific atproto_identity error codes that indicate "not found" 163 + // rather than actual failures 164 + if error_str.starts_with("error-atproto-identity-resolve-4") { 165 + // DNS resolution - check if it's a "no records" scenario 166 + error_str.contains("NoRecordsFound") 167 + } else if error_str.starts_with("error-atproto-identity-resolve-5") { 168 + // HTTP resolution - check if it's a hostname lookup failure 169 + error_str.contains("No address associated with hostname") || 170 + error_str.contains("failed to lookup address information") 171 + } else { 172 + false 173 + } 147 174 } 148 175 149 176 /// Process a single handle resolution work item ··· 201 228 ); 202 229 } 203 230 Ok(Err(e)) => { 204 - self.metrics 205 - .total_failed 206 - .fetch_add(1, std::sync::atomic::Ordering::Relaxed); 231 + let error_str = e.to_string(); 232 + 233 + if Self::is_soft_failure(&error_str) { 234 + // This is a soft failure - handle simply doesn't support this resolution method 235 + self.metrics 236 + .total_not_found 237 + .fetch_add(1, std::sync::atomic::Ordering::Relaxed); 238 + 239 + // Publish not-found metrics 240 + self.metrics_publisher 241 + .incr("task.handle_resolution.not_found") 242 + .await; 207 243 208 - // Publish failure metrics 209 - self.metrics_publisher 210 - .incr("task.handle_resolution.failed") 211 - .await; 244 + debug!( 245 + handle = %work.handle, 246 + error = %error_str, 247 + duration_ms = duration_ms, 248 + "Handle not found (soft failure)" 249 + ); 250 + } else { 251 + // This is a real error 252 + self.metrics 253 + .total_failed 254 + .fetch_add(1, std::sync::atomic::Ordering::Relaxed); 255 + 256 + // Publish failure metrics 257 + self.metrics_publisher 258 + .incr("task.handle_resolution.failed") 259 + .await; 212 260 213 - error!( 214 - handle = %work.handle, 215 - error = %e, 216 - duration_ms = duration_ms, 217 - "Handle resolution failed" 218 - ); 261 + error!( 262 + handle = %work.handle, 263 + error = %error_str, 264 + duration_ms = duration_ms, 265 + "Handle resolution failed" 266 + ); 267 + } 219 268 } 220 269 Err(_) => { 221 270 self.metrics ··· 406 455 assert_eq!(metrics.total_succeeded.load(Ordering::Relaxed), 0); 407 456 assert_eq!(metrics.total_failed.load(Ordering::Relaxed), 0); 408 457 assert_eq!(metrics.total_cached.load(Ordering::Relaxed), 0); 458 + assert_eq!(metrics.total_not_found.load(Ordering::Relaxed), 0); 409 459 410 460 // Test incrementing 411 461 metrics.total_processed.fetch_add(1, Ordering::Relaxed); ··· 415 465 assert_eq!(metrics.total_processed.load(Ordering::Relaxed), 1); 416 466 assert_eq!(metrics.total_succeeded.load(Ordering::Relaxed), 1); 417 467 assert_eq!(metrics.total_cached.load(Ordering::Relaxed), 1); 468 + } 469 + 470 + #[test] 471 + fn test_is_soft_failure() { 472 + // Test DNS NoRecordsFound pattern (error-atproto-identity-resolve-4) 473 + let dns_no_records = "error-atproto-identity-resolve-4 DNS resolution failed: ResolveError { kind: Proto(ProtoError { kind: NoRecordsFound { query: Query { name: Name(\"_atproto.noahshachtman.bsky.social.railway.internal.\"), query_type: TXT, query_class: IN }, soa: None, ns: None, negative_ttl: None, response_code: NotImp, trusted: true, authorities: None } }) }"; 474 + assert!(HandleResolverTask::is_soft_failure(dns_no_records)); 475 + 476 + // Test HTTP hostname not found pattern (error-atproto-identity-resolve-5) 477 + let http_no_hostname = "error-atproto-identity-resolve-5 HTTP resolution failed: reqwest::Error { kind: Request, url: \"https://mattie.thegem.city/.well-known/atproto-did\", source: hyper_util::client::legacy::Error(Connect, ConnectError(\"dns error\", Custom { kind: Uncategorized, error: \"failed to lookup address information: No address associated with hostname\" })) }"; 478 + assert!(HandleResolverTask::is_soft_failure(http_no_hostname)); 479 + 480 + // Test alternate HTTP hostname failure message 481 + let http_lookup_failed = "error-atproto-identity-resolve-5 HTTP resolution failed: reqwest::Error { kind: Request, url: \"https://example.com/.well-known/atproto-did\", source: hyper_util::client::legacy::Error(Connect, ConnectError(\"dns error\", Custom { kind: Uncategorized, error: \"failed to lookup address information\" })) }"; 482 + assert!(HandleResolverTask::is_soft_failure(http_lookup_failed)); 483 + 484 + // Test DNS error that is NOT a soft failure (different DNS error) 485 + let dns_real_error = "error-atproto-identity-resolve-4 DNS resolution failed: timeout"; 486 + assert!(!HandleResolverTask::is_soft_failure(dns_real_error)); 487 + 488 + // Test HTTP error that is NOT a soft failure (connection timeout) 489 + let http_timeout = "error-atproto-identity-resolve-5 HTTP resolution failed: connection timeout"; 490 + assert!(!HandleResolverTask::is_soft_failure(http_timeout)); 491 + 492 + // Test HTTP error that is NOT a soft failure (500 error) 493 + let http_500 = "error-atproto-identity-resolve-5 HTTP resolution failed: status code 500"; 494 + assert!(!HandleResolverTask::is_soft_failure(http_500)); 495 + 496 + // Test QuickDID errors should never be soft failures 497 + let quickdid_error = "error-quickdid-resolve-1 Failed to resolve subject: internal server error"; 498 + assert!(!HandleResolverTask::is_soft_failure(quickdid_error)); 499 + 500 + // Test other atproto_identity error codes should not be soft failures 501 + let other_atproto_error = "error-atproto-identity-resolve-1 Some other error"; 502 + assert!(!HandleResolverTask::is_soft_failure(other_atproto_error)); 418 503 } 419 504 }