enforce datastream telemetry-only invariant: ban frame types from control code

The datastream is metrics/logging only; control decisions must never branch
on a frame.  This was a recurring cultural problem with no structural
enforcement.  This change makes it a compile-time and CI-enforced fact.

datastream crate (lib.rs):
- Stop re-exporting Frame, DatastreamEvent, FrameDelivery at crate root.
   is now a compile error (E0425).  These types live
  only in datastream::frame::* and are documented as the observer surface.
- Safe identity types (ChannelId, StreamId, Position, Record, etc.) remain
  re-exported at root for producer-side callers.

orchestration/app.rs:
- Extracted all frame-touching code (CollectedDatastreamFrame,
  drain_datastream_connections, update_load_progress_from_frame,
  drain_frames, archive_collected_frame, pump, OrchDatastream,
  DashboardSupport) into two new observability modules:
  frame_collector.rs and orch_datastream.rs.
- The orchestrator now interacts through a FrameCollector whose
  drain/drain_with_progress methods take closures; it never names Frame,
  DatastreamEvent, or CollectedDatastreamFrame.
- StageLoadProgress (the one control-relevant signal previously scraped
  from frame payloads) is extracted inside FrameCollector and handed to
  the control loop as plain data.

xtask:
- New check-telemetry-isolation command scans control-plane modules
  (orchestration/, distribution/, data-plane/, provisioning/) for
  forbidden frame-type references and fails the build if any are found.

Verified: workspace builds (myelin + dashboard feature), datastream 29
tests pass, myelin 64 lib tests pass, check-telemetry-isolation passes
clean.

Signed-off-by: Zachery Aaron Shores-Chmielewski <zacheryasc@gmail.com>
This commit is contained in:
Zachery Aaron Shores-Chmielewski 2026-08-12 16:14:57 +04:00
parent 1853d3dac5
commit b8aff00dc1
19 changed files with 858 additions and 666 deletions

View file

@ -9,8 +9,6 @@ members = [
"crates/transport", "crates/transport",
"crates/distribution", "crates/distribution",
"crates/iroh-driver", "crates/iroh-driver",
"crates/datastream",
"crates/data-plane",
"crates/dashboard", "crates/dashboard",
"apps/myelin", "apps/myelin",
"xtask", "xtask",
@ -23,9 +21,6 @@ default-members = [
"crates/provisioning", "crates/provisioning",
"crates/transport", "crates/transport",
"crates/distribution", "crates/distribution",
"crates/iroh-driver",
"crates/datastream",
"crates/data-plane",
"crates/dashboard", "crates/dashboard",
"apps/myelin", "apps/myelin",
"tools/vastai", "tools/vastai",

View file

@ -12,9 +12,10 @@ use std::thread;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use datastream::{ use datastream::{
ChannelContent, ChannelId, DatastreamEndpoint, DatastreamProducer, Frame, Lifetime, NodeId, ChannelContent, ChannelId, DatastreamEndpoint, DatastreamProducer, Lifetime, NodeId,
StreamDescriptor, StreamId, StreamOrigin, StreamDescriptor, StreamId, StreamOrigin,
}; };
use datastream::frame::Frame;
use serde::Deserialize; use serde::Deserialize;
use serde_json::{Value, json}; use serde_json::{Value, json};
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]

View file

@ -17,10 +17,11 @@ use std::thread;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use datastream::{ use datastream::{
ChannelContent, ChannelId, DATASTREAM_PUBLISHER_NAME, DatastreamEndpoint, DatastreamEvent, ChannelContent, ChannelId, DATASTREAM_PUBLISHER_NAME, DatastreamEndpoint,
DatastreamProducer, DatastreamPublisherActor, DatastreamSubscribe, DatastreamSubscription, DatastreamProducer, DatastreamPublisherActor, DatastreamSubscribe, DatastreamSubscription,
Lifetime, NodeId, Record, StreamDescriptor, StreamId, StreamOrigin, Lifetime, NodeId, Record, StreamDescriptor, StreamId, StreamOrigin,
}; };
use datastream::frame::DatastreamEvent;
use crate::codecs::register_myelin_actor_codecs; use crate::codecs::register_myelin_actor_codecs;
use crate::gguf_shard::{StageShardPlan, materialize_stage_shard_http, validate_stage_shard_cache}; use crate::gguf_shard::{StageShardPlan, materialize_stage_shard_http, validate_stage_shard_cache};

View file

@ -2,7 +2,7 @@ use std::fs::{self, File, OpenOptions};
use std::io::{BufRead, BufReader, Write}; use std::io::{BufRead, BufReader, Write};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use datastream::{Frame, StreamId}; use datastream::frame::{Frame, StreamId};
use serde_json::json; use serde_json::json;
use crate::observability::benchmark; use crate::observability::benchmark;

View file

@ -0,0 +1,310 @@
//! Frame collection and load-progress extraction for the control loop.
//!
//! [`FrameCollector`] wraps the mpsc channel that buffers datastream frames
//! drained from the iroh driver. It exposes two drain methods that forward
//! queued frames to sinks (dashboard/archive) via closures; the control loop
//! never names [`Frame`] or [`DatastreamEvent`] directly — frames reach sinks
//! only through these closures. Load-progress extraction
//! ([`StageLoadProgress`]) is co-located here because it is the one legitimate
//! read of telemetry content for control decisions (weight-load liveness).
use datastream::frame::{ChannelRef, DatastreamEvent, Frame, StreamId};
use iroh_driver::IrohDriver;
use serde_json::Value;
use std::collections::BTreeMap;
use std::sync::mpsc;
use std::time::Instant;
use crate::observability::orch_datastream::DashboardSupport;
/// A datastream frame queued from a remote node, with its resolved channel name.
#[derive(Clone, Debug)]
struct CollectedDatastreamFrame {
stream: StreamId,
channel_name: String,
frame: Frame,
}
/// Per-node load progress distilled from telemetry frames (control input).
#[derive(Clone, Debug, Default)]
pub(crate) struct StageLoadProgress {
pub(crate) node_id: u64,
pub(crate) stage_index: Option<u32>,
pub(crate) phase: Option<String>,
pub(crate) bytes_done: Option<u64>,
pub(crate) bytes_total: Option<u64>,
pub(crate) last_progress: Option<Instant>,
pub(crate) last_worker_event: Option<String>,
pub(crate) failure_reason: Option<String>,
pub(crate) host_gpu_samples: u64,
}
impl StageLoadProgress {
pub(crate) fn to_json(&self) -> Value {
serde_json::json!({
"node_id": self.node_id,
"stage_index": self.stage_index,
"phase": self.phase.as_deref().unwrap_or("unknown"),
"bytes_done": self.bytes_done,
"bytes_total": self.bytes_total,
"last_progress_age_ms": self.last_progress.map(|at| at.elapsed().as_millis()),
"last_worker_event": self.last_worker_event,
"failure_reason": self.failure_reason,
"host_gpu_samples": self.host_gpu_samples,
"host_gpu_missing": self.host_gpu_samples == 0,
})
}
}
/// Buffers datastream frames drained from the driver and forwards them to sinks.
pub(crate) struct FrameCollector {
tx: mpsc::Sender<CollectedDatastreamFrame>,
rx: mpsc::Receiver<CollectedDatastreamFrame>,
}
impl FrameCollector {
pub(crate) fn new() -> Self {
let (tx, rx) = mpsc::channel();
Self { tx, rx }
}
/// Drain iroh datastream connections into the internal queue.
pub(crate) fn pump(&self, driver: &IrohDriver) {
drain_datastream_connections(driver, &self.tx);
}
/// Drain queued frames, forwarding each via the closure. No progress extraction.
pub(crate) fn drain<F>(&self, forward: F)
where
F: FnMut(&StreamId, &str, &Frame),
{
let mut forward = forward;
while let Ok(collected) = self.rx.try_recv() {
forward(&collected.stream, &collected.channel_name, &collected.frame);
}
}
/// Drain queued frames, extracting load progress and forwarding each via the closure.
pub(crate) fn drain_with_progress<F>(
&self,
progress: &mut BTreeMap<u64, StageLoadProgress>,
forward: F,
) where
F: FnMut(&StreamId, &str, &Frame),
{
let mut forward = forward;
let now = Instant::now();
while let Ok(collected) = self.rx.try_recv() {
update_load_progress_from_frame(progress, &collected, now);
forward(&collected.stream, &collected.channel_name, &collected.frame);
}
}
}
fn update_load_progress_from_frame(
progress: &mut BTreeMap<u64, StageLoadProgress>,
collected: &CollectedDatastreamFrame,
now: Instant,
) {
let stream_node_id = collected.stream.node.as_str().parse::<u64>().ok();
if collected.channel_name == "host.gpu" {
if let Some(node_id) = stream_node_id {
let entry = progress
.entry(node_id)
.or_insert_with(|| StageLoadProgress {
node_id,
..StageLoadProgress::default()
});
entry.host_gpu_samples = entry.host_gpu_samples.saturating_add(1);
}
return;
}
let Ok(value) = serde_json::from_slice::<Value>(&collected.frame.payload) else {
return;
};
if value.get("type").and_then(Value::as_str) == Some("NodeEvent") {
update_load_progress_from_node_event(progress, &value, now);
return;
}
if collected.channel_name == "myelin.worker.weights" {
let Some(node_id) = stream_node_id else {
return;
};
update_load_progress_from_worker_event(progress, node_id, None, &value, now);
}
}
fn update_load_progress_from_node_event(
progress: &mut BTreeMap<u64, StageLoadProgress>,
value: &Value,
now: Instant,
) {
let Some(node_id) = numeric_json_field(value, "node_id") else {
return;
};
let stage_index =
numeric_json_field(value, "stage_index").and_then(|stage| u32::try_from(stage).ok());
let phase = value.get("phase").and_then(Value::as_str);
let status = value.get("status").and_then(Value::as_str);
let detail = value.get("detail").unwrap_or(&Value::Null);
if phase == Some("load_weights") {
let load_phase = match status {
Some("started") => Some("loading_weights"),
Some("ready") => Some("weights_loaded"),
Some("failed") => Some("failed"),
_ => None,
};
if let Some(load_phase) = load_phase {
let entry = progress
.entry(node_id)
.or_insert_with(|| StageLoadProgress {
node_id,
..StageLoadProgress::default()
});
entry.stage_index = stage_index.or(entry.stage_index);
entry.phase = Some(load_phase.to_owned());
entry.last_progress = Some(now);
if status == Some("failed") {
entry.failure_reason = detail
.get("error")
.and_then(Value::as_str)
.map(str::to_owned);
}
}
}
if let Some(worker_event) = detail.get("event") {
update_load_progress_from_worker_event(progress, node_id, stage_index, worker_event, now);
}
}
fn update_load_progress_from_worker_event(
progress: &mut BTreeMap<u64, StageLoadProgress>,
node_id: u64,
stage_index: Option<u32>,
event: &Value,
now: Instant,
) {
let Some(event_type) = event.get("type").and_then(Value::as_str) else {
return;
};
let Some(phase) = load_phase_for_worker_event(event_type) else {
return;
};
let entry = progress
.entry(node_id)
.or_insert_with(|| StageLoadProgress {
node_id,
..StageLoadProgress::default()
});
entry.stage_index = stage_index.or(entry.stage_index);
entry.phase = Some(phase.to_owned());
entry.last_worker_event = Some(event_type.to_owned());
entry.last_progress = Some(now);
if let Some(bytes_done) =
numeric_json_field(event, "bytes_done").or_else(|| numeric_json_field(event, "bytes"))
{
entry.bytes_done = Some(bytes_done);
}
if let Some(bytes_total) = numeric_json_field(event, "bytes_total") {
entry.bytes_total = Some(bytes_total);
}
}
fn numeric_json_field(value: &Value, field: &str) -> Option<u64> {
value
.get(field)
.and_then(|value| value.as_u64().or_else(|| value.as_str()?.parse().ok()))
}
fn load_phase_for_worker_event(event_type: &str) -> Option<&'static str> {
match event_type {
"GgufDownloadStarted" | "GgufDownloadProgress" => Some("prefetching_model"),
"GgufCacheReady" => Some("cache_ready"),
"StageShardFetchStarted"
| "StageShardRangeFetchStarted"
| "StageShardRangeFetchReady"
| "StageShardTensorFetchStarted"
| "StageShardTensorFetchReady" => Some("fetching_stage_shard"),
"StageShardCacheReady" => Some("stage_shard_cache_ready"),
"StageShardReady" => Some("stage_shard_ready"),
"StageShardFetchFailed" => Some("failed"),
"PipelineStageFromGgufStarted" => Some("constructing_stage"),
"PipelineStageFromGgufReady" => Some("stage_constructed"),
"TokenizerBuildStarted" => Some("building_tokenizer"),
"TokenizerBuildReady" => Some("tokenizer_ready"),
"WeightsLoaded" => Some("weights_loaded"),
"WorkerFatal" => Some("failed"),
_ => None,
}
}
fn drain_datastream_connections(
driver: &IrohDriver,
frame_tx: &mpsc::Sender<CollectedDatastreamFrame>,
) {
for read in driver.drain_datastream_reads() {
let mut channels = read
.header
.channels
.iter()
.map(|descriptor| {
(
ChannelRef {
stream: descriptor.stream.clone(),
channel: descriptor.id,
},
descriptor.name.clone(),
)
})
.collect::<BTreeMap<_, _>>();
for event in read.events {
match event {
DatastreamEvent::ChannelDeclared(descriptor) => {
channels.insert(
ChannelRef {
stream: descriptor.stream.clone(),
channel: descriptor.id,
},
descriptor.name,
);
}
DatastreamEvent::Frame(delivery) => {
let channel_name = channels
.get(&delivery.channel)
.cloned()
.unwrap_or_else(|| format!("channel#{}", delivery.channel.channel.0));
let frame = Frame::new(
delivery.channel.channel,
delivery.position,
delivery.payload,
);
if frame_tx
.send(CollectedDatastreamFrame {
stream: delivery.channel.stream,
channel_name,
frame,
})
.is_err()
{
return;
}
}
DatastreamEvent::StreamDeclared(_) | DatastreamEvent::StreamEnded(_) => {}
}
}
}
}
/// Publish a frame to the dashboard, if one is attached. Used by the producer
/// flush path as well as the control-loop drain closures.
pub(crate) fn ingest_dashboard_frame(
dashboard: Option<&DashboardSupport>,
stream: &StreamId,
channel: &str,
frame: &Frame,
) {
if let Some(dashboard) = dashboard {
dashboard.publish_frame(stream, channel, frame);
}
}

