swactor/crates/distribution/tests/t_diag_iroh_driver.rs

318 lines
10 KiB
Rust
Raw Normal View History

2026-05-20 07:41:30 +00:00
//! Tier-2 connection-cache + NodeMap delta tracking (S7).
//!
//! Verifies two things the iroh driver promises after S7:
//!
//! 1. **Every peer-addr handoff to iroh is surfaced.** Each time the
//! driver feeds iroh a relay URL (or a direct address) for a peer,
//! a [`Event::NodeMapUpdate`] event is emitted with the source the
//! addr came from (`join_seed`, `swim_metadata`,
//! `explicit_relay_cache`, `home_relay_fallback`).
//!
//! 2. **Tier-2 snapshots carry the per-peer cache lifecycle.** Once
//! a connection has been opened and used, the snapshot's
//! `body.iroh.connection_cache` block contains an entry for the
//! peer with `generation >= 1`, `created_at_ms` populated, and
//! `last_successful_send_at_ms` populated.
//!
//! These are behavioural checks: the test asserts on the shape of
//! the event stream and snapshot, not on exact timestamps or
//! generation counts (which can wobble with iroh's dynamics).
#![cfg(feature = "iroh")]
mod common;
use std::sync::Arc;
use std::time::Duration;
use common::iroh::*;
use distribution::diagnostics::{
Aggregator, Event, Identity, InMemorySink, Role, SnapshotTrigger,
};
use distribution::diagnostics::iroh_introspect::IntrospectConfig;
use distribution::iroh_driver::IrohDriver;
use distribution::types::NodeId;
use iroh::PublicKey;
fn install_full_diagnostics(
driver: &mut IrohDriver,
run_id: &str,
) -> (Arc<InMemorySink>, Arc<Aggregator<Arc<InMemorySink>>>) {
let sink = Arc::new(InMemorySink::new());
let identity = Identity::new(driver.node_id(), Role::stage(), run_id);
let aggregator = Arc::new(Aggregator::new(identity, sink.clone()));
driver.install_diagnostics_with_config(
aggregator.clone(),
IntrospectConfig {
scrape_interval: Duration::from_millis(50),
},
);
(sink, aggregator)
}
fn node_hex(id: &NodeId) -> String {
let mut s = String::with_capacity(64);
for b in id.0 {
s.push_str(&format!("{:02x}", b));
}
s
}
#[test]
fn join_emits_node_map_update_for_seed_addr() {
let mut a = make_driver();
let mut b = make_driver();
let (sink_a, _agg_a) = install_full_diagnostics(&mut a, "run-s7-join");
let _ = install_full_diagnostics(&mut b, "run-s7-join");
let b_id = b.node_id();
let b_addr = b.endpoint_addr();
a.join(&[b_addr]);
// No pumping required — the NodeMapUpdate emit happens inside
// join() before any async work is kicked off.
let saw = sink_a.records().iter().any(|r| {
matches!(
&r.event,
Event::NodeMapUpdate { peer, from_source, accepted }
if peer == &b_id
&& (from_source == "join_seed"
|| from_source == "join_seed_direct")
&& *accepted
)
});
a.shutdown();
b.shutdown();
assert!(
saw,
"join() should emit NodeMapUpdate with from_source in \
{{join_seed, join_seed_direct}} for the seed peer",
);
}
#[test]
fn join_with_bare_seed_emits_no_node_map_update() {
let mut a = make_driver();
let (sink_a, _agg_a) = install_full_diagnostics(&mut a, "run-s7-bare");
// Build a peer addr with neither relay nor direct addresses —
// just the public key. iroh has nothing new to learn here, so
// we shouldn't emit a NodeMapUpdate either.
let bare_key = iroh::SecretKey::from_bytes(&[0xcd; 32]).public();
let bare_addr = iroh::EndpointAddr::new(bare_key);
let bare_id = NodeId(*bare_key.as_bytes());
a.join(&[bare_addr]);
let saw = sink_a
.records()
.iter()
.any(|r| matches!(&r.event, Event::NodeMapUpdate { peer, .. } if peer == &bare_id));
a.shutdown();
assert!(
!saw,
"bare-key join() should NOT emit NodeMapUpdate — iroh has \
no new address to learn",
);
}
#[test]
fn cache_aggregate_appears_in_tier2_snapshot_after_successful_send() {
let mut a = make_driver();
let mut b = make_driver();
let (sink_a, agg_a) = install_full_diagnostics(&mut a, "run-s7-cache");
let _ = install_full_diagnostics(&mut b, "run-s7-cache");
let a_id = a.node_id();
let b_id = b.node_id();
let b_hex = node_hex(&b_id);
let b_addr = b.endpoint_addr();
a.join(&[b_addr]);
let converged = pump_until_pair(
&mut a,
&mut b,
Duration::from_secs(5),
|a, b| {
let a_key = PublicKey::from_bytes(&a.node_id().0).unwrap();
let b_key = PublicKey::from_bytes(&b.node_id().0).unwrap();
sees_alive(a, &b_key) && sees_alive(b, &a_key)
},
);
assert!(converged, "nodes did not converge within 5s");
// Pump a few more rounds so SWIM probes (which exercise
// send_message → note_send_success on the cached connection)
// run beyond the initial join handshake.
for _ in 0..10 {
pump_one(&mut a);
pump_one(&mut b);
std::thread::sleep(Duration::from_millis(20));
}
a.force_iroh_introspect_refresh();
let snap = agg_a.snapshot(SnapshotTrigger::OnDemand);
a.shutdown();
b.shutdown();
let iroh = snap
.body
.iroh
.as_ref()
.expect("snapshot must include tier-2 iroh block");
let entry = iroh
.connection_cache
.iter()
.find(|e| e.peer_node_id_hex == b_hex)
.unwrap_or_else(|| {
panic!(
"tier-2 snapshot connection_cache should include B \
(= {}); got entries: {:#?}",
b_hex, iroh.connection_cache,
);
});
assert!(
entry.generation >= 1,
"expected cache generation >= 1 after a successful dial; \
got {} (entry: {:#?})",
entry.generation,
entry,
);
assert!(
entry.created_at_ms.is_some(),
"expected created_at_ms to be populated after a successful \
dial; got {:#?}",
entry,
);
assert!(
entry.last_successful_send_at_ms.is_some(),
"expected last_successful_send_at_ms to be populated after a \
message round-trip; got {:#?}",
entry,
);
// Sanity: a's own id should not appear (the cache only tracks
// outbound peers).
let a_hex = node_hex(&a_id);
assert!(
!iroh.connection_cache.iter().any(|e| e.peer_node_id_hex == a_hex),
"connection_cache should not list the local node itself",
);
// Confirm that the NodeMapUpdate emit fired on the get_or_connect
// dial path too — once SWIM probes start, the driver feeds iroh
// a relay URL resolved from one of its caches.
let saw_dial_path_nmu = sink_a.records().iter().any(|r| {
matches!(
&r.event,
Event::NodeMapUpdate { peer, from_source, accepted }
if peer == &b_id
&& (from_source == "explicit_relay_cache"
|| from_source == "swim_metadata"
|| from_source == "home_relay_fallback")
&& *accepted
)
});
// Without a relay configured (RelayMode::Disabled) the home-relay
// path is not available; if all caches are empty we'd skip the
// emit entirely. Accept either: a NodeMapUpdate from the dial
// path *or* the join-seed one we already asserted on.
let saw_join_seed_nmu = sink_a.records().iter().any(|r| {
matches!(
&r.event,
Event::NodeMapUpdate { peer, from_source, .. }
if peer == &b_id
&& (from_source == "join_seed"
|| from_source == "join_seed_direct")
)
});
assert!(
saw_dial_path_nmu || saw_join_seed_nmu,
"expected at least one NodeMapUpdate event for B (either from \
join() or from the dial path); got {:#?}",
sink_a
.records()
.iter()
.filter_map(|r| match &r.event {
Event::NodeMapUpdate { peer, from_source, accepted } =>
Some((peer, from_source.clone(), *accepted)),
_ => None,
})
.collect::<Vec<_>>(),
);
}
#[test]
fn generation_in_events_matches_snapshot_aggregate() {
let mut a = make_driver();
let mut b = make_driver();
let (sink_a, agg_a) = install_full_diagnostics(&mut a, "run-s7-gen");
let _ = install_full_diagnostics(&mut b, "run-s7-gen");
let b_id = b.node_id();
let b_hex = node_hex(&b_id);
let b_addr = b.endpoint_addr();
a.join(&[b_addr]);
let converged = pump_until_pair(
&mut a,
&mut b,
Duration::from_secs(5),
|a, b| {
let a_key = PublicKey::from_bytes(&a.node_id().0).unwrap();
let b_key = PublicKey::from_bytes(&b.node_id().0).unwrap();
sees_alive(a, &b_key) && sees_alive(b, &a_key)
},
);
assert!(converged, "nodes did not converge within 5s");
// A few more rounds so we get a ConnectionCacheHit event recorded
// against the established connection.
for _ in 0..10 {
pump_one(&mut a);
pump_one(&mut b);
std::thread::sleep(Duration::from_millis(20));
}
a.force_iroh_introspect_refresh();
let snap = agg_a.snapshot(SnapshotTrigger::OnDemand);
a.shutdown();
b.shutdown();
// Find the highest generation reported in any per-touch event
// for B and compare against the snapshot aggregate. They should
// agree — the post-processor depends on this invariant when
// stitching events to snapshots.
let max_event_gen = sink_a
.records()
.iter()
.filter_map(|r| match &r.event {
Event::ConnectionCacheHit { peer, generation }
| Event::ConnectionCacheInvalidated { peer, generation, .. }
if peer == &b_id =>
{
Some(*generation)
}
_ => None,
})
.max()
.expect("expected at least one cache-touch event for B");
let iroh = snap.body.iroh.as_ref().expect("tier-2 iroh block");
let entry = iroh
.connection_cache
.iter()
.find(|e| e.peer_node_id_hex == b_hex)
.expect("snapshot connection_cache must include B");
assert!(
entry.generation >= max_event_gen,
"snapshot generation ({}) should be >= the highest generation \
seen on the event stream ({})",
entry.generation,
max_event_gen,
);
}