a profile or handle change publishes from post-activate without waiting for a push
Signed-off-by: dawn <90008@klbr.net>
939 unmodified linesExpand all940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005939 unmodified linesExpand all "cached handle kept after failed refresh:{fourth_html}" );}
fn active_generation(output: &Path) -> String { let active: serde_json::Value = serde_json::from_slice(&std::fs::read(output.join("active.json")).unwrap()).unwrap(); active["generation"].as_str().unwrap().to_string()}
#[test]fn a_profile_change_alone_gets_one_generation_from_post_activate() { let env = setup_test_env(); assert!(knot_static(&env.config, ["generate"]).status.success()); let first = get_only_generation_dir(&env.output); let first_id = first.file_name().unwrap().to_str().unwrap().to_string(); assert!( knot_static(&env.config, ["activate", &first_id]) .status .success() ); let _ = knot_static(&env.config, ["post-activate"]); assert_eq!(active_generation(&env.output), first_id, "nothing changed");
std::fs::write( env.output.join("owner-handles.json"), serde_json::to_vec(&serde_json::json!({ "version": 1, "entries": { OWNER: { "handle": "ptr.pet", "verified_at_unix": 1_700_000_000u64, "profile": { "description": "hey gemini!" }, } } })) .unwrap(), ) .unwrap(); let _ = knot_static(&env.config, ["post-activate"]); let second_id = active_generation(&env.output); assert_ne!( second_id, first_id, "the profile is published without a push" ); let social = env .output .join("generations") .join(&second_id) .join("www/_atproto/social"); let answered = walkdir(&social).into_iter().any(|file| { std::fs::read_to_string(file) .is_ok_and(|body| body.contains(r#""description":"hey gemini!""#)) }); assert!(answered, "getProfile answers the cached profile");
let before = generation_dirs(&env.output); let _ = knot_static(&env.config, ["post-activate"]); assert_eq!(active_generation(&env.output), second_id); assert_eq!( generation_dirs(&env.output), before, "a second sync builds nothing" );}83 unmodified lines848586878889909192939438 unmodified lines13313413513613713813914014123 unmodified lines83 unmodified lines pub profile: serde_json::Value, pub repos: Vec<RepoEntry>, #[serde(default, skip_serializing_if = "Option::is_none")] pub global_input_digest: Option<String>, /// the handle cache's answers, see `index_page::served_digest` #[serde(default, skip_serializing_if = "Option::is_none")] pub handles_digest: Option<String>, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub rebuilt_repos: Vec<String>, #[serde(default, skip_serializing_if = "is_zero_stats")] pub stats: BuildStats,38 unmodified lines static_version: "test".to_string(), profile: serde_json::json!({ "atproto": {} }), repos: Vec::new(), global_input_digest: None, handles_digest: None, rebuilt_repos: Vec::new(), stats: BuildStats::default(), } }23 unmodified lines286 unmodified linesExpand all287288289290291292293294295296291292293294295296297297298299300301302303304305306307308256 unmodified linesExpand all56556656756856256356456556656957057157257357461 unmodified lines63663763863964064164264364495 unmodified lines286 unmodified linesExpand all }, ) .collect(); trace.record("plan: unchanged pass", t_outcomes); let handles = crate::index_page::served_digest(&crate::index_page::cached_handles(&output_root)); let answers_handles = active .as_ref() .is_some_and(|active| active.manifest.handles_digest.as_deref() == Some(handles.as_str())); if answers_handles if unchanged_shortcut( &snapshots, &outcomes, &carried_scope, &hosted, active.as_ref(), ) { && unchanged_shortcut( &snapshots, &outcomes, &carried_scope, &hosted, active.as_ref(), ) { let generation = match outcomes.first() { Some(RepoOutcome::Unchanged { generation }) => generation.clone(), _ => active .as_ref()256 unmodified linesExpand all .map_err(|_| anyhow::anyhow!("atproto settle panicked"))? })? }; crate::index_page::verify_unseen(&config, &crate::xrpc::social::accounts(&atproto_repos)).await; crate::atproto::emit_with( &build_dir, &atproto_repos, &crate::xrpc::social::Social::of(&config), )?; let social = crate::xrpc::social::Social::of(&config); crate::atproto::emit_with(&build_dir, &atproto_repos, &social)?; crate::blobs::emit(&output_root, &build_dir, &atproto_repos, &captured)?; let git_xrpc_repos: Vec<crate::git_xrpc::SealedRepo> = { let shared_blobs = output_root.join(crate::git_xrpc::SHARED_BLOBS); let inputs: Vec<crate::git_xrpc::RepoInput> = atproto_sources61 unmodified lines }), repos: entries, rebuilt_repos, global_input_digest: Some(global.config_digest.clone()), handles_digest: Some(social.handles_digest.clone()), stats: BuildStats::default(), };
// the top-level exports are mounts. size what is inside them, not the link95 unmodified lines209 unmodified linesExpand all210211212213214215216217218104 unmodified linesExpand all209 unmodified linesExpand all shallow_head: None, moved_wants: Vec::new(), }], global_input_digest: digest, handles_digest: None, stats: BuildStats::default(), } }
104 unmodified linesExpand all381 unmodified linesExpand all382383384385386387388389390194 unmodified linesExpand all381 unmodified linesExpand all static_version: "test".to_string(), profile: serde_json::json!({}), repos: vec![entry(did, fingerprint, Some("release-1"))], global_input_digest: Some("cfg".to_string()), handles_digest: None, rebuilt_repos: rebuilt.iter().map(|s| s.to_string()).collect(), stats: BuildStats::default(), } }194 unmodified linesExpand all1234567855 unmodified lines646566676768697071686970717273747576777879808182838485867687888990919293949596979814 unmodified lines1131141151161179911811912012112240 unmodified lines163164165166167168169170171172173174175365 unmodified linesExpand alluse knot_runtime::Clock;use std::collections::{BTreeMap, BTreeSet};use std::path::Path;use std::sync::Arc;use std::time::Duration;
use anyhow::Context as _;use futures::StreamExt as _;55 unmodified lines )) }}
pub(crate) fn cached_owner_labels( output_root: &Path, owners: &BTreeSet<&str>,) -> BTreeMap<String, String> { let stored = Datastore::at(output_root)/// what a generation answers from the cache. verification times and failures don't countpub(crate) fn served_digest(stored: &StoredHandles) -> String { let served: BTreeMap<&str, (&str, Option<String>, &Option<Map<String, Value>>)> = stored .entries .iter() .map(|(did, entry)| { let answered = (entry.handle.as_str(), entry.avatar_file(), &entry.profile); (did.as_str(), answered) }) .collect(); crate::cas::Digest::of_bytes(&serde_json::to_vec(&served).expect("handles serialize")).hex()}
pub(crate) fn cached_handles(output_root: &Path) -> StoredHandles { Datastore::at(output_root) .record::<schema::OwnerHandles>(&()) .read() .ok() .flatten() .unwrap_or_default(); .unwrap_or_default()}
pub(crate) fn cached_owner_labels( output_root: &Path, owners: &BTreeSet<&str>,) -> BTreeMap<String, String> { let stored = cached_handles(output_root); owners .iter() .map(|&did| { let label = stored14 unmodified lines ..config.http_limits() }}
/// a change the active generation doesn't answer yet gets a generation of its ownpub async fn sync_handles(config: &ValidatedConfig) {pub async fn sync_handles(config: &Arc<ValidatedConfig>) { let Some(current) = crate::publish::current_generation(&config.output_root) else { return; }; let gen_dir = config.output_root.join("generations").join(current);40 unmodified lines refresh_handles_with(&atproto, &pds, &avatars, &named, &mut stored, now).await; if stored != before && record.put(&stored).is_ok() { sweep_avatars(&avatars, &stored); } if manifest.handles_digest.as_deref() != Some(served_digest(&stored).as_str()) && let Err(error) = crate::writes::publish_scoped(config, None).await { tracing::warn!("publish the refreshed handles: {error:#}"); }}
fn refresh_atproto( config: &ValidatedConfig,365 unmodified linesExpand all28 unmodified lines293031323333343536373839656 unmodified linesExpand all28 unmodified lines#[cfg(test)]mod tests;
pub use handles::sync_handles;pub(crate) use handles::{StoredHandles, cached_owner_labels, verify_unseen};pub(crate) use handles::{ StoredHandles, cached_handles, cached_owner_labels, served_digest, verify_unseen,};use handles::{read_activity, read_descriptions};
pub const KAOMOJI: &[&str] = &[ "(˶ᵔ ᵕ ᵔ˶)",656 unmodified linesExpand all32 unmodified lines333435363738394041299 unmodified linesExpand all32 unmodified lines static_version: "fixture".to_string(), profile: serde_json::json!({}), repos: vec![entry("did:plc:fixture")], global_input_digest: None, handles_digest: None, stats: BuildStats::default(), }; manifest.generation_id = manifest.compute_id(); manifest299 unmodified linesExpand all464 unmodified linesExpand all465466467468469470471472473895 unmodified linesExpand all464 unmodified linesExpand all profile: serde_json::json!({}), repos: vec![repo_entry("did:plc:repo")], rebuilt_repos: Vec::new(), global_input_digest: None, handles_digest: None, stats: BuildStats::default(), };
(config, manifest, generation)895 unmodified linesExpand all63 unmodified lines646566676869707172304 unmodified linesExpand all63 unmodified lines .unwrap() .clone(), )] .into(), handles_digest: String::new(), }; crate::atproto::emit_with(&generation, std::slice::from_ref(&settled), &social).unwrap(); crate::blobs::emit(temp.path(), &generation, &[], &Default::default()).unwrap(); crate::nginx::emit_at(&config, &manifest, &generation, &generation).unwrap();304 unmodified linesExpand all53 unmodified lines54555657585960616224 unmodified lines87888990909192939495969192939495969721 unmodified lines119120121122123124125126127421 unmodified linesExpand all53 unmodified lines pub avatars: BTreeMap<String, Avatar>, pub avatars_dir: Option<PathBuf>, /// org.tangled.actor.profile records, without their avatar pub profiles: BTreeMap<String, serde_json::Map<String, Value>>, pub handles_digest: String,}
pub struct Avatar { pub cid: String,24 unmodified lines}
impl Social { pub fn of(config: &ValidatedConfig) -> Self { let entries = crate::datastore::Datastore::at(&config.output_root) .record::<crate::datastore::schema::OwnerHandles>(&()) .read() .ok() .flatten() .unwrap_or_default() .entries; let stored = crate::index_page::cached_handles(&config.output_root); let handles_digest = crate::index_page::served_digest(&stored); let entries = stored.entries; let avatars_dir = config.output_root.join(AVATARS_DIR); let avatars = entries .iter() .filter_map(|(did, entry)| {21 unmodified lines .collect(), avatars, avatars_dir: Some(avatars_dir), profiles, handles_digest, } }
fn handle(&self, did: &str) -> &str {421 unmodified linesExpand all226 unmodified linesExpand all227228229230231232233234235478 unmodified linesExpand all226 unmodified linesExpand all .as_object() .unwrap() .clone(), )]), handles_digest: String::new(), }; let generation = dir.path().join("generation"); crate::xrpc::emit_with(&generation, std::slice::from_ref(&settled), &social).unwrap(); let maps = Served::parse(&generation);478 unmodified linesExpand all