Constellation, Spacedust, Slingshot, UFOs: atproto crates and services for microcosm

slingshot cache hit rates

+58 -30
+1 -1
slingshot/api-description.md
··· 1 _A [gravitational slingshot](https://en.wikipedia.org/wiki/Gravity_assist) makes use of the gravity and relative movements of celestial bodies to accelerate a spacecraft and change its trajectory._ 2 3 4 - # Slingshot: edge record cache 5 6 Applications in [ATProtocol](https://atproto.com/) store data in users' own [PDS](https://atproto.com/guides/self-hosting) (Personal Data Server), which are distributed across thousands of independently-run servers all over the world. Trying to access this data poses challenges for client applications: 7
··· 1 _A [gravitational slingshot](https://en.wikipedia.org/wiki/Gravity_assist) makes use of the gravity and relative movements of celestial bodies to accelerate a spacecraft and change its trajectory._ 2 3 4 + # Slingshot: edge record and identity cache 5 6 Applications in [ATProtocol](https://atproto.com/) store data in users' own [PDS](https://atproto.com/guides/self-hosting) (Personal Data Server), which are distributed across thousands of independently-run servers all over the world. Trying to access this data poses challenges for client applications: 7
+42 -23
slingshot/src/identity.rs
··· 11 /// 1. handle -> DID resolution: getRecord must accept a handle for `repo` param 12 /// 2. DID -> PDS resolution: so we know where to getRecord 13 /// 3. DID -> handle resolution: for bidirectional handle validation and in case we want to offer this 14 - use std::time::Duration; 15 use tokio::sync::Mutex; 16 use tokio_util::sync::CancellationToken; 17 ··· 264 handle: &Handle, 265 ) -> Result<Option<Did>, IdentityError> { 266 let key = IdentityKey::Handle(handle.clone()); 267 let entry = self 268 .cache 269 .get_or_fetch(&key, { 270 let handle = handle.clone(); 271 let resolver = self.handle_resolver.clone(); 272 || async move { 273 - match resolver.resolve(&handle).await { 274 - Ok(did) => Ok(IdentityVal(UtcDateTime::now(), IdentityData::Did(did))), 275 - Err(atrium_identity::Error::NotFound) => { 276 - Ok(IdentityVal(UtcDateTime::now(), IdentityData::NotFound)) 277 - } 278 Err(other) => { 279 log::debug!("other error resolving handle: {other:?}"); 280 - Err(IdentityError::ResolutionFailed(other)) 281 } 282 - } 283 } 284 }) 285 .await?; ··· 314 did: &Did, 315 ) -> Result<Option<PartialMiniDoc>, IdentityError> { 316 let key = IdentityKey::Did(did.clone()); 317 let entry = self 318 .cache 319 .get_or_fetch(&key, { 320 let did = did.clone(); 321 let resolver = self.did_resolver.clone(); 322 || async move { 323 - match resolver.resolve(&did).await { 324 - Ok(did_doc) => { 325 // TODO: fix in atrium: should verify id is did 326 - if did_doc.id != did.to_string() { 327 - return Err(IdentityError::BadDidDoc( 328 - "did doc's id did not match did".to_string(), 329 - )); 330 - } 331 - let mini_doc = did_doc.try_into().map_err(IdentityError::BadDidDoc)?; 332 - Ok(IdentityVal(UtcDateTime::now(), IdentityData::Doc(mini_doc))) 333 - } 334 - Err(atrium_identity::Error::NotFound) => { 335 - Ok(IdentityVal(UtcDateTime::now(), IdentityData::NotFound)) 336 - } 337 - Err(other) => Err(IdentityError::ResolutionFailed(other)), 338 - } 339 } 340 }) 341 .await?;
··· 11 /// 1. handle -> DID resolution: getRecord must accept a handle for `repo` param 12 /// 2. DID -> PDS resolution: so we know where to getRecord 13 /// 3. DID -> handle resolution: for bidirectional handle validation and in case we want to offer this 14 + use std::time::{Duration, Instant}; 15 use tokio::sync::Mutex; 16 use tokio_util::sync::CancellationToken; 17 ··· 264 handle: &Handle, 265 ) -> Result<Option<Did>, IdentityError> { 266 let key = IdentityKey::Handle(handle.clone()); 267 + metrics::counter!("slingshot_get_handle").increment(1); 268 let entry = self 269 .cache 270 .get_or_fetch(&key, { 271 let handle = handle.clone(); 272 let resolver = self.handle_resolver.clone(); 273 || async move { 274 + let t0 = Instant::now(); 275 + let (res, success) = match resolver.resolve(&handle).await { 276 + Ok(did) => ( 277 + Ok(IdentityVal(UtcDateTime::now(), IdentityData::Did(did))), 278 + "true", 279 + ), 280 + Err(atrium_identity::Error::NotFound) => ( 281 + Ok(IdentityVal(UtcDateTime::now(), IdentityData::NotFound)), 282 + "false", 283 + ), 284 Err(other) => { 285 log::debug!("other error resolving handle: {other:?}"); 286 + (Err(IdentityError::ResolutionFailed(other)), "false") 287 } 288 + }; 289 + metrics::histogram!("slingshot_fetch_handle", "success" => success) 290 + .record(t0.elapsed()); 291 + res 292 } 293 }) 294 .await?; ··· 323 did: &Did, 324 ) -> Result<Option<PartialMiniDoc>, IdentityError> { 325 let key = IdentityKey::Did(did.clone()); 326 + metrics::counter!("slingshot_get_did_doc").increment(1); 327 let entry = self 328 .cache 329 .get_or_fetch(&key, { 330 let did = did.clone(); 331 let resolver = self.did_resolver.clone(); 332 || async move { 333 + let t0 = Instant::now(); 334 + let (res, success) = match resolver.resolve(&did).await { 335 + Ok(did_doc) if did_doc.id != did.to_string() => ( 336 // TODO: fix in atrium: should verify id is did 337 + Err(IdentityError::BadDidDoc( 338 + "did doc's id did not match did".to_string(), 339 + )), 340 + "false", 341 + ), 342 + Ok(did_doc) => match did_doc.try_into() { 343 + Ok(mini_doc) => ( 344 + Ok(IdentityVal(UtcDateTime::now(), IdentityData::Doc(mini_doc))), 345 + "true", 346 + ), 347 + Err(e) => (Err(IdentityError::BadDidDoc(e)), "false"), 348 + }, 349 + Err(atrium_identity::Error::NotFound) => ( 350 + Ok(IdentityVal(UtcDateTime::now(), IdentityData::NotFound)), 351 + "false", 352 + ), 353 + Err(other) => (Err(IdentityError::ResolutionFailed(other)), "false"), 354 + }; 355 + metrics::histogram!("slingshot_fetch_did_doc", "success" => success) 356 + .record(t0.elapsed()); 357 + res 358 } 359 }) 360 .await?;
+13 -4
slingshot/src/server.rs
··· 9 use std::path::PathBuf; 10 use std::str::FromStr; 11 use std::sync::Arc; 12 use tokio_util::sync::CancellationToken; 13 14 use poem::{ ··· 609 610 let at_uri = format!("at://{}/{}/{}", &*did, &*collection, &*rkey); 611 612 let fr = self 613 .cache 614 .get_or_fetch(&at_uri, { 615 let cid = cid.clone(); 616 let repo_api = self.repo.clone(); 617 - || async move { repo_api.get_record(&did, &collection, &rkey, &cid).await } 618 }) 619 .await; 620 ··· 690 } 691 692 // TODO 693 - // #[oai(path = "/com.atproto.identity.resolveHandle", method = "get")] 694 // #[oai(path = "/com.atproto.identity.resolveDid", method = "get")] 695 // but these are both not specified to do bidirectional validation, which is what we want to offer 696 // com.atproto.identity.resolveIdentity seems right, but requires returning the full did-doc ··· 699 // handle -> verified did + pds url 700 // 701 // we could do horrible things and implement resolveIdentity with only a stripped-down fake did doc 702 - // but this will *definitely* cause problems because eg. we're not currently storing pubkeys and 703 - // those are a little bit important 704 } 705 706 #[derive(Debug, Clone, Serialize)]
··· 9 use std::path::PathBuf; 10 use std::str::FromStr; 11 use std::sync::Arc; 12 + use std::time::Instant; 13 use tokio_util::sync::CancellationToken; 14 15 use poem::{ ··· 610 611 let at_uri = format!("at://{}/{}/{}", &*did, &*collection, &*rkey); 612 613 + metrics::counter!("slingshot_get_record").increment(1); 614 let fr = self 615 .cache 616 .get_or_fetch(&at_uri, { 617 let cid = cid.clone(); 618 let repo_api = self.repo.clone(); 619 + || async move { 620 + let t0 = Instant::now(); 621 + let res = repo_api.get_record(&did, &collection, &rkey, &cid).await; 622 + let success = if res.is_ok() { "true" } else { "false" }; 623 + metrics::histogram!("slingshot_fetch_record", "success" => success) 624 + .record(t0.elapsed()); 625 + res 626 + } 627 }) 628 .await; 629 ··· 699 } 700 701 // TODO 702 // #[oai(path = "/com.atproto.identity.resolveDid", method = "get")] 703 // but these are both not specified to do bidirectional validation, which is what we want to offer 704 // com.atproto.identity.resolveIdentity seems right, but requires returning the full did-doc ··· 707 // handle -> verified did + pds url 708 // 709 // we could do horrible things and implement resolveIdentity with only a stripped-down fake did doc 710 + // but this will *definitely* cause problems probably 711 + // 712 + // resolveMiniDoc gets most of this well enough. 713 } 714 715 #[derive(Debug, Clone, Serialize)]
+2 -2
slingshot/static/index.html
··· 43 <body> 44 <header class="custom-header scalar-app"> 45 <p> 46 - TODO: thing 47 </p> 48 <nav> 49 <b>a <a href="https://microcosm.blue">microcosm</a> project</b> 50 <a href="https://bsky.app/profile/microcosm.blue">@microcosm.blue</a> 51 - <a href="https://github.com/at-microcosm">github</a> 52 </nav> 53 </header> 54
··· 43 <body> 44 <header class="custom-header scalar-app"> 45 <p> 46 + get atproto records and identities faster 47 </p> 48 <nav> 49 <b>a <a href="https://microcosm.blue">microcosm</a> project</b> 50 <a href="https://bsky.app/profile/microcosm.blue">@microcosm.blue</a> 51 + <a href="https://tangled.org/microcosm.blue/microcosm-rs">tangled</a> 52 </nav> 53 </header> 54