swactor/crates/distribution/tests/t_diag_event_emission.rs
Zachery Aaron Shores-Chmielewski b23f82e9a9 feat(distribution): add diagnostics subsystem
Structured observability for the iroh/SWIM layer: Aggregator, typed Event/Snapshot
types, Sink (NoopSink default), ProbeScheduler, process stats, and host/iroh/swim
introspection, plus the swactor-diag-collector, -postproc, and -iroh-relay binaries
that assemble and render per-run bundles. Generalizes the pipeline-parallel-inference
example to N stages and adds the topology-planner spec.


Signed-off-by: Zachery Aaron Shores-Chmielewski <zacheryasc@gmail.com>
2026-05-20 11:41:30 +04:00

197 lines
6.6 KiB
Rust

//! Verifies that the diagnostics emitter wired through `IrohDriver` and
//! `SwimNode` actually produces the structured events the post-processor
//! relies on (`DIAGNOSTICS_PLAN.md` T1.3).
//!
//! The test forms a two-node iroh cluster with `RelayMode::Disabled`,
//! installs an [`InMemorySink`] on each side via an [`Aggregator`], and
//! pumps until the nodes see each other alive. The recorded event
//! stream is then checked against the minimum vocabulary S4 promises:
//! `SwimTransition`, `DialStarted`, `DialOutcome`, `MessageSent`,
//! `MessageReceived`, plus the `boot` record emitted at aggregator
//! construction.
//!
//! Behavioural assertions only — no specific counts or ordering beyond
//! "monotonic_seq is strictly increasing per node", so the test
//! survives small changes in dial-retry counts or probe cadence.
#![cfg(feature = "iroh")]
mod common;
use std::sync::Arc;
use std::time::Duration;
use common::iroh::*;
use distribution::diagnostics::{
Aggregator, DialOutcome as DiagDialOutcome, Event, Identity, InMemorySink, PeerState, Role,
};
use distribution::iroh_driver::IrohDriver;
use iroh::PublicKey;
/// Wire an `InMemorySink`-backed aggregator into `driver`. The sink is
/// returned so the caller can inspect the event stream afterwards.
fn wire_inmemory_diagnostics(driver: &mut IrohDriver, run_id: &str) -> 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.set_diagnostics(aggregator);
sink
}
#[test]
fn two_node_join_produces_dial_message_and_swim_transition_events() {
let mut a = make_driver();
let mut b = make_driver();
let sink_a = wire_inmemory_diagnostics(&mut a, "run-emission");
let sink_b = wire_inmemory_diagnostics(&mut b, "run-emission");
// A joins B. From A's perspective this triggers dial + message-send;
// B sees an incoming connection + message + a membership transition
// into Alive.
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.shutdown();
b.shutdown();
// ── A's side: dialed B, sent the join, observed B as Alive. ─────
let records_a = sink_a.records();
assert!(
records_a
.iter()
.any(|r| matches!(r.event, Event::DialStarted { .. })),
"A should have emitted at least one DialStarted",
);
assert!(
records_a.iter().any(|r| matches!(
r.event,
Event::DialOutcome {
outcome: DiagDialOutcome::Success,
..
}
)),
"A should have emitted a successful DialOutcome",
);
assert!(
records_a
.iter()
.any(|r| matches!(r.event, Event::MessageSent { .. })),
"A should have emitted at least one MessageSent",
);
assert!(
records_a.iter().any(|r| matches!(
&r.event,
Event::SwimTransition {
to: PeerState::Alive,
..
}
)),
"A should have observed B transitioning to Alive",
);
// ── B's side: accepted A's connection, processed the join,
// observed A as Alive. ────────────────────────────────────────
let records_b = sink_b.records();
assert!(
records_b
.iter()
.any(|r| matches!(r.event, Event::MessageReceived { .. })),
"B should have emitted at least one MessageReceived",
);
assert!(
records_b.iter().any(|r| matches!(
&r.event,
Event::SwimTransition {
to: PeerState::Alive,
..
}
)),
"B should have observed A transitioning to Alive",
);
// ── Boot record sent at aggregator construction. ────────────────
assert_eq!(sink_a.boots().len(), 1, "exactly one boot record on A");
assert_eq!(sink_b.boots().len(), 1, "exactly one boot record on B");
// ── monotonic_seq strictly increases per node. ──────────────────
let mut prev = 0u64;
for rec in &records_a {
assert!(
rec.monotonic_seq > prev,
"A's monotonic_seq must strictly increase (saw {} after {})",
rec.monotonic_seq,
prev,
);
prev = rec.monotonic_seq;
}
}
#[test]
fn dial_to_unreachable_peer_emits_non_success_dial_outcome() {
use iroh::{EndpointAddr, SecretKey};
let mut a = make_driver();
let sink = wire_inmemory_diagnostics(&mut a, "run-unreachable");
// Construct a well-formed but unreachable peer addr: arbitrary
// (but fixed) key, no direct addresses, no relay. With
// `RelayMode::Disabled` iroh has no way to reach this peer and
// the dial must fail.
let unreachable_sk = SecretKey::from_bytes(&[0xab; 32]);
let unreachable_addr = EndpointAddr::new(unreachable_sk.public());
a.join(&[unreachable_addr]);
// Pump until the spawned join task records at least one dial
// attempt, or give up after a few seconds.
let deadline = std::time::Instant::now() + Duration::from_secs(8);
while std::time::Instant::now() < deadline {
pump_one(&mut a);
if sink
.records()
.iter()
.any(|r| matches!(r.event, Event::DialOutcome { .. }))
{
break;
}
std::thread::sleep(Duration::from_millis(50));
}
a.shutdown();
let records = sink.records();
assert!(
records
.iter()
.any(|r| matches!(r.event, Event::DialStarted { .. })),
"should have emitted at least one DialStarted",
);
let outcomes: Vec<_> = records
.iter()
.filter_map(|r| match &r.event {
Event::DialOutcome { outcome, .. } => Some(outcome.clone()),
_ => None,
})
.collect();
assert!(
!outcomes.is_empty(),
"should have emitted at least one DialOutcome",
);
assert!(
outcomes
.iter()
.all(|o| !matches!(o, DiagDialOutcome::Success)),
"no dial should succeed to an unreachable peer, got: {outcomes:?}",
);
}