swactor/crates/datastream/src/store.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

160 lines
5.4 KiB
Rust

//! The stored stream — the consumer's source of truth (spec §8).
//!
//! Storage holds each node's **complete** stream, whole and append-only.
//! Nothing is thinned, aggregated, decoded-and-discarded, or truncated at
//! ingest (spec §8.2); everything a view ever shows is derived from here
//! (spec §9.1). Frames on channels the consumer cannot decode are kept as
//! opaque bytes, in order, alongside the rest (spec §8.3) — the store never
//! looks at a channel or a payload.
//!
//! A [`StoredStream`] is keyed in the [`Store`] by [`StreamId`] — node plus
//! lifetime — so a re-incarnated node does not append to its prior life
//! (spec §8.4).
use std::collections::BTreeMap;
use super::frame::{Frame, Position, StreamId};
/// One node's reconstructed stream: its frames in position order, whole.
///
/// Backed by a position-keyed map so out-of-order arrivals land in order
/// and a position seen twice collapses to one (the carrier may not
/// fabricate content, spec §9). Gaps are not stored — they are *derived*
/// at read time from the positions that are present (spec §9.1).
#[derive(Debug, Clone, Default)]
pub struct StoredStream {
frames: BTreeMap<u64, Frame>,
}
impl StoredStream {
/// An empty stream.
pub fn new() -> Self {
StoredStream::default()
}
/// Record a delivered frame. Idempotent by position: the first frame
/// seen for a position wins and is never mutated (append-only,
/// spec §8.2). Returns `true` if this was the first time the position
/// was seen.
pub fn record(&mut self, frame: Frame) -> bool {
match self.frames.entry(frame.position.0) {
std::collections::btree_map::Entry::Vacant(slot) => {
slot.insert(frame);
true
}
std::collections::btree_map::Entry::Occupied(_) => false,
}
}
/// The stored frames, in position order.
pub fn frames(&self) -> impl Iterator<Item = &Frame> {
self.frames.values()
}
/// The stored frames cloned into a vector, in position order. Handy for
/// asserting against the reference model.
pub fn to_vec(&self) -> Vec<Frame> {
self.frames.values().cloned().collect()
}
/// How many frames are stored.
pub fn len(&self) -> usize {
self.frames.len()
}
/// Whether the stream has no frames yet.
pub fn is_empty(&self) -> bool {
self.frames.is_empty()
}
/// The frame at an exact position, if stored.
pub fn at(&self, position: Position) -> Option<&Frame> {
self.frames.get(&position.0)
}
/// The **interior** gaps — runs of positions assigned between the first
/// and last delivered frame but never delivered (spec §7.5, §8), each as
/// one [`GapSpan`].
///
/// Cost is O(stored frames), never O(gap size): it walks adjacent stored
/// positions and reads each span's endpoints from them, rather than
/// enumerating the (possibly enormous) range in between. A stream that
/// brackets a huge interior gap — what a long consumer outage produces
/// (spec §7.4), or a single wild position from a corrupt datagram — still
/// surfaces in work proportional to the frames held, not to `u64::MAX`.
///
/// Only interior gaps are knowable: a position lost *after* the last
/// delivered frame leaves no bracketing frame to reveal it, so it shows
/// up as the stream simply ending (spec §7.4 node death), not a gap.
pub fn gap_spans(&self) -> Vec<GapSpan> {
let mut spans = Vec::new();
let mut prev: Option<u64> = None;
for &p in self.frames.keys() {
if let Some(q) = prev
&& p > q + 1
{
spans.push(GapSpan { start: q + 1, end: p - 1 });
}
prev = Some(p);
}
spans
}
}
/// A contiguous run of missing positions surfaced in a stored stream
/// (spec §7.5). Inclusive on both ends.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct GapSpan {
/// First missing position.
pub start: u64,
/// Last missing position.
pub end: u64,
}
impl GapSpan {
/// How many positions the gap spans (always at least one).
pub fn count(&self) -> u64 {
self.end - self.start + 1
}
}
/// All stored streams at the consumer, keyed by [`StreamId`] (spec §8.4).
///
/// Two streams with the same node but different lifetime are distinct keys
/// and never merge.
#[derive(Debug, Clone, Default)]
pub struct Store {
streams: BTreeMap<StreamId, StoredStream>,
}
impl Store {
/// An empty store.
pub fn new() -> Self {
Store::default()
}
/// The stored stream for a node's life, if any frames have landed.
pub fn stream(&self, id: &StreamId) -> Option<&StoredStream> {
self.streams.get(id)
}
/// The stored stream for a node's life, creating an empty one if needed.
pub fn stream_mut(&mut self, id: &StreamId) -> &mut StoredStream {
self.streams.entry(id.clone()).or_default()
}
/// Every stream id the store holds, in a stable order.
pub fn stream_ids(&self) -> impl Iterator<Item = &StreamId> {
self.streams.keys()
}
/// How many distinct streams the store holds.
pub fn len(&self) -> usize {
self.streams.len()
}
/// Whether the store holds no streams.
pub fn is_empty(&self) -> bool {
self.streams.is_empty()
}
}