View file

@ -3,7 +3,9 @@
pub(crate) mod benchmark; pub(crate) mod benchmark;
#[cfg(feature = "dashboard")] #[cfg(feature = "dashboard")]
pub(crate) mod dashboard_view; pub(crate) mod dashboard_view;
pub(crate) mod frame_collector;
pub(crate) mod frame_archive; pub(crate) mod frame_archive;
pub(crate) mod lifecycle; pub(crate) mod lifecycle;
pub(crate) mod orch_datastream;
pub(crate) mod provisioning_logs; pub(crate) mod provisioning_logs;
pub(crate) mod telemetry; pub(crate) mod telemetry;

View file

@ -0,0 +1,338 @@
//! Orchestrator-owned datastream producer plus dashboard sink support.
//!
//! [`OrchDatastream`] owns the producer-side endpoint that emits bootstrap,
//! prompt, provisioning, and SWIM telemetry. [`DashboardSupport`] adapts the
//! optional live dashboard. Both are consumed by the control loop in
//! `orchestration::app`; frame-bearing read paths live in `frame_collector`.
use datastream::frame::{Frame, StreamId};
use datastream::{
ChannelContent, ChannelId, DatastreamEndpoint, DatastreamProducer, Lifetime, NodeId, Record,
StreamDescriptor, StreamOrigin,
};
use serde_json::{Value, json};
use std::collections::BTreeMap;
use std::path::Path;
use std::sync::Arc;
use crate::observability::benchmark;
use crate::observability::frame_archive::FrameArchive;
use crate::observability::frame_collector::ingest_dashboard_frame;
#[cfg(feature = "dashboard")]
use crate::observability::dashboard_view::MyelinClusterDashboardView;
use crate::observability::telemetry::{
MYELIN_PROVISIONING_EVENTS, MyelinProvisionEventRecord, MyelinProvisionLogRecord,
myelin_provision_log_channel,
};
#[cfg(feature = "dashboard")]
use crate::orchestration::app::env_optional;
use crate::provisioning::{ProvisionEvent, ProvisionLogLine};
use distribution::telemetry::{MembershipTransition, SwimProbeEvent};
use swactor_engine::EngineHandle;
use swactor::stats::StatsHook;
pub(crate) const MYELIN_ORCH_BOOTSTRAP: &str = "myelin.orch.bootstrap";
pub(crate) const MYELIN_ORCH_PROMPT: &str = "myelin.orch.prompt";
pub(crate) const MYELIN_SWIM_MEMBERSHIP: &str = "myelin.swim.membership";
pub(crate) const MYELIN_STAGE_ROUTE: &str = "myelin.orch.stage_route";
pub(crate) struct OrchDatastream {
stream: StreamId,
endpoint: DatastreamEndpoint,
producer: DatastreamProducer,
channels: BTreeMap<String, ChannelId>,
channel_names: BTreeMap<ChannelId, String>,
archive: Option<FrameArchive>,
}
impl OrchDatastream {
pub(crate) fn new(run_id: u64, frame_log: Option<&Path>) -> Result<Self, String> {
let stream = StreamId::new(NodeId::new("myelin-orchestrator"), Lifetime(run_id));
let endpoint = DatastreamEndpoint::with_descriptor(
StreamDescriptor {
stream: stream.clone(),
label: Some("myelin orchestrator".to_owned()),
origin: StreamOrigin::Orchestrator,
},
4096,
1024,
);
let producer = endpoint.producer();
let mut out = Self {
stream,
endpoint,
producer,
channels: BTreeMap::new(),
channel_names: BTreeMap::new(),
archive: frame_log
.map(|p| FrameArchive::open_with_label(p, "datastream frame log"))
.transpose()?,
};
for name in [
MYELIN_PROVISIONING_EVENTS,
MYELIN_ORCH_BOOTSTRAP,
MYELIN_ORCH_PROMPT,
MYELIN_SWIM_MEMBERSHIP,
MYELIN_STAGE_ROUTE,
] {
out.channel_by_name(name);
}
out.record_channel::<MembershipTransition>();
out.record_channel::<SwimProbeEvent>();
Ok(out)
}
pub(crate) fn channel_by_name(&mut self, name: &str) -> ChannelId {
if let Some(id) = self.channels.get(name).copied() {
return id;
}
let id = self.producer.register_channel(
name,
ChannelContent::JsonRecord {
schema: Some(name.to_owned()),
},
);
self.channels.insert(name.to_owned(), id);
self.channel_names.insert(id, name.to_owned());
id
}
pub(crate) fn record_channel<R: Record>(&mut self) -> ChannelId {
if let Some(id) = self.channels.get(R::CHANNEL).copied() {
return id;
}
let id = self.producer.register_record::<R>();
self.channels.insert(R::CHANNEL.to_owned(), id);
self.channel_names.insert(id, R::CHANNEL.to_owned());
id
}
pub(crate) fn emit_event(
&mut self,
dashboard: Option<&DashboardSupport>,
event: ProvisionEvent,
) {
let payload = serde_json::to_vec(&MyelinProvisionEventRecord::new(event))
.expect("serialize provisioning event");
self.emit_bytes(dashboard, MYELIN_PROVISIONING_EVENTS, payload);
}
pub(crate) fn emit_log(
&mut self,
dashboard: Option<&DashboardSupport>,
line: ProvisionLogLine,
) {
let channel = myelin_provision_log_channel(line.node_id, line.stream);
let payload = serde_json::to_vec(&MyelinProvisionLogRecord::new(line))
.expect("serialize provision log");
self.emit_bytes(dashboard, &channel, payload);
}
pub(crate) fn emit_bootstrap(
&mut self,
dashboard: Option<&DashboardSupport>,
run_id: u64,
node_id: u64,
phase: &str,
status: &str,
detail: Value,
) {
self.emit_bootstrap_to_channel(
dashboard,
MYELIN_ORCH_BOOTSTRAP,
run_id,
node_id,
phase,
status,
detail,
);
}
pub(crate) fn emit_bootstrap_to_channel(
&mut self,
dashboard: Option<&DashboardSupport>,
channel: &str,
run_id: u64,
node_id: u64,
phase: &str,
status: &str,
detail: Value,
) {
let benchmark = benchmark::stamp("myelin-orchestrator");
let payload = serde_json::to_vec(&json!({
"schema_version": benchmark["schema_version"].clone(),
"type":"OrchBootstrap",
"event_type":"OrchBootstrap",
"event_name":phase,
"phase":phase,
"status":status,
"run_id":run_id,
"node_id":node_id,
"producer_component":benchmark["producer_component"].clone(),
"producer_instance_id":benchmark["producer_instance_id"].clone(),
"producer_process_id":benchmark["producer_process_id"].clone(),
"producer_sequence":benchmark["producer_sequence"].clone(),
"wall_clock_unix_ms":benchmark["wall_clock_unix_ms"].clone(),
"monotonic_ms":benchmark["monotonic_ms"].clone(),
"clock_source":benchmark["clock_source"].clone(),
"span_id":format!("myelin-orchestrator:{run_id}:{}:{phase}", benchmark["producer_sequence"]),
"parent_span_id":Value::Null,
"benchmark":benchmark,
"detail":detail,
}))
.expect("serialize orch bootstrap event");
self.emit_bytes(dashboard, channel, payload);
}
pub(crate) fn emit_prompt(
&mut self,
dashboard: Option<&DashboardSupport>,
run_id: u64,
node_id: u64,
request_id: u64,
phase: &str,
status: &str,
detail: Value,
) {
let benchmark = benchmark::stamp("myelin-orchestrator");
let payload = serde_json::to_vec(&json!({
"schema_version": benchmark["schema_version"].clone(),
"type":"OrchPromptEvent",
"event_type":"OrchPromptEvent",
"event_name":phase,
"phase":phase,
"status":status,
"run_id":run_id,
"node_id":node_id,
"request_id":request_id,
"producer_component":benchmark["producer_component"].clone(),
"producer_instance_id":benchmark["producer_instance_id"].clone(),
"producer_process_id":benchmark["producer_process_id"].clone(),
"producer_sequence":benchmark["producer_sequence"].clone(),
"wall_clock_unix_ms":benchmark["wall_clock_unix_ms"].clone(),
"monotonic_ms":benchmark["monotonic_ms"].clone(),
"clock_source":benchmark["clock_source"].clone(),
"span_id":format!("myelin-orchestrator:{run_id}:{request_id}:{}:{phase}", benchmark["producer_sequence"]),
"parent_span_id":format!("request:{request_id}"),
"benchmark":benchmark,
"detail":detail,
}))
.expect("serialize orch prompt event");
self.emit_bytes(dashboard, MYELIN_ORCH_PROMPT, payload);
}
pub(crate) fn emit_record<R: Record>(
&mut self,
dashboard: Option<&DashboardSupport>,
record: &R,
) {
let id = self.record_channel::<R>();
self.producer.submit_record(id, record);
self.flush(dashboard, "orchestrator");
}
pub(crate) fn emit_bytes(
&mut self,
dashboard: Option<&DashboardSupport>,
channel: &str,
payload: Vec<u8>,
) {
self.emit_bytes_from(dashboard, channel, payload, "orchestrator");
}
pub(crate) fn emit_bytes_from(
&mut self,
dashboard: Option<&DashboardSupport>,
channel: &str,
payload: Vec<u8>,
source: &str,
) {
let id = self.channel_by_name(channel);
self.producer.submit_bytes(id, payload);
self.flush(dashboard, source);
}
pub(crate) fn flush(&mut self, dashboard: Option<&DashboardSupport>, source: &str) {
let stream = self.stream.clone();
for frame in self.endpoint.mux().drain() {
let channel = self
.channel_names
.get(&frame.channel)
.cloned()
.unwrap_or_else(|| format!("channel#{}", frame.channel.0));
ingest_dashboard_frame(dashboard, &stream, &channel, &frame);
self.archive_frame(source, &stream, &channel, &frame);
}
}
pub(crate) fn archive_frame(
&mut self,
source: &str,
stream: &StreamId,
channel: &str,
frame: &Frame,
) {
if let Some(archive) = &mut self.archive {
let _ = archive.record(source, stream, channel, frame);
}
}
/// Attach the datastream stats hook on `channel` so actor snapshots flow to it.
pub(crate) fn stats_hook_on(&self, channel: ChannelId) -> Arc<dyn StatsHook> {
self.producer.stats_hook_on(channel)
}
}
#[cfg(feature = "dashboard")]
pub(crate) struct DashboardSupport {
handle: dashboard::DashboardHandle,
}
#[cfg(feature = "dashboard")]
impl DashboardSupport {
pub(crate) fn start(enabled: bool, engine: &EngineHandle) -> Result<Option<Self>, String> {
if !enabled {
return Ok(None);
}
let mut config = dashboard::DashboardConfig::default();
if let Some(port) = env_optional("MYELIN_DASHBOARD_PORT") {
config.port = port
.parse::<u16>()
.map_err(|e| format!("invalid MYELIN_DASHBOARD_PORT={port:?}: {e}"))?;
}
let handle = dashboard::DashboardHandle::new(config);
handle.register_view(Arc::new(MyelinClusterDashboardView::new()));
engine.spawn(handle.http_server());
Ok(Some(Self { handle }))
}
pub(crate) fn publish_frame(&self, stream: &StreamId, channel: &str, frame: &Frame) {
self.handle.publish(dashboard::FrameEvent {
stream: dashboard::StreamEvent {
node: stream.node.as_str().to_string(),
life: stream.life.0,
},
channel: channel.to_owned(),
position: frame.position.0,
payload: frame.payload.clone(),
});
}
}
#[cfg(not(feature = "dashboard"))]
pub(crate) struct DashboardSupport;
#[cfg(not(feature = "dashboard"))]
impl DashboardSupport {
pub(crate) fn start(enabled: bool, _engine: &EngineHandle) -> Result<Option<Self>, String> {
if enabled {
return Err(
"MYELIN_DASHBOARD requires building myelin-system with feature dashboard"
.to_owned(),
);
}
Ok(None)
}
pub(crate) fn publish_frame(&self, _stream: &StreamId, _channel: &str, _frame: &Frame) {}
}

