swactor/apps/pipeline-parallel-inference/tests/t_fleet.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

83 lines
3.5 KiB
Rust

//! Scenario: the orchestrator-hosted fleet path over the swactor cluster transport.
//!
//! A stage's datastream frames, shipped as `DatastreamFrame` messages to the
//! orchestrator's `DatastreamSink` actor, must surface as a row in the Fleet
//! table. This is the producer→consumer mechanism the live dashboard relies on:
//! the stage runs a [`FleetEmitter`] over a `ClusterFrameSink`, the orchestrator
//! runs the [`DatastreamSink`] actor folding into a `FleetView`, and the `vastai`
//! plugin serves the resulting JSON. There is no dedicated channel — telemetry
//! rides the same transport as everything else. Exercised in-process (one
//! runtime) so it tests the real fold without standing up iroh.
use std::sync::{Arc, Mutex, OnceLock};
use std::time::{Duration, Instant};
use swactor::actor::ActorAddress;
use swactor::runtime::{Runtime, RuntimeConfig};
use dashboard::datastream_source::FleetView;
use datastream::catalog::RuntimeStats;
use datastream::DatastreamSink;
use pipeline_parallel_inference::fleet::FleetEmitter;
#[test]
fn stage_frames_over_cluster_transport_appear_in_the_fleet_table() {
let rt = Arc::new(Runtime::new(RuntimeConfig::default()));
// Consumer: the orchestrator's DatastreamSink actor folding each delivery
// into a FleetView and caching the fleet JSON the dashboard serves.
let cache: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
let sink_addr = {
let mut view = FleetView::new(None);
let cache = Arc::clone(&cache);
rt.spawn(DatastreamSink::new(move |stream, frame| {
let update = view.ingest(&stream, &frame);
*cache.lock().unwrap() = Some(update.fleet_json);
}))
.expect("spawn datastream-sink actor")
};
// Producer: one stage's emitter shipping over the cluster transport to the
// resolved sink. The pre-filled slot stands in for SWIM name resolution.
let node_hex = "ab".repeat(32); // 64 hex chars = a 32-byte node id
let slot: Arc<OnceLock<ActorAddress>> = Arc::new(OnceLock::new());
slot.set(sink_addr).expect("set sink slot");
let mut emitter = FleetEmitter::new(
Arc::clone(&rt),
Arc::clone(&slot),
&node_hex,
1,
"pp-stage-0",
"127.0.0.1:5000",
);
// Tick the emitter (ships frames over the cluster transport) and the runtime
// (delivers them to the sink actor) until the node row shows up. The expected
// value — a row whose `id` is our node hex — is what we emit, never read back
// from the consumer first.
let deadline = Instant::now() + Duration::from_secs(5);
let mut found = None;
while Instant::now() < deadline {
emitter.tick(&[], RuntimeStats::default(), false, 0);
rt.tick();
let snapshot = cache.lock().unwrap().clone();
if let Some(json) = snapshot {
let v: serde_json::Value = serde_json::from_str(&json).expect("fleet json parses");
let has_node = v["nodes"]
.as_array()
.map(|ns| ns.iter().any(|n| n["id"] == serde_json::json!(node_hex)))
.unwrap_or(false);
if has_node {
found = Some(v);
break;
}
}
std::thread::sleep(Duration::from_millis(20));
}
let v = found.expect("the stage's emitted frames must surface as a Fleet-table row");
assert!(
v["node_count"].as_u64().unwrap_or(0) >= 1,
"fleet table must report at least one live node, got {v}",
);
}