mirror of
https://relay.ngit.dev/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit-grasp.git
synced 2026-10-06 15:38:25 +00:00
A global five-second cursor makes descendant recovery latency scale with every relay, while advancing at request admission can skip a filter whose queued or active subscription later fails. Permanent coverage also needs a historic baseline because limit:0 protects only events arriving after admission. Give each constrained relay one EOSE-confirmed fallback cursor. Every five-second maintenance pass may start one filter per constrained relay, but never a second while that relay has a queued or active descendant batch. Successful terminal completion advances the cursor; failure retains the same filter. Later requests begin at the last successful upper bound minus fifteen minutes. All work uses the existing transient permit queue without request-class priority. Pair every permanent live admission with complete descendant history, and augment ordinary core historic batches with direct-member filters already known locally. A test-only NIP-11 subscription limit forces fallback end to end; a normal source proves retained live delivery. Large frontiers are verified to split across several grouped subscriptions. Cursors remain session-local, descendants remain non-recursive, core and auxiliary filter packing remain separate, and multi-connection sharding is excluded. Correctness assumes failed or CLOSED batches are removed through the existing terminal paths, which now release the matching in-flight cursor without advancing it. Validated locally with cargo check --lib, cargo test --lib (673 passed), both descendant integration scenarios, and the large-frontier grouping test. Archive production at the pre-amend behavior-identical tip installed permanent coverage, split six filters across two subscriptions, and rotated constrained Shakespeare filters sequentially with successful terminal results. The amendment only corrects the terminal log label so it also describes the historic baseline used by permanent coverage.
167 lines
5.9 KiB
Rust
167 lines
5.9 KiB
Rust
//! Scheduled repository-descendant synchronization scenarios.
|
|
|
|
use std::time::Duration;
|
|
|
|
use nostr_sdk::prelude::*;
|
|
|
|
use crate::common::{sync_helpers::*, TestRelay};
|
|
|
|
async fn wait_for_log(path: &std::path::Path, needle: &str, timeout: Duration) -> bool {
|
|
let deadline = tokio::time::Instant::now() + timeout;
|
|
loop {
|
|
if std::fs::read_to_string(path)
|
|
.unwrap_or_default()
|
|
.contains(needle)
|
|
{
|
|
return true;
|
|
}
|
|
if tokio::time::Instant::now() >= deadline {
|
|
return false;
|
|
}
|
|
tokio::time::sleep(Duration::from_millis(100)).await;
|
|
}
|
|
}
|
|
|
|
/// Events which reference a direct repository-thread member, but not the
|
|
/// repository or its root event, are recovered by the scheduled historic
|
|
/// pass. The recovered events do not recursively extend the frontier.
|
|
#[tokio::test]
|
|
async fn historic_sync_recovers_one_generation_of_parent_only_descendants() {
|
|
let source = TestRelay::start_with_relay_max_subscriptions(5).await;
|
|
let keys = Keys::generate();
|
|
let repo_id = "scheduled-descendant-history";
|
|
|
|
// Seed the source before the syncing relay exists so this exercises the
|
|
// complete historic rotation, not ordinary live coverage.
|
|
let source_domains = [source.domain()];
|
|
let source_refs = source_domains
|
|
.iter()
|
|
.map(String::as_str)
|
|
.collect::<Vec<_>>();
|
|
let (_announcement, _source_git) =
|
|
setup_announcement_on_relay(&source, &keys, &source_refs, repo_id).await;
|
|
let source_client = TestClient::new(source.url(), keys.clone())
|
|
.await
|
|
.expect("connect to source relay");
|
|
let issue =
|
|
build_layer2_issue_event(&keys, &repo_coord(&keys, repo_id), "Repository thread root")
|
|
.expect("build issue");
|
|
let direct_reply = build_layer3_reply_with_e_tag(&keys, &issue.id, "Direct reply")
|
|
.expect("build direct reply");
|
|
let parent_only = build_layer3_reply_with_e_tag(
|
|
&keys,
|
|
&direct_reply.id,
|
|
"Reply visible only through the scheduled descendant query",
|
|
)
|
|
.expect("build parent-only descendant");
|
|
let recursive = build_layer3_reply_with_e_tag(
|
|
&keys,
|
|
&parent_only.id,
|
|
"Deliberately out-of-scope recursive descendant",
|
|
)
|
|
.expect("build recursive descendant");
|
|
for event in [&issue, &direct_reply, &parent_only, &recursive] {
|
|
source_client
|
|
.send_event(event)
|
|
.await
|
|
.expect("seed source event");
|
|
}
|
|
|
|
let syncing = TestRelay::start_with_sync(None).await;
|
|
let domains = [source.domain(), syncing.domain()];
|
|
let domain_refs = domains.iter().map(String::as_str).collect::<Vec<_>>();
|
|
let (_target_announcement, _target_git) =
|
|
setup_announcement_on_relay(&syncing, &keys, &domain_refs, repo_id).await;
|
|
|
|
assert!(
|
|
wait_for_event_on_relay(
|
|
syncing.url(),
|
|
Filter::new().id(parent_only.id),
|
|
Duration::from_secs(20),
|
|
)
|
|
.await,
|
|
"scheduled historic rotation should recover a parent-only descendant"
|
|
);
|
|
assert!(
|
|
wait_for_log(
|
|
&syncing.log_path(),
|
|
"Started queued descendant fallback query",
|
|
Duration::from_secs(5),
|
|
)
|
|
.await,
|
|
"the constrained source should exercise EOSE-closing fallback"
|
|
);
|
|
assert!(
|
|
!wait_for_event_on_relay(
|
|
syncing.url(),
|
|
Filter::new().id(recursive.id),
|
|
Duration::from_secs(7),
|
|
)
|
|
.await,
|
|
"recovered descendants must not recursively extend the frontier"
|
|
);
|
|
|
|
source_client.disconnect().await;
|
|
syncing.stop().await;
|
|
source.stop().await;
|
|
}
|
|
|
|
/// A relay with spare NIP-11 subscription capacity receives permanent
|
|
/// descendant coverage, so a later parent-only event arrives without waiting
|
|
/// for the fallback rotation.
|
|
#[tokio::test]
|
|
async fn descendant_live_coverage_is_preferred_when_capacity_remains() {
|
|
let source = TestRelay::start().await;
|
|
let keys = Keys::generate();
|
|
let repo_id = "live-descendant-coverage";
|
|
let source_domains = [source.domain()];
|
|
let source_refs = source_domains
|
|
.iter()
|
|
.map(String::as_str)
|
|
.collect::<Vec<_>>();
|
|
let (_announcement, _source_git) =
|
|
setup_announcement_on_relay(&source, &keys, &source_refs, repo_id).await;
|
|
let source_client = TestClient::new(source.url(), keys.clone())
|
|
.await
|
|
.expect("connect to source relay");
|
|
let issue = build_layer2_issue_event(&keys, &repo_coord(&keys, repo_id), "Live root")
|
|
.expect("build issue");
|
|
let direct_reply = build_layer3_reply_with_e_tag(&keys, &issue.id, "Direct reply")
|
|
.expect("build direct reply");
|
|
source_client.send_event(&issue).await.unwrap();
|
|
source_client.send_event(&direct_reply).await.unwrap();
|
|
|
|
let syncing = TestRelay::start_with_sync(None).await;
|
|
let domains = [source.domain(), syncing.domain()];
|
|
let domain_refs = domains.iter().map(String::as_str).collect::<Vec<_>>();
|
|
let (_target_announcement, _target_git) =
|
|
setup_announcement_on_relay(&syncing, &keys, &domain_refs, repo_id).await;
|
|
assert!(
|
|
wait_for_log(
|
|
&syncing.log_path(),
|
|
"Installed auxiliary descendant live coverage",
|
|
Duration::from_secs(20),
|
|
)
|
|
.await,
|
|
"spare source capacity should retain descendant coverage"
|
|
);
|
|
|
|
let parent_only =
|
|
build_layer3_reply_with_e_tag(&keys, &direct_reply.id, "Arrives through live coverage")
|
|
.expect("build parent-only descendant");
|
|
source_client.send_event(&parent_only).await.unwrap();
|
|
assert!(
|
|
wait_for_event_on_relay(
|
|
syncing.url(),
|
|
Filter::new().id(parent_only.id),
|
|
Duration::from_secs(10),
|
|
)
|
|
.await,
|
|
"parent-only descendant should arrive through retained live coverage"
|
|
);
|
|
|
|
source_client.disconnect().await;
|
|
syncing.stop().await;
|
|
source.stop().await;
|
|
}
|