View file

@ -16,18 +16,15 @@ use crate::node_actor::{
NodeAgentMsg, StageEdgeKindWire, StageInboundEdgeWire, StageObjectSpecWire, NodeAgentMsg, StageEdgeKindWire, StageInboundEdgeWire, StageObjectSpecWire,
StageOutboundEdgeWire, StageProvisionWire, StageRingSpecWire, StageOutboundEdgeWire, StageProvisionWire, StageRingSpecWire,
}; };
#[cfg(feature = "dashboard")] use crate::observability::frame_collector::{FrameCollector, StageLoadProgress};
use crate::observability::dashboard_view::MyelinClusterDashboardView; use crate::observability::orch_datastream::{
use crate::observability::{benchmark, frame_archive::FrameArchive}; DashboardSupport, OrchDatastream, MYELIN_STAGE_ROUTE, MYELIN_SWIM_MEMBERSHIP,
};
use crate::orchestration::actor::{OrchestratorActor, OrchestratorMsg, OrchestratorReport}; use crate::orchestration::actor::{OrchestratorActor, OrchestratorMsg, OrchestratorReport};
use crate::orchestration::config::{DEFAULT_CONFIG_PATH, TomlConfigOverlay}; use crate::orchestration::config::{DEFAULT_CONFIG_PATH, TomlConfigOverlay};
use crate::gguf_shard::{StageShardPlan, plan_stage_shard}; use crate::gguf_shard::{StageShardPlan, plan_stage_shard};
use crate::node_provisioning::{ProviderKind, provider_kind}; use crate::node_provisioning::{ProviderKind, provider_kind};
use crate::observability::telemetry::{
MYELIN_PROVISIONING_EVENTS, MyelinProvisionEventRecord, MyelinProvisionLogRecord,
myelin_provision_log_channel,
};
use crate::orchestration::cluster_reconciler::{ProvisionedClusterGuard, ReconcilerNodeBinding}; use crate::orchestration::cluster_reconciler::{ProvisionedClusterGuard, ReconcilerNodeBinding};
use crate::orchestration::distribution_stack::{DistributionRuntimeStack, duration_ms_u64}; use crate::orchestration::distribution_stack::{DistributionRuntimeStack, duration_ms_u64};
use crate::orchestration::provider_adapters::relay::{ use crate::orchestration::provider_adapters::relay::{
@ -54,14 +51,9 @@ use ::provisioning::{
RunId as ClusterRunId, RunNodeGroupSpec, SwactorId, SwarmJoinTemplate, RunId as ClusterRunId, RunNodeGroupSpec, SwactorId, SwarmJoinTemplate,
}; };
use data_plane::object_record as ingress; use data_plane::object_record as ingress;
use datastream::{ use datastream::{DatastreamPublisherMsg, DatastreamSubscribe, SubscriptionRequest};
ChannelContent, ChannelId, ChannelRef, DatastreamEndpoint, DatastreamEvent, DatastreamProducer,
DatastreamPublisherMsg, DatastreamSubscribe, Frame, Lifetime, NodeId, Record, StreamDescriptor,
StreamId, StreamOrigin, SubscriptionRequest,
};
use distribution::node::DistributedNodeConfig; use distribution::node::DistributedNodeConfig;
use distribution::swim::telemetry::ObservedTransition; use distribution::swim::telemetry::ObservedTransition;
use distribution::telemetry::{MembershipTransition, SwimProbeEvent};
use distribution::types::{MemberState, NodeId as DistNodeId}; use distribution::types::{MemberState, NodeId as DistNodeId};
use iroh::EndpointAddr; use iroh::EndpointAddr;
use iroh_driver::{ use iroh_driver::{
@ -87,10 +79,6 @@ const PUMP_INTERVAL: Duration = Duration::from_millis(10);
const RUNTIME_READY_ACK_RETRY_INTERVAL: Duration = Duration::from_millis(250); const RUNTIME_READY_ACK_RETRY_INTERVAL: Duration = Duration::from_millis(250);
const STAGE_PROVISION_ACTIVE_RESEND_AFTER: Duration = Duration::from_secs(60); const STAGE_PROVISION_ACTIVE_RESEND_AFTER: Duration = Duration::from_secs(60);
const PIPELINE_PROMPT_WAIT_LOG_INTERVAL: Duration = Duration::from_secs(15); const PIPELINE_PROMPT_WAIT_LOG_INTERVAL: Duration = Duration::from_secs(15);
const MYELIN_ORCH_BOOTSTRAP: &str = "myelin.orch.bootstrap";
const MYELIN_ORCH_PROMPT: &str = "myelin.orch.prompt";
const MYELIN_SWIM_MEMBERSHIP: &str = "myelin.swim.membership";
const MYELIN_STAGE_ROUTE: &str = "myelin.orch.stage_route";
const DATASTREAM_FRAME_LOG_ENV: &str = "MYELIN_DATASTREAM_FRAME_LOG"; const DATASTREAM_FRAME_LOG_ENV: &str = "MYELIN_DATASTREAM_FRAME_LOG";
pub(crate) fn run_with_options<I>( pub(crate) fn run_with_options<I>(
@ -222,7 +210,7 @@ where
}; };
let actors_channel = orch_datastream.channel_by_name("runtime.actors"); let actors_channel = orch_datastream.channel_by_name("runtime.actors");
let orch_stats_hook = orch_datastream.producer.stats_hook_on(actors_channel); let orch_stats_hook = orch_datastream.stats_hook_on(actors_channel);
// Build the core swactor runtime parts, clone the routing handle needed by // Build the core swactor runtime parts, clone the routing handle needed by
// integrations, then hand the workers to the engine. The engine owns both // integrations, then hand the workers to the engine. The engine owns both
@ -348,7 +336,7 @@ where
json!({"transport":"iroh","routes":"attached","protocol_ticker":"engine-hosted"}), json!({"transport":"iroh","routes":"attached","protocol_ticker":"engine-hosted"}),
); );
let (frame_tx, frame_rx) = mpsc::channel::<CollectedDatastreamFrame>(); let collector = FrameCollector::new();
bootstrap( bootstrap(
&mut orch_datastream, &mut orch_datastream,
None, None,
@ -490,8 +478,7 @@ where
driver: &mut driver, driver: &mut driver,
stack: &stack, stack: &stack,
obs_rx: &obs_rx, obs_rx: &obs_rx,
frame_rx: &frame_rx, collector: &collector,
frame_tx: &frame_tx,
orchestrator_reports: &orchestrator_reports, orchestrator_reports: &orchestrator_reports,
stop_rx: &stop_rx, stop_rx: &stop_rx,
dashboard: dashboard.as_ref(), dashboard: dashboard.as_ref(),
@ -570,8 +557,7 @@ where
driver: &mut driver, driver: &mut driver,
stack: &stack, stack: &stack,
obs_rx: &obs_rx, obs_rx: &obs_rx,
frame_rx: &frame_rx, collector: &collector,
frame_tx: &frame_tx,
orchestrator_reports: &orchestrator_reports, orchestrator_reports: &orchestrator_reports,
stop_rx: &stop_rx, stop_rx: &stop_rx,
dashboard: dashboard.as_ref(), dashboard: dashboard.as_ref(),
@ -2053,8 +2039,7 @@ struct RuntimeReadyAckLoop<'a> {
driver: &'a mut IrohDriver, driver: &'a mut IrohDriver,
stack: &'a DistributionRuntimeStack, stack: &'a DistributionRuntimeStack,
obs_rx: &'a mpsc::Receiver<PluginObservation>, obs_rx: &'a mpsc::Receiver<PluginObservation>,
frame_rx: &'a mpsc::Receiver<CollectedDatastreamFrame>, collector: &'a FrameCollector,
frame_tx: &'a mpsc::Sender<CollectedDatastreamFrame>,
orchestrator_reports: &'a swactor::runtime::Inbox<OrchestratorReport>, orchestrator_reports: &'a swactor::runtime::Inbox<OrchestratorReport>,
stop_rx: &'a mpsc::Receiver<()>, stop_rx: &'a mpsc::Receiver<()>,
dashboard: Option<&'a DashboardSupport>, dashboard: Option<&'a DashboardSupport>,
@ -2078,8 +2063,7 @@ fn wait_for_runtime_ready_acks(
driver, driver,
stack, stack,
obs_rx, obs_rx,
frame_rx, collector,
frame_tx,
orchestrator_reports, orchestrator_reports,
stop_rx, stop_rx,
dashboard, dashboard,
@ -2127,7 +2111,7 @@ fn wait_for_runtime_ready_acks(
}) { }) {
return Ok(false); return Ok(false);
} }
pump(driver, frame_tx); collector.pump(driver);
drain_orch_stdio_capture( drain_orch_stdio_capture(
orch_stdio_rx, orch_stdio_rx,
orch_datastream, orch_datastream,
@ -2143,7 +2127,12 @@ fn wait_for_runtime_ready_acks(
while let Ok(observation) = obs_rx.try_recv() { while let Ok(observation) = obs_rx.try_recv() {
emit_plugin_observation(orch_datastream, dashboard, provider, &observation); emit_plugin_observation(orch_datastream, dashboard, provider, &observation);
} }
drain_frames(frame_rx, dashboard, orch_datastream); collector.drain(|stream, channel, frame| {
if let Some(d) = dashboard {
d.publish_frame(stream, channel, frame);
}
orch_datastream.archive_frame("node", stream, channel, frame);
});
while let Some(report) = orchestrator_reports.try_recv() { while let Some(report) = orchestrator_reports.try_recv() {
let OrchestratorReport::NodeRuntimeReadyAck { let OrchestratorReport::NodeRuntimeReadyAck {
run_id: ack_run_id, run_id: ack_run_id,
@ -2340,8 +2329,7 @@ fn start_and_provision_workers(
driver, driver,
stack, stack,
obs_rx, obs_rx,
frame_rx, collector,
frame_tx,
orchestrator_reports, orchestrator_reports,
stop_rx, stop_rx,
dashboard, dashboard,
@ -2437,8 +2425,13 @@ fn start_and_provision_workers(
if provisioned_nodes.awaiting_runtime() || provisioned_nodes.is_converged() { if provisioned_nodes.awaiting_runtime() || provisioned_nodes.is_converged() {
break; break;
} }
pump(driver, frame_tx); collector.pump(driver);
drain_frames(frame_rx, dashboard, orch_datastream); collector.drain(|stream, channel, frame| {
if let Some(d) = dashboard {
d.publish_frame(stream, channel, frame);
}
orch_datastream.archive_frame("node", stream, channel, frame);
});
drain_orch_stdio_capture( drain_orch_stdio_capture(
orch_stdio_rx, orch_stdio_rx,
orch_datastream, orch_datastream,
@ -2487,8 +2480,7 @@ fn start_and_provision_workers(
driver, driver,
stack, stack,
obs_rx, obs_rx,
frame_rx, collector,
frame_tx,
orchestrator_reports, orchestrator_reports,
stop_rx, stop_rx,
dashboard, dashboard,
@ -2533,8 +2525,7 @@ fn start_and_provision_workers(
driver, driver,
stack, stack,
obs_rx, obs_rx,
frame_rx, collector,
frame_tx,
orchestrator_reports, orchestrator_reports,
stop_rx, stop_rx,
dashboard, dashboard,
@ -2576,8 +2567,13 @@ fn start_and_provision_workers(
provisioned_nodes provisioned_nodes
.poll(SystemTime::now()) .poll(SystemTime::now())
.map_err(|error| format!("cluster convergence: {error}"))?; .map_err(|error| format!("cluster convergence: {error}"))?;
pump(driver, frame_tx); collector.pump(driver);
drain_frames(frame_rx, dashboard, orch_datastream); collector.drain(|stream, channel, frame| {
if let Some(d) = dashboard {
d.publish_frame(stream, channel, frame);
}
orch_datastream.archive_frame("node", stream, channel, frame);
});
while let Ok(observation) = obs_rx.try_recv() { while let Ok(observation) = obs_rx.try_recv() {
emit_plugin_observation(orch_datastream, dashboard, &config.provider, &observation); emit_plugin_observation(orch_datastream, dashboard, &config.provider, &observation);
} }
@ -2612,8 +2608,7 @@ fn start_and_provision_workers(
driver, driver,
stack, stack,
obs_rx, obs_rx,
frame_rx, collector,
frame_tx,
orchestrator_reports, orchestrator_reports,
stop_rx, stop_rx,
dashboard, dashboard,
@ -2636,8 +2631,7 @@ fn start_and_provision_workers(
driver, driver,
stack, stack,
obs_rx, obs_rx,
frame_rx, collector,
frame_tx,
orchestrator_reports, orchestrator_reports,
stop_rx, stop_rx,
dashboard, dashboard,
@ -2967,8 +2961,7 @@ fn wait_for_runtime_readies(
driver, driver,
stack, stack,
obs_rx, obs_rx,
frame_rx, collector,
frame_tx,
orchestrator_reports, orchestrator_reports,
stop_rx, stop_rx,
dashboard, dashboard,
@ -2988,7 +2981,7 @@ fn wait_for_runtime_readies(
cluster.current_attempt(*node_id) cluster.current_attempt(*node_id)
== Some(::provisioning::NodeAttemptId(ready.readiness_id)) == Some(::provisioning::NodeAttemptId(ready.readiness_id))
}); });
pump(driver, frame_tx); collector.pump(driver);
emit_swim_transitions( emit_swim_transitions(
orch_datastream, orch_datastream,
dashboard, dashboard,
@ -2997,7 +2990,12 @@ fn wait_for_runtime_readies(
stack, stack,
); );
emit_swim_probe_events(orch_datastream, dashboard, stack, "runtime_ready_wait"); emit_swim_probe_events(orch_datastream, dashboard, stack, "runtime_ready_wait");
drain_frames(frame_rx, dashboard, orch_datastream); collector.drain(|stream, channel, frame| {
if let Some(d) = dashboard {
d.publish_frame(stream, channel, frame);
}
orch_datastream.archive_frame("node", stream, channel, frame);
});
drain_orch_stdio_capture( drain_orch_stdio_capture(
orch_stdio_rx, orch_stdio_rx,
orch_datastream, orch_datastream,
@ -3072,8 +3070,7 @@ fn wait_for_weights_loaded_count(
driver, driver,
stack, stack,
obs_rx, obs_rx,
frame_rx, collector,
frame_tx,
orchestrator_reports, orchestrator_reports,
stop_rx, stop_rx,
dashboard, dashboard,
@ -3096,7 +3093,7 @@ fn wait_for_weights_loaded_count(
let mut stage_last_sends = BTreeMap::<u32, Instant>::new(); let mut stage_last_sends = BTreeMap::<u32, Instant>::new();
let mut load_progress = BTreeMap::<u64, StageLoadProgress>::new(); let mut load_progress = BTreeMap::<u64, StageLoadProgress>::new();
loop { loop {
pump(driver, frame_tx); collector.pump(driver);
emit_swim_transitions(orch_datastream, dashboard, run_id, node_id, stack); emit_swim_transitions(orch_datastream, dashboard, run_id, node_id, stack);
emit_swim_probe_events(orch_datastream, dashboard, stack, "weights_loaded_wait"); emit_swim_probe_events(orch_datastream, dashboard, stack, "weights_loaded_wait");
drain_orch_stdio_capture(orch_stdio_rx, orch_datastream, dashboard, run_id, node_id); drain_orch_stdio_capture(orch_stdio_rx, orch_datastream, dashboard, run_id, node_id);
@ -3124,7 +3121,7 @@ fn wait_for_weights_loaded_count(
let mut provision = PipelineStageProvision { let mut provision = PipelineStageProvision {
driver: &mut *driver, driver: &mut *driver,
stack, stack,
frame_tx, collector,
dashboard, dashboard,
orch_datastream: &mut *orch_datastream, orch_datastream: &mut *orch_datastream,
run_id, run_id,
@ -3194,7 +3191,12 @@ fn wait_for_weights_loaded_count(
| PluginObservation::StderrLine { .. } => {} | PluginObservation::StderrLine { .. } => {}
} }
} }
drain_frames_with_load_progress(frame_rx, dashboard, orch_datastream, &mut load_progress); collector.drain_with_progress(&mut load_progress, |stream, channel, frame| {
if let Some(d) = dashboard {
d.publish_frame(stream, channel, frame);
}
orch_datastream.archive_frame("node", stream, channel, frame);
});
while let Some(report) = orchestrator_reports.try_recv() { while let Some(report) = orchestrator_reports.try_recv() {
match report { match report {
OrchestratorReport::WeightsReady { OrchestratorReport::WeightsReady {
@ -3230,7 +3232,7 @@ fn wait_for_weights_loaded_count(
struct PipelineStageProvision<'a> { struct PipelineStageProvision<'a> {
driver: &'a mut IrohDriver, driver: &'a mut IrohDriver,
stack: &'a DistributionRuntimeStack, stack: &'a DistributionRuntimeStack,
frame_tx: &'a mpsc::Sender<CollectedDatastreamFrame>, collector: &'a FrameCollector,
dashboard: Option<&'a DashboardSupport>, dashboard: Option<&'a DashboardSupport>,
orch_datastream: &'a mut OrchDatastream, orch_datastream: &'a mut OrchDatastream,
run_id: u64, run_id: u64,
@ -3428,7 +3430,7 @@ fn send_pipeline_stage_provision(
ctx.pipeline_coordinator, ctx.pipeline_coordinator,
ctx.stage_shard_plans, ctx.stage_shard_plans,
)?; )?;
pump(ctx.driver, ctx.frame_tx); ctx.collector.pump(ctx.driver);
Ok(()) Ok(())
} }
@ -3442,43 +3444,6 @@ struct ActivePrompt {
events: mpsc::Sender<PromptEvent>, events: mpsc::Sender<PromptEvent>,
} }
#[derive(Clone, Debug)]
struct CollectedDatastreamFrame {
stream: StreamId,
channel_name: String,
frame: Frame,
}
#[derive(Clone, Debug, Default)]
struct StageLoadProgress {
node_id: u64,
stage_index: Option<u32>,
phase: Option<String>,
bytes_done: Option<u64>,
bytes_total: Option<u64>,
last_progress: Option<Instant>,
last_worker_event: Option<String>,
failure_reason: Option<String>,
host_gpu_samples: u64,
}
impl StageLoadProgress {
fn to_json(&self) -> Value {
json!({
"node_id": self.node_id,
"stage_index": self.stage_index,
"phase": self.phase.as_deref().unwrap_or("unknown"),
"bytes_done": self.bytes_done,
"bytes_total": self.bytes_total,
"last_progress_age_ms": self.last_progress.map(|at| at.elapsed().as_millis()),
"last_worker_event": self.last_worker_event,
"failure_reason": self.failure_reason,
"host_gpu_samples": self.host_gpu_samples,
"host_gpu_missing": self.host_gpu_samples == 0,
})
}
}
fn stage_load_phase_is_active(phase: Option<&str>) -> bool { fn stage_load_phase_is_active(phase: Option<&str>) -> bool {
matches!( matches!(
phase, phase,
@ -3556,425 +3521,6 @@ fn stage_load_liveness_detail(
}) })
} }
fn update_load_progress_from_frame(
progress: &mut BTreeMap<u64, StageLoadProgress>,
collected: &CollectedDatastreamFrame,
now: Instant,
) {
let stream_node_id = collected.stream.node.as_str().parse::<u64>().ok();
if collected.channel_name == "host.gpu" {
if let Some(node_id) = stream_node_id {
let entry = progress
.entry(node_id)
.or_insert_with(|| StageLoadProgress {
node_id,
..StageLoadProgress::default()
});
entry.host_gpu_samples = entry.host_gpu_samples.saturating_add(1);
}
return;
}
let Ok(value) = serde_json::from_slice::<Value>(&collected.frame.payload) else {
return;
};
if value.get("type").and_then(Value::as_str) == Some("NodeEvent") {
update_load_progress_from_node_event(progress, &value, now);
return;
}
if collected.channel_name == "myelin.worker.weights" {
let Some(node_id) = stream_node_id else {
return;
};
update_load_progress_from_worker_event(progress, node_id, None, &value, now);
}
}
fn update_load_progress_from_node_event(
progress: &mut BTreeMap<u64, StageLoadProgress>,
value: &Value,
now: Instant,
) {
let Some(node_id) = numeric_json_field(value, "node_id") else {
return;
};
let stage_index =
numeric_json_field(value, "stage_index").and_then(|stage| u32::try_from(stage).ok());
let phase = value.get("phase").and_then(Value::as_str);
let status = value.get("status").and_then(Value::as_str);
let detail = value.get("detail").unwrap_or(&Value::Null);
if phase == Some("load_weights") {
let load_phase = match status {
Some("started") => Some("loading_weights"),
Some("ready") => Some("weights_loaded"),
Some("failed") => Some("failed"),
_ => None,
};
if let Some(load_phase) = load_phase {
let entry = progress
.entry(node_id)
.or_insert_with(|| StageLoadProgress {
node_id,
..StageLoadProgress::default()
});
entry.stage_index = stage_index.or(entry.stage_index);
entry.phase = Some(load_phase.to_owned());
entry.last_progress = Some(now);
if status == Some("failed") {
entry.failure_reason = detail
.get("error")
.and_then(Value::as_str)
.map(str::to_owned);
}
}
}
if let Some(worker_event) = detail.get("event") {
update_load_progress_from_worker_event(progress, node_id, stage_index, worker_event, now);
}
}
fn update_load_progress_from_worker_event(
progress: &mut BTreeMap<u64, StageLoadProgress>,
node_id: u64,
stage_index: Option<u32>,
event: &Value,
now: Instant,
) {
let Some(event_type) = event.get("type").and_then(Value::as_str) else {
return;
};
let Some(phase) = load_phase_for_worker_event(event_type) else {
return;
};
let entry = progress
.entry(node_id)
.or_insert_with(|| StageLoadProgress {
node_id,
..StageLoadProgress::default()
});
entry.stage_index = stage_index.or(entry.stage_index);
entry.phase = Some(phase.to_owned());
entry.last_worker_event = Some(event_type.to_owned());
entry.last_progress = Some(now);
if let Some(bytes_done) =
numeric_json_field(event, "bytes_done").or_else(|| numeric_json_field(event, "bytes"))
{
entry.bytes_done = Some(bytes_done);
}
if let Some(bytes_total) = numeric_json_field(event, "bytes_total") {
entry.bytes_total = Some(bytes_total);
}
}
fn numeric_json_field(value: &Value, field: &str) -> Option<u64> {
value
.get(field)
.and_then(|value| value.as_u64().or_else(|| value.as_str()?.parse().ok()))
}
fn load_phase_for_worker_event(event_type: &str) -> Option<&'static str> {
match event_type {
"GgufDownloadStarted" | "GgufDownloadProgress" => Some("prefetching_model"),
"GgufCacheReady" => Some("cache_ready"),
"StageShardFetchStarted"
| "StageShardRangeFetchStarted"
| "StageShardRangeFetchReady"
| "StageShardTensorFetchStarted"
| "StageShardTensorFetchReady" => Some("fetching_stage_shard"),
"StageShardCacheReady" => Some("stage_shard_cache_ready"),
"StageShardReady" => Some("stage_shard_ready"),
"StageShardFetchFailed" => Some("failed"),
"PipelineStageFromGgufStarted" => Some("constructing_stage"),
"PipelineStageFromGgufReady" => Some("stage_constructed"),
"TokenizerBuildStarted" => Some("building_tokenizer"),
"TokenizerBuildReady" => Some("tokenizer_ready"),
"WeightsLoaded" => Some("weights_loaded"),
"WorkerFatal" => Some("failed"),
_ => None,
}
}
fn drain_datastream_connections(
driver: &IrohDriver,
frame_tx: &mpsc::Sender<CollectedDatastreamFrame>,
) {
for read in driver.drain_datastream_reads() {
let mut channels = read
.header
.channels
.iter()
.map(|descriptor| {
(
ChannelRef {
stream: descriptor.stream.clone(),
channel: descriptor.id,
},
descriptor.name.clone(),
)
})
.collect::<BTreeMap<_, _>>();
for event in read.events {
match event {
DatastreamEvent::ChannelDeclared(descriptor) => {
channels.insert(
ChannelRef {
stream: descriptor.stream.clone(),
channel: descriptor.id,
},
descriptor.name,
);
}
DatastreamEvent::Frame(delivery) => {
let channel_name = channels
.get(&delivery.channel)
.cloned()
.unwrap_or_else(|| format!("channel#{}", delivery.channel.channel.0));
let frame = Frame::new(
delivery.channel.channel,
delivery.position,
delivery.payload,
);
if frame_tx
.send(CollectedDatastreamFrame {
stream: delivery.channel.stream,
channel_name,
frame,
})
.is_err()
{
return;
}
}
DatastreamEvent::StreamDeclared(_) | DatastreamEvent::StreamEnded(_) => {}
}
}
}
}
struct OrchDatastream {
stream: StreamId,
endpoint: DatastreamEndpoint,
producer: DatastreamProducer,
channels: BTreeMap<String, ChannelId>,
channel_names: BTreeMap<ChannelId, String>,
archive: Option<FrameArchive>,
}
impl OrchDatastream {
fn new(run_id: u64, frame_log: Option<&Path>) -> Result<Self, String> {
let stream = StreamId::new(NodeId::new("myelin-orchestrator"), Lifetime(run_id));
let endpoint = DatastreamEndpoint::with_descriptor(
StreamDescriptor {
stream: stream.clone(),
label: Some("myelin orchestrator".to_owned()),
origin: StreamOrigin::Orchestrator,
},
4096,
1024,
);
let producer = endpoint.producer();
let mut out = Self {
stream,
endpoint,
producer,
channels: BTreeMap::new(),
channel_names: BTreeMap::new(),
archive: frame_log
.map(|p| FrameArchive::open_with_label(p, "datastream frame log"))
.transpose()?,
};
for name in [
MYELIN_PROVISIONING_EVENTS,
MYELIN_ORCH_BOOTSTRAP,
MYELIN_ORCH_PROMPT,
MYELIN_SWIM_MEMBERSHIP,
MYELIN_STAGE_ROUTE,
] {
out.channel_by_name(name);
}
out.record_channel::<MembershipTransition>();
out.record_channel::<SwimProbeEvent>();
Ok(out)
}
fn channel_by_name(&mut self, name: &str) -> ChannelId {
if let Some(id) = self.channels.get(name).copied() {
return id;
}
let id = self.producer.register_channel(
name,
ChannelContent::JsonRecord {
schema: Some(name.to_owned()),
},
);
self.channels.insert(name.to_owned(), id);
self.channel_names.insert(id, name.to_owned());
id
}
fn record_channel<R: Record>(&mut self) -> ChannelId {
if let Some(id) = self.channels.get(R::CHANNEL).copied() {
return id;
}
let id = self.producer.register_record::<R>();
self.channels.insert(R::CHANNEL.to_owned(), id);
self.channel_names.insert(id, R::CHANNEL.to_owned());
id
}
fn emit_event(&mut self, dashboard: Option<&DashboardSupport>, event: ProvisionEvent) {
let payload = serde_json::to_vec(&MyelinProvisionEventRecord::new(event))
.expect("serialize provisioning event");
self.emit_bytes(dashboard, MYELIN_PROVISIONING_EVENTS, payload);
}
fn emit_log(&mut self, dashboard: Option<&DashboardSupport>, line: ProvisionLogLine) {
let channel = myelin_provision_log_channel(line.node_id, line.stream);
let payload = serde_json::to_vec(&MyelinProvisionLogRecord::new(line))
.expect("serialize provision log");
self.emit_bytes(dashboard, &channel, payload);
}
fn emit_bootstrap(
&mut self,
dashboard: Option<&DashboardSupport>,
run_id: u64,
node_id: u64,
phase: &str,
status: &str,
detail: Value,
) {
self.emit_bootstrap_to_channel(
dashboard,
MYELIN_ORCH_BOOTSTRAP,
run_id,
node_id,
phase,
status,
detail,
);
}
fn emit_bootstrap_to_channel(
&mut self,
dashboard: Option<&DashboardSupport>,
channel: &str,
run_id: u64,
node_id: u64,
phase: &str,
status: &str,
detail: Value,
) {
let benchmark = benchmark::stamp("myelin-orchestrator");
let payload = serde_json::to_vec(&json!({
"schema_version": benchmark["schema_version"].clone(),
"type":"OrchBootstrap",
"event_type":"OrchBootstrap",
"event_name":phase,
"phase":phase,
"status":status,
"run_id":run_id,
"node_id":node_id,
"producer_component":benchmark["producer_component"].clone(),
"producer_instance_id":benchmark["producer_instance_id"].clone(),
"producer_process_id":benchmark["producer_process_id"].clone(),
"producer_sequence":benchmark["producer_sequence"].clone(),
"wall_clock_unix_ms":benchmark["wall_clock_unix_ms"].clone(),
"monotonic_ms":benchmark["monotonic_ms"].clone(),
"clock_source":benchmark["clock_source"].clone(),
"span_id":format!("myelin-orchestrator:{run_id}:{}:{phase}", benchmark["producer_sequence"]),
"parent_span_id":Value::Null,
"benchmark":benchmark,
"detail":detail,
}))
.expect("serialize orch bootstrap event");
self.emit_bytes(dashboard, channel, payload);
}
fn emit_prompt(
&mut self,
dashboard: Option<&DashboardSupport>,
run_id: u64,
node_id: u64,
request_id: u64,
phase: &str,
status: &str,
detail: Value,
) {
let benchmark = benchmark::stamp("myelin-orchestrator");
let payload = serde_json::to_vec(&json!({
"schema_version": benchmark["schema_version"].clone(),
"type":"OrchPromptEvent",
"event_type":"OrchPromptEvent",
"event_name":phase,
"phase":phase,
"status":status,
"run_id":run_id,
"node_id":node_id,
"request_id":request_id,
"producer_component":benchmark["producer_component"].clone(),
"producer_instance_id":benchmark["producer_instance_id"].clone(),
"producer_process_id":benchmark["producer_process_id"].clone(),
"producer_sequence":benchmark["producer_sequence"].clone(),
"wall_clock_unix_ms":benchmark["wall_clock_unix_ms"].clone(),
"monotonic_ms":benchmark["monotonic_ms"].clone(),
"clock_source":benchmark["clock_source"].clone(),
"span_id":format!("myelin-orchestrator:{run_id}:{request_id}:{}:{phase}", benchmark["producer_sequence"]),
"parent_span_id":format!("request:{request_id}"),
"benchmark":benchmark,
"detail":detail,
}))
.expect("serialize orch prompt event");
self.emit_bytes(dashboard, MYELIN_ORCH_PROMPT, payload);
}
fn emit_record<R: Record>(&mut self, dashboard: Option<&DashboardSupport>, record: &R) {
let id = self.record_channel::<R>();
self.producer.submit_record(id, record);
self.flush(dashboard, "orchestrator");
}
fn emit_bytes(
&mut self,
dashboard: Option<&DashboardSupport>,
channel: &str,
payload: Vec<u8>,
) {
self.emit_bytes_from(dashboard, channel, payload, "orchestrator");
}
fn emit_bytes_from(
&mut self,
dashboard: Option<&DashboardSupport>,
channel: &str,
payload: Vec<u8>,
source: &str,
) {
let id = self.channel_by_name(channel);
self.producer.submit_bytes(id, payload);
self.flush(dashboard, source);
}
fn flush(&mut self, dashboard: Option<&DashboardSupport>, source: &str) {
let stream = self.stream.clone();
for frame in self.endpoint.mux().drain() {
let channel = self
.channel_names
.get(&frame.channel)
.cloned()
.unwrap_or_else(|| format!("channel#{}", frame.channel.0));
ingest_dashboard_frame(dashboard, &stream, &channel, &frame);
self.archive_frame(source, &stream, &channel, &frame);
}
}
fn archive_frame(&mut self, source: &str, stream: &StreamId, channel: &str, frame: &Frame) {
if let Some(archive) = &mut self.archive {
let _ = archive.record(source, stream, channel, frame);
}
}
}
struct OrchStdioCapture; struct OrchStdioCapture;
struct OrchStdioLine { struct OrchStdioLine {
@ -4066,60 +3612,6 @@ fn drain_orch_stdio_capture(
} }
} }
#[cfg(feature = "dashboard")]
struct DashboardSupport {
handle: dashboard::DashboardHandle,
}
#[cfg(feature = "dashboard")]
impl DashboardSupport {
fn start(enabled: bool, engine: &EngineHandle) -> Result<Option<Self>, String> {
if !enabled {
return Ok(None);
}
let mut config = dashboard::DashboardConfig::default();
if let Some(port) = env_optional("MYELIN_DASHBOARD_PORT") {
config.port = port
.parse::<u16>()
.map_err(|e| format!("invalid MYELIN_DASHBOARD_PORT={port:?}: {e}"))?;
}
let handle = dashboard::DashboardHandle::new(config);
handle.register_view(Arc::new(MyelinClusterDashboardView::new()));
engine.spawn(handle.http_server());
Ok(Some(Self { handle }))
}
fn publish_frame(&self, stream: &StreamId, channel: &str, frame: &Frame) {
self.handle.publish(dashboard::FrameEvent {
stream: dashboard::StreamEvent {
node: stream.node.as_str().to_string(),
life: stream.life.0,
},
channel: channel.to_owned(),
position: frame.position.0,
payload: frame.payload.clone(),
});
}
}
#[cfg(not(feature = "dashboard"))]
struct DashboardSupport;
#[cfg(not(feature = "dashboard"))]
impl DashboardSupport {
fn start(enabled: bool, _engine: &EngineHandle) -> Result<Option<Self>, String> {
if enabled {
return Err(
"MYELIN_DASHBOARD requires building myelin-system with feature dashboard"
.to_owned(),
);
}
Ok(None)
}
fn publish_frame(&self, _stream: &StreamId, _channel: &str, _frame: &Frame) {}
}
struct ChannelObservationSink { struct ChannelObservationSink {
tx: Mutex<mpsc::Sender<PluginObservation>>, tx: Mutex<mpsc::Sender<PluginObservation>>,
} }
@ -4254,8 +3746,7 @@ fn wait_for_weights_loaded(ctx: RuntimeReadyAckLoop<'_>, stage_index: u32) -> Re
driver, driver,
stack: _, stack: _,
obs_rx, obs_rx,
frame_rx, collector,
frame_tx,
orchestrator_reports, orchestrator_reports,
stop_rx, stop_rx,
dashboard, dashboard,
@ -4267,7 +3758,7 @@ fn wait_for_weights_loaded(ctx: RuntimeReadyAckLoop<'_>, stage_index: u32) -> Re
.. ..
} = ctx; } = ctx;
loop { loop {
pump(driver, frame_tx); collector.pump(driver);
drain_orch_stdio_capture(orch_stdio_rx, orch_datastream, dashboard, run_id, node_id); drain_orch_stdio_capture(orch_stdio_rx, orch_datastream, dashboard, run_id, node_id);
if stop_requested(stop_rx) { if stop_requested(stop_rx) {
return Err("shutdown requested while waiting for weights loaded".to_owned()); return Err("shutdown requested while waiting for weights loaded".to_owned());
@ -4275,7 +3766,12 @@ fn wait_for_weights_loaded(ctx: RuntimeReadyAckLoop<'_>, stage_index: u32) -> Re
drain_observations_with_exit(obs_rx, dashboard, orch_datastream, provider, |_, status| { drain_observations_with_exit(obs_rx, dashboard, orch_datastream, provider, |_, status| {
format!("node exited while loading weights: {status:?}") format!("node exited while loading weights: {status:?}")
})?; })?;
drain_frames(frame_rx, dashboard, orch_datastream); collector.drain(|stream, channel, frame| {
if let Some(d) = dashboard {
d.publish_frame(stream, channel, frame);
}
orch_datastream.archive_frame("node", stream, channel, frame);
});
while let Some(report) = orchestrator_reports.try_recv() { while let Some(report) = orchestrator_reports.try_recv() {
match report { match report {
OrchestratorReport::WeightsReady { OrchestratorReport::WeightsReady {
@ -4914,8 +4410,7 @@ fn serve_prompts(
driver, driver,
stack, stack,
obs_rx, obs_rx,
frame_rx, collector,
frame_tx,
stop_rx, stop_rx,
dashboard, dashboard,
orch_datastream, orch_datastream,
@ -4945,7 +4440,7 @@ fn serve_prompts(
}; };
let mut active: Option<ActivePrompt> = None; let mut active: Option<ActivePrompt> = None;
loop { loop {
pump(driver, frame_tx); collector.pump(driver);
orch_datastream.flush(dashboard, "orchestrator"); orch_datastream.flush(dashboard, "orchestrator");
if let Some(pipeline) = pipeline_runtime.as_mut() { if let Some(pipeline) = pipeline_runtime.as_mut() {
pipeline.poll_driver(driver); pipeline.poll_driver(driver);
@ -4967,7 +4462,12 @@ fn serve_prompts(
&provider, &provider,
|_, status| format!("node exited: {status:?}"), |_, status| format!("node exited: {status:?}"),
)?; )?;
drain_frames(frame_rx, dashboard, orch_datastream); collector.drain(|stream, channel, frame| {
if let Some(d) = dashboard {
d.publish_frame(stream, channel, frame);
}
orch_datastream.archive_frame("node", stream, channel, frame);
});
drain_orch_stdio_capture(orch_stdio_rx, orch_datastream, dashboard, run_id, node_id); drain_orch_stdio_capture(orch_stdio_rx, orch_datastream, dashboard, run_id, node_id);
let swim_transitions = let swim_transitions =
emit_swim_transitions(orch_datastream, dashboard, run_id, node_id, stack); emit_swim_transitions(orch_datastream, dashboard, run_id, node_id, stack);
@ -4997,7 +4497,7 @@ fn serve_prompts(
orchestrator_actor, orchestrator_actor,
OrchestratorMsg::ObserveOperatorStop { run_id }, OrchestratorMsg::ObserveOperatorStop { run_id },
); );
pump(driver, frame_tx); collector.pump(driver);
return Ok(()); return Ok(());
} }
@ -5299,58 +4799,6 @@ fn emit_plugin_observation(
} }
} }
fn drain_frames(
frame_rx: &mpsc::Receiver<CollectedDatastreamFrame>,
dashboard: Option<&DashboardSupport>,
orch_datastream: &mut OrchDatastream,
) {
while let Ok(collected) = frame_rx.try_recv() {
archive_collected_frame(collected, dashboard, orch_datastream);
}
}
fn drain_frames_with_load_progress(
frame_rx: &mpsc::Receiver<CollectedDatastreamFrame>,
dashboard: Option<&DashboardSupport>,
orch_datastream: &mut OrchDatastream,
progress: &mut BTreeMap<u64, StageLoadProgress>,
) {
while let Ok(collected) = frame_rx.try_recv() {
update_load_progress_from_frame(progress, &collected, Instant::now());
archive_collected_frame(collected, dashboard, orch_datastream);
}
}
fn archive_collected_frame(
collected: CollectedDatastreamFrame,
dashboard: Option<&DashboardSupport>,
orch_datastream: &mut OrchDatastream,
) {
ingest_dashboard_frame(
dashboard,
&collected.stream,
&collected.channel_name,
&collected.frame,
);
orch_datastream.archive_frame(
"node",
&collected.stream,
&collected.channel_name,
&collected.frame,
);
}
fn ingest_dashboard_frame(
dashboard: Option<&DashboardSupport>,
stream: &StreamId,
channel: &str,
frame: &Frame,
) {
if let Some(dashboard) = dashboard {
dashboard.publish_frame(stream, channel, frame);
}
}
fn emit_swim_transitions( fn emit_swim_transitions(
orch_datastream: &mut OrchDatastream, orch_datastream: &mut OrchDatastream,
dashboard: Option<&DashboardSupport>, dashboard: Option<&DashboardSupport>,
@ -5404,15 +4852,7 @@ fn emit_swim_probe_events(
} }
} }
/// Drain iroh ingress/egress queues and datastream connections. Core pub(crate) fn env_optional(name: &str) -> Option<String> {
/// progression and protocol tick injection are owned by the engine (see
/// `spawn_protocol_ticker`); this only drains integration-owned queues
/// (ENGINE_SPEC.md).
fn pump(driver: &IrohDriver, frame_tx: &mpsc::Sender<CollectedDatastreamFrame>) {
drain_datastream_connections(driver, frame_tx);
}
fn env_optional(name: &str) -> Option<String> {
std::env::var(name) std::env::var(name)
.ok() .ok()
.map(|value| value.trim().to_owned()) .map(|value| value.trim().to_owned())

View file

@ -9,7 +9,7 @@ use datastream::hardware::gpu::{
GpuDeviceSample, GpuProcessSample, HOST_GPU_CHANNEL, HostGpuSample, GpuDeviceSample, GpuProcessSample, HOST_GPU_CHANNEL, HostGpuSample,
}; };
use datastream::hardware::net::{HOST_NET_CHANNEL, HostNetSample, NetInterfaceSample}; use datastream::hardware::net::{HOST_NET_CHANNEL, HostNetSample, NetInterfaceSample};
use datastream::record::Record; use datastream::Record;
use parking_lot::RwLock; use parking_lot::RwLock;
use serde::Serialize; use serde::Serialize;
use serde_json::{Value, json}; use serde_json::{Value, json};

View file

@ -7,9 +7,9 @@ use swactor::actor::ActorAddress;
use swactor::process_observer::ProcessOutputObserver; use swactor::process_observer::ProcessOutputObserver;
use swactor::runtime::Runtime; use swactor::runtime::Runtime;
use super::frame::{ChannelId, Frame, Lifetime, NodeId, StreamId}; use crate::frame::{ChannelId, Frame, Lifetime, NodeId, StreamId};
use crate::record::Record;
use super::mux::Mux; use super::mux::Mux;
use super::record::Record;
use super::wire::{DatastreamFrame, encode_delivery}; use super::wire::{DatastreamFrame, encode_delivery};
/// Legacy sink for frames after mux drain has assigned positions. /// Legacy sink for frames after mux drain has assigned positions.

View file

@ -1,12 +1,29 @@
//! The per-node telemetry **datastream** (see `DATASTREAM_SPEC.md`). //! The per-node telemetry **datastream** (see `DATASTREAM_SPEC.md`).
//! //!
//! A deliberately dumb pipe: producers dump bytes tagged by stream-local //! A deliberately dumb pipe: producers dump bytes tagged with a stream-local
//! channel id, a single per-node mux accepts those bytes and assigns canonical //! channel id, a single per-node mux accepts those bytes and assigns canonical
//! positions during drain, the endpoint broadcasts catalog-aware events to //! positions during drain, the endpoint broadcasts catalog-aware events to
//! subscribers, ingest reconstructs streams by position, and views are //! subscribers, ingest reconstructs streams by position, and views are
//! read-time projections over stored frames. Nothing between a producer and a //! read-time projections over stored frames. Nothing between a producer and a
//! view interprets the payload. //! view interprets the payload.
//! //!
//! ## Producer vs observer surface
//!
//! This crate has two surfaces:
//!
//! - **Producer** — re-exported at the crate root ([`DatastreamEndpoint`],
//! [`DatastreamProducer`], [`Record`], [`ChannelId`], [`StreamId`], …).
//! Everything control-plane and actor code needs to *emit* telemetry.
//!
//! - **Observer** — in submodules ([`frame::Frame`], [`frame::DatastreamEvent`],
//! [`store::Store`], [`views`], [`ingest::Consumer`]). Everything a sink
//! (dashboard, archive, transport) needs to *read* telemetry.
//!
//! The crate root deliberately does **not** re-export [`frame::Frame`] or
//! [`frame::DatastreamEvent`]. `use datastream::Frame` is a compile error; the
//! full path `datastream::frame::Frame` compiles but is banned in control-plane
//! modules by `cargo xtask check-telemetry-isolation`.
//!
//! ```text //! ```text
//! producers (caller-owned records + text) //! producers (caller-owned records + text)
//! │ bytes tagged by registered ChannelId //! │ bytes tagged by registered ChannelId
@ -46,17 +63,18 @@ pub mod transport;
pub mod views; pub mod views;
pub mod wire; pub mod wire;
// ── Producer surface (re-exported at root; safe for control-plane code) ──
pub use endpoint::{ pub use endpoint::{
CatalogSnapshot, ChannelRegistrationError, DatastreamEndpoint, DatastreamProducer, CatalogSnapshot, ChannelRegistrationError, DatastreamEndpoint, DatastreamProducer,
DatastreamSnapshot, DatastreamSubscription, DeliveryFanout, EndpointTick, SubscriberSnapshot, DatastreamSnapshot, DatastreamSubscription, DeliveryFanout, EndpointTick, SubscriberSnapshot,
SubscriptionId, frame_event_to_delivery, SubscriptionId,
}; };
pub use frame::{ pub use frame::{
ChannelContent, ChannelContentKind, ChannelDescriptor, ChannelFilter, ChannelId, ChannelRef, ChannelContent, ChannelContentKind, ChannelDescriptor, ChannelFilter, ChannelId, ChannelRef,
DatastreamEvent, Frame, FrameDelivery, Lifetime, NodeId, Position, SourceFilter, Lifetime, NodeId, Position, SourceFilter, StreamDescriptor, StreamId, StreamOrigin,
StreamDescriptor, StreamId, StreamOrigin, SubscriptionRequest, SubscriptionRequest,
}; };
pub use ingest::Consumer;
pub use mux::Mux; pub use mux::Mux;
pub use publisher_actor::{ pub use publisher_actor::{
DATASTREAM_PUBLISHER_NAME, DatastreamPublisherActor, DatastreamPublisherMsg, DATASTREAM_PUBLISHER_NAME, DatastreamPublisherActor, DatastreamPublisherMsg,
@ -64,6 +82,11 @@ pub use publisher_actor::{
}; };
pub use record::{ChannelKind, ChannelRegistry, Record}; pub use record::{ChannelKind, ChannelRegistry, Record};
pub use sink_actor::{DATASTREAM_SINK_NAME, DatastreamSink}; pub use sink_actor::{DATASTREAM_SINK_NAME, DatastreamSink};
pub use store::{GapSpan, Store, StoredStream};
pub use transport::{Delivery, Reorder, ScriptedTransport, StreamScript}; // ── Observer surface (in submodules; NOT re-exported at root) ──
pub use views::{Body, LogEntry, MergedFrame}; //
// frame::Frame, frame::DatastreamEvent, frame::FrameDelivery,
// store::Store, ingest::Consumer, views::*, transport::Delivery
//
// Access these via their module paths (e.g. `datastream::frame::Frame`).
// Control-plane modules must not import them — enforced by CI.

View file

@ -7,7 +7,7 @@
use crossbeam_channel::{Receiver, Sender, TryRecvError, TrySendError, bounded}; use crossbeam_channel::{Receiver, Sender, TryRecvError, TrySendError, bounded};
use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::atomic::{AtomicU64, Ordering};
use super::frame::{ChannelId, Frame, Position, StreamId}; use crate::frame::{ChannelId, Frame, Position, StreamId};
struct PendingFrame { struct PendingFrame {
channel: ChannelId, channel: ChannelId,

View file

@ -14,7 +14,7 @@
use swactor::actor::ActorInterface; use swactor::actor::ActorInterface;
use swactor::runtime::Ctx; use swactor::runtime::Ctx;
use super::frame::{Frame, StreamId}; use crate::frame::{Frame, StreamId};
use super::wire::{DatastreamFrame, decode_delivery}; use super::wire::{DatastreamFrame, decode_delivery};
/// Receives legacy [`DatastreamFrame`] cluster messages and folds each decoded /// Receives legacy [`DatastreamFrame`] cluster messages and folds each decoded

View file

@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize};
use swactor::Error; use swactor::Error;
use swactor_transport::{Codec, CodecRegistry, NetworkMessage}; use swactor_transport::{Codec, CodecRegistry, NetworkMessage};
use super::frame::{ChannelId, DatastreamEvent, Frame, Lifetime, NodeId, Position, StreamId}; use crate::frame::{ChannelId, DatastreamEvent, Frame, Lifetime, NodeId, Position, StreamId};
/// Why a buffer could not be decoded as an envelope. /// Why a buffer could not be decoded as an envelope.
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]

View file

@ -1,14 +1,13 @@
use std::sync::Arc; use std::sync::Arc;
use std::thread; use std::thread;
use datastream::frame::Frame;
use datastream::ingest::Consumer; use datastream::ingest::Consumer;
use datastream::mux::Mux; use datastream::mux::Mux;
use datastream::transport::{Delivery, Reorder, ScriptedTransport, StreamScript}; use datastream::transport::{Delivery, Reorder, ScriptedTransport, StreamScript};
use datastream::views::{self, Body, LogEntry}; use datastream::views::{self, Body, LogEntry};
use datastream::wire::{decode_delivery, encode_delivery}; use datastream::wire::{decode_delivery, encode_delivery};
use datastream::{ use datastream::{ChannelId, ChannelKind, ChannelRegistry, Lifetime, NodeId, Position, Record, StreamId};
ChannelId, ChannelKind, ChannelRegistry, Frame, Lifetime, NodeId, Position, Record, StreamId,
};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
const RESOURCE_CHANNEL: ChannelId = ChannelId(1); const RESOURCE_CHANNEL: ChannelId = ChannelId(1);

View file

@ -2,9 +2,9 @@ use std::time::Duration;
use datastream::{ use datastream::{
ChannelContent, ChannelContentKind, ChannelFilter, ChannelId, DatastreamEndpoint, ChannelContent, ChannelContentKind, ChannelFilter, ChannelId, DatastreamEndpoint,
DatastreamEvent, FrameDelivery, Lifetime, NodeId, Position, Record, SourceFilter, StreamId, Lifetime, NodeId, Position, Record, SourceFilter, StreamId, SubscriptionRequest,
SubscriptionRequest,
}; };
use datastream::frame::{DatastreamEvent, FrameDelivery};
use serde_json::Value; use serde_json::Value;
use swactor::actor::ActorAddress; use swactor::actor::ActorAddress;
use swactor::stats::ActorSnapshot; use swactor::stats::ActorSnapshot;

View file

@ -4,11 +4,12 @@ use std::collections::{HashMap, HashSet};
use std::net::UdpSocket; use std::net::UdpSocket;
use std::time::Duration; use std::time::Duration;
use datastream::frame::Frame;
use datastream::ingest::Consumer; use datastream::ingest::Consumer;
use datastream::mux::Mux; use datastream::mux::Mux;
use datastream::transport::Delivery; use datastream::transport::Delivery;
use datastream::wire::{decode_delivery, encode_delivery}; use datastream::wire::{decode_delivery, encode_delivery};
use datastream::{ChannelId, Frame, Lifetime, NodeId, Position, Record, StreamId}; use datastream::{ChannelId, Lifetime, NodeId, Position, Record, StreamId};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
const RESOURCE_CHANNEL: ChannelId = ChannelId(1); const RESOURCE_CHANNEL: ChannelId = ChannelId(1);

View file

@ -5,10 +5,9 @@ use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use crossbeam_channel::TryRecvError; use crossbeam_channel::TryRecvError;
use datastream::{ use datastream::{DatastreamSnapshot, DatastreamSubscription};
ChannelDescriptor, ChannelId, ChannelRef, DatastreamEvent, DatastreamSnapshot, use datastream::frame::{ChannelDescriptor, ChannelId, ChannelRef, DatastreamEvent, FrameDelivery, Position,
DatastreamSubscription, FrameDelivery, Position, StreamDescriptor, StreamDescriptor,};
};
use iroh::endpoint::{Connection, RecvStream, SendStream}; use iroh::endpoint::{Connection, RecvStream, SendStream};
use iroh::{Endpoint, EndpointAddr}; use iroh::{Endpoint, EndpointAddr};
use swactor_engine::EngineHandle; use swactor_engine::EngineHandle;

View file

@ -266,6 +266,7 @@ COMMANDS:
Run real cargo myelin-chat acceptance check and write benchmark artifacts. Run real cargo myelin-chat acceptance check and write benchmark artifacts.
myelin-chat-compare <baseline-summary.json> <candidate-summary.json> myelin-chat-compare <baseline-summary.json> <candidate-summary.json>
Compare two benchmark summaries and report comparable deltas. Compare two benchmark summaries and report comparable deltas.
check-telemetry-isolation Verify no frame types appear in control-plane modules.
test Run the basic non-binding test barrier: root crate plus each test Run the basic non-binding test barrier: root crate plus each
non-binding repository package with `cargo test -p`." non-binding repository package with `cargo test -p`."
); );
@ -6851,9 +6852,91 @@ mod tests {
} }
} }
/// Scan control-plane source files for forbidden telemetry frame type references.
/// The datastream is metrics/logging only; control decisions must never branch
/// on a frame. Frame types live in `datastream::frame::*` (not re-exported at
/// root) and must not appear in orchestration or other control modules.
fn check_telemetry_isolation() -> ExitCode {
/// Directories whose .rs files are control-plane: they must not touch
/// frame types or read-side modules.
const CONTROL_DIRS: &[&str] = &[
"apps/myelin/src/orchestration",
"crates/distribution/src",
"crates/data-plane/src",
"crates/provisioning/src",
];
/// Substrings that indicate a telemetry frame type or read-side module
/// has leaked into control code. `datastream::frame::` covers Frame,
/// DatastreamEvent, FrameDelivery, and every other frame-module type.
const FORBIDDEN: &[&str] = &[
"datastream::frame::",
"datastream::store::",
"datastream::ingest::",
"datastream::views::",
"datastream::transport::",
"CollectedDatastreamFrame",
];
let mut violations = Vec::new();
for dir in CONTROL_DIRS {
collect_rs_files(dir, &mut violations);
}
let mut found = false;
for file in &violations {
let Ok(src) = std::fs::read_to_string(file) else {
continue;
};
for (lineno, line) in src.lines().enumerate() {
for pat in FORBIDDEN {
if line.contains(pat) {
eprintln!(
"telemetry-isolation violation: {file}:{}: {}",
lineno + 1,
line.trim()
);
found = true;
}
}
}
}
if found {
eprintln!(
"\ntelemetry-isolation: control-plane code must not import frame types \
or read-side modules. Use the datastream producer API (root re-exports) \
for emitting telemetry, never `datastream::frame::*` for reading it."
);
ExitCode::from(1)
} else {
println!("telemetry-isolation: OK — no frame types in control-plane modules.");
ExitCode::SUCCESS
}
}
/// Recursively collect .rs file paths under `dir` into `out`.
fn collect_rs_files(dir: &str, out: &mut Vec<String>) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
if let Some(s) = path.to_str() {
collect_rs_files(s, out);
}
} else if path.extension().is_some_and(|ext| ext == "rs") {
if let Some(s) = path.to_str() {
out.push(s.to_owned());
}
}
}
}
fn main() -> ExitCode { fn main() -> ExitCode {
let mut args = std::env::args().skip(1); let mut args = std::env::args().skip(1);
match args.next().as_deref() { match args.next().as_deref() {
Some("check-telemetry-isolation") => check_telemetry_isolation(),
Some("test") if args.next().is_none() => run_tests(), Some("test") if args.next().is_none() => run_tests(),
Some("myelin-chat-check") => run_myelin_chat_check(args.collect()), Some("myelin-chat-check") => run_myelin_chat_check(args.collect()),
Some("myelin-chat-compare") => run_myelin_chat_compare(args.collect()), Some("myelin-chat-compare") => run_myelin_chat_compare(args.collect()),