swactor/apps/pipeline-parallel-inference/tests/common/mod.rs
Zachery Aaron Shores-Chmielewski ae9ca3bcf3 feat: datastream feature cleaning
Promote pipeline-parallel-inference to a first-class app and consolidate observability on the datastream wire, decoupling the dashboard crate from `distribution`.

- apps/pipeline-parallel-inference: move the example out of `examples/` into `apps/` as its own workspace, rename binaries to `pp-worker`/`pp-orchestrator`, and strip release binaries
- cluster: add `ClusterNode`, a synchronous facade over the actorized distribution protocol (IrohDriver + per-node Runtime hosting Swim/Registry/Metadata/Directory actors with a `MembershipFanout`), replacing ad-hoc `driver.node()`/`tick()` call sites
- fleet: add per-node fleet telemetry that ships identity/resource records as `DatastreamFrame`s over the cluster transport to the orchestrator's `DatastreamSink`, folded into a `FleetView` on a 3s tick
- provision: add best-effort, opt-in SSH boot-phase telemetry (`PP_DEPLOY_KEY`) that streams rented-node boot logs onto the orchestrator's datastream as `proc.boot.<stage>.*`
- dashboard: rewire the crate dependency from `distribution` to `datastream`, drop the standalone `swactor-datastream-dashboard` binary, and rewrite `datastream_source.rs` to demux per-node frames into Overview/Distribution/Fleet views with live-node TTL filtering
- distribution: refresh dist/netmap plugin copy and README from "Kademlia routing" to gossip-directory terminology

Signed-off-by: Zachery Aaron Shores-Chmielewski <zacheryasc@gmail.com>
2026-06-09 13:29:07 +04:00

148 lines
4.9 KiB
Rust

//! Shared test fixture for the pipeline-parallel-inference test suite.
//!
//! Single source of truth for "make a node on the actorized distribution
//! stack" used by `t_topology`, `t_cluster`, and `t_integration`. Each helper
//! returns a [`ClusterNode`] (driver + per-node swactor runtime + the four
//! protocol actors), pumped synchronously by tests.
//!
//! Mirrors `crates/distribution/tests/common/iroh.rs` — same idea, but the
//! codec registry is the pp `inference_codec_registry()` (which already layers
//! the protocol codec) so app actors share the runtime with protocol actors.
#![allow(dead_code)]
use std::sync::OnceLock;
use std::time::{Duration, Instant};
use iroh::{PublicKey, RelayMode};
use tokio::runtime::{Handle, Runtime as TokioRuntime};
use distribution::iroh_driver::IrohDriverConfig;
use distribution::node::DistributedNodeConfig;
use distribution::registry::RegistryConfig;
use distribution::swim::probe::SwimConfig;
use pipeline_parallel_inference::cluster::ClusterNode;
use pipeline_parallel_inference::messages::inference_codec_registry;
/// Wall-clock granularity of one gossip round for tests. Smaller than
/// production so the test suite finishes quickly; the actors are wall-clock
/// driven so the values flow through unchanged.
const TICK: Duration = Duration::from_millis(10);
pub fn test_node_config() -> DistributedNodeConfig {
DistributedNodeConfig {
swim: SwimConfig {
probe_interval: TICK,
probe_timeout: TICK * 3,
indirect_probes: 1,
suspicion_timeout: TICK * 5,
dead_reprobe_interval: Duration::ZERO,
..SwimConfig::default()
},
cache_capacity: 100,
registry: RegistryConfig::default(),
metadata_lambda: 3,
}
}
/// Shared tokio runtime for sync `#[test]`s — iroh needs a tokio context, and
/// each test creating its own multi-thread runtime would balloon thread count.
fn test_tokio_handle() -> Handle {
static RT: OnceLock<TokioRuntime> = OnceLock::new();
RT.get_or_init(|| TokioRuntime::new().expect("build test tokio runtime"))
.handle()
.clone()
}
pub fn make_node() -> ClusterNode {
ClusterNode::with_handle(
test_tokio_handle(),
IrohDriverConfig {
secret_key: None,
relay_mode: RelayMode::Disabled,
node: test_node_config(),
peer_auth: None,
additional_alpns: vec![],
},
test_node_config(),
inference_codec_registry(),
|_| {},
)
.expect("failed to create cluster node")
}
/// One pump iteration of a single node (driver + runtime).
pub fn pump_one(node: &mut ClusterNode) {
node.pump_once();
}
/// One pump iteration across every node, in order.
pub fn pump_all(nodes: &mut [ClusterNode]) {
for n in nodes.iter_mut() {
n.pump_once();
}
}
/// Pump every node until `cond` holds or `timeout` expires. Returns the
/// outcome so callers can assert on it. Sleeps ~10ms per iteration so SWIM's
/// wall-clock timers advance.
pub fn pump_until<F>(nodes: &mut [ClusterNode], timeout: Duration, cond: F) -> bool
where
F: Fn(&[ClusterNode]) -> bool,
{
let start = Instant::now();
while start.elapsed() < timeout {
pump_all(nodes);
if cond(nodes) {
return true;
}
std::thread::sleep(Duration::from_millis(10));
}
false
}
/// Whether `node` sees `peer_key` as `Alive` in its membership mirror.
pub fn sees_alive(node: &ClusterNode, peer_key: &PublicKey) -> bool {
let peer = distribution::types::NodeId(*peer_key.as_bytes());
node.sees_alive(&peer)
}
pub fn pubkey_of(node: &ClusterNode) -> PublicKey {
PublicKey::from_bytes(&node.node_id().0).expect("valid node id")
}
/// Build a three-node cluster (orchestrator + stage 0 + stage 1) joined via
/// the orchestrator as the seed. Returns the nodes in `[orchestrator,
/// stage0, stage1]` order once every node sees every other node alive.
pub fn make_three_node_cluster() -> [ClusterNode; 3] {
let mut orch = make_node();
let mut s0 = make_node();
let mut s1 = make_node();
let seed = orch.endpoint_addr();
s0.join(&[seed.clone()]);
s1.join(&[seed]);
let orch_key = pubkey_of(&orch);
let s0_key = pubkey_of(&s0);
let s1_key = pubkey_of(&s1);
let start = Instant::now();
let mut converged = false;
while start.elapsed() < Duration::from_secs(10) {
orch.pump_once();
s0.pump_once();
s1.pump_once();
let orch_sees_all = sees_alive(&orch, &s0_key) && sees_alive(&orch, &s1_key);
let s0_sees_all = sees_alive(&s0, &orch_key) && sees_alive(&s0, &s1_key);
let s1_sees_all = sees_alive(&s1, &orch_key) && sees_alive(&s1, &s0_key);
if orch_sees_all && s0_sees_all && s1_sees_all {
converged = true;
break;
}
std::thread::sleep(Duration::from_millis(20));
}
assert!(converged, "3-node cluster did not converge within 10s");
[orch, s0, s1]
}