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:
parent
1853d3dac5
commit
b8aff00dc1
19 changed files with 858 additions and 666 deletions
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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")]
|
||||||
|
|
|
||||||
|
|
@ -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};
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
310
apps/myelin/src/observability/frame_collector.rs
Normal file
310
apps/myelin/src/observability/frame_collector.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
338
apps/myelin/src/observability/orch_datastream.rs
Normal file
338
apps/myelin/src/observability/orch_datastream.rs
Normal 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) {}
|
||||||
|
}
|
||||||
|
|
@ -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())
|
||||||
|
|
|
||||||
|
|
@ -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};
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)]
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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()),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue