cli

Command-line interface for Radroots
git clone https://radroots.dev/git/cli.git
Log | Files | Refs | README | LICENSE

commit fb4b11e7f00721b20abdce710c217e259317b59f
parent f278d9d9eada740291deac9ad2c9ec5692875db0
Author: triesap <tyson@radroots.org>
Date:   Sat, 16 May 2026 23:25:12 +0000

cli: report partial relay fetches

Diffstat:
Msrc/runtime/sync.rs | 94+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
1 file changed, 90 insertions(+), 4 deletions(-)

diff --git a/src/runtime/sync.rs b/src/runtime/sync.rs @@ -301,6 +301,10 @@ where &executor, &sync_record_from_ingest(scope, &config.relay.urls, &receipt, &ingest, started_at)?, )?; + let failed_relays = relay_failures(receipt.failed_relays); + let failed_count = ingest.failed_count + failed_relays.len(); + let reason_code = relay_ingest_reason_code(&ingest, &failed_relays).map(str::to_owned); + let reason = relay_ingest_reason(&ingest, &failed_relays); let freshness = freshness_for_scope_from_executor(config, &executor, scope)?; let queue = radroots_replica_sync_status(&executor)?; @@ -320,17 +324,17 @@ where target_relays: receipt.target_relays, connected_relays: receipt.connected_relays, acknowledged_relays: Vec::new(), - failed_relays: relay_failures(receipt.failed_relays), + failed_relays, fetched_count: Some(ingest.fetched_count), ingested_count: Some(ingest.ingested_count), publishable_count: None, published_count: None, skipped_count: Some(ingest.skipped_count), unsupported_count: Some(ingest.unsupported_count), - failed_count: Some(ingest.failed_count), + failed_count: Some(failed_count), publish_plan: None, - reason_code: ingest.reason_code().map(str::to_owned), - reason: ingest.reason(), + reason_code, + reason, actions: vec![scope.ready_action().to_owned()], }) } @@ -1354,6 +1358,46 @@ impl RelayIngestCounts { } } +fn relay_ingest_reason_code( + ingest: &RelayIngestCounts, + failed_relays: &[RelayFailureView], +) -> Option<&'static str> { + ingest + .reason_code() + .or_else(|| (!failed_relays.is_empty()).then_some("relay_fetch_partial")) +} + +fn relay_ingest_reason( + ingest: &RelayIngestCounts, + failed_relays: &[RelayFailureView], +) -> Option<String> { + let mut parts = Vec::new(); + if let Some(reason) = ingest.reason() { + parts.push(reason); + } + if !failed_relays.is_empty() { + parts.push(format!( + "{} relay(s) failed during fetch: {}", + failed_relays.len(), + relay_failure_reason(failed_relays) + )); + } + + if parts.is_empty() { + None + } else { + Some(parts.join("; ")) + } +} + +fn relay_failure_reason(failed_relays: &[RelayFailureView]) -> String { + failed_relays + .iter() + .map(|failure| format!("{}: {}", failure.relay, failure.reason)) + .collect::<Vec<_>>() + .join("; ") +} + #[derive(Debug, Clone, Copy)] pub(crate) enum RelayIngestScope { SyncPull, @@ -2011,6 +2055,48 @@ mod tests { } #[test] + fn sync_pull_reports_partial_relay_fetch_reason_code() { + let dir = tempdir().expect("tempdir"); + let config = sample_config( + dir.path(), + vec![ + "wss://relay-a.example.com".to_owned(), + "wss://relay-b.example.com".to_owned(), + ], + ); + crate::runtime::local::init(&config).expect("store init"); + let seller = identity(13); + + let view = pull_with_fetcher(&config, |relays, _| { + Ok(DirectRelayFetchReceipt { + target_relays: relays.to_vec(), + connected_relays: vec![relays[0].clone()], + failed_relays: vec![DirectRelayFailure { + relay: relays[1].clone(), + reason: "connection refused".to_owned(), + }], + events: vec![listing_event(&seller)], + }) + }) + .expect("sync pull partial relay fetch"); + + assert_eq!(view.state, "ready"); + assert_eq!(view.connected_relays, vec!["wss://relay-a.example.com"]); + assert_eq!(view.failed_relays.len(), 1); + assert_eq!(view.failed_count, Some(1)); + assert_eq!(view.reason_code.as_deref(), Some("relay_fetch_partial")); + assert!( + view.reason + .as_deref() + .expect("partial relay reason") + .contains("relay(s) failed during fetch") + ); + let run = view.freshness.run.as_ref().expect("run freshness"); + assert_eq!(run.last_state, "partial"); + assert_eq!(run.failed_count, Some(1)); + } + + #[test] fn sync_pull_reports_no_overwrite_skips_without_replacing_projection() { let dir = tempdir().expect("tempdir"); let config = sample_config(dir.path(), vec!["wss://relay.example.com".to_owned()]);