2026-06-23 15:42:28 +00:00
|
|
|
//! Generic datastream emission helpers.
|
2026-06-06 17:53:25 +00:00
|
|
|
//!
|
2026-06-23 15:42:28 +00:00
|
|
|
//! This module owns only integration mechanics: a per-node mux, process-output
|
|
|
|
|
//! observer plumbing, generic record/text/byte submission, and sinks that ship
|
|
|
|
|
//! ordered frames. The records and channel names belong to the crates that own
|
|
|
|
|
//! those domains.
|
2026-06-06 17:53:25 +00:00
|
|
|
|
|
|
|
|
use std::sync::Arc;
|
|
|
|
|
use std::sync::OnceLock;
|
|
|
|
|
|
|
|
|
|
use swactor::actor::ActorAddress;
|
|
|
|
|
use swactor::process_observer::ProcessOutputObserver;
|
|
|
|
|
use swactor::runtime::Runtime;
|
|
|
|
|
|
2026-06-23 15:42:28 +00:00
|
|
|
use super::frame::{ChannelId, Frame, Lifetime, NodeId, Position, StreamId};
|
2026-06-06 17:53:25 +00:00
|
|
|
use super::mux::Mux;
|
2026-06-23 15:42:28 +00:00
|
|
|
use super::record::Record;
|
|
|
|
|
use super::wire::{DatastreamFrame, encode_delivery};
|
2026-06-06 17:53:25 +00:00
|
|
|
|
|
|
|
|
/// Where assembled frames go once the mux has ordered them. A sink is the only
|
|
|
|
|
/// place transport lives; the emitter knows nothing about it.
|
|
|
|
|
pub trait FrameSink: Send {
|
|
|
|
|
/// Ship one ordered frame for `stream`. Best-effort: a sink may drop.
|
|
|
|
|
fn ship(&mut self, stream: &StreamId, frame: &Frame);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-23 15:42:28 +00:00
|
|
|
/// Static identity a node needs to build its mux.
|
2026-06-06 17:53:25 +00:00
|
|
|
pub struct EmitterConfig {
|
|
|
|
|
pub node_hex: String,
|
|
|
|
|
pub life: u64,
|
|
|
|
|
pub mux_capacity: usize,
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-23 15:42:28 +00:00
|
|
|
/// Forwards managed-process output into a node's mux using a caller-owned
|
|
|
|
|
/// channel mapping.
|
2026-06-06 17:53:25 +00:00
|
|
|
struct MuxProcObserver {
|
|
|
|
|
mux: Arc<Mux>,
|
2026-06-23 15:42:28 +00:00
|
|
|
channel_for: Arc<dyn Fn(&str, bool) -> ChannelId + Send + Sync>,
|
2026-06-06 17:53:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ProcessOutputObserver for MuxProcObserver {
|
|
|
|
|
fn on_output(&self, label: &str, is_stderr: bool, data: &[u8]) {
|
|
|
|
|
self.mux
|
2026-06-23 15:42:28 +00:00
|
|
|
.submit((self.channel_for)(label, is_stderr), data.to_vec());
|
2026-06-06 17:53:25 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-23 15:42:28 +00:00
|
|
|
/// A per-node emitter. Owns ordering (its [`Mux`]) and ships through its
|
|
|
|
|
/// [`FrameSink`]. It does not know any domain-specific record type.
|
2026-06-06 17:53:25 +00:00
|
|
|
pub struct DatastreamEmitter {
|
|
|
|
|
stream_id: StreamId,
|
|
|
|
|
mux: Arc<Mux>,
|
|
|
|
|
sink: Box<dyn FrameSink>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl DatastreamEmitter {
|
2026-06-23 15:42:28 +00:00
|
|
|
/// Build a node's emitter: a mux keyed by its stream id.
|
2026-06-06 17:53:25 +00:00
|
|
|
pub fn new(cfg: EmitterConfig, sink: Box<dyn FrameSink>) -> Self {
|
|
|
|
|
let stream_id = StreamId::new(NodeId::new(&cfg.node_hex), Lifetime(cfg.life));
|
|
|
|
|
let mux = Arc::new(Mux::new(stream_id.clone(), cfg.mux_capacity));
|
|
|
|
|
Self {
|
|
|
|
|
stream_id,
|
|
|
|
|
mux,
|
|
|
|
|
sink,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-23 15:42:28 +00:00
|
|
|
/// The stream this emitter produces.
|
|
|
|
|
pub fn stream_id(&self) -> &StreamId {
|
|
|
|
|
&self.stream_id
|
2026-06-21 19:54:41 +00:00
|
|
|
}
|
|
|
|
|
|
2026-06-23 15:42:28 +00:00
|
|
|
/// The node's mux, for producers that submit directly.
|
2026-06-06 17:53:25 +00:00
|
|
|
pub fn mux(&self) -> &Arc<Mux> {
|
|
|
|
|
&self.mux
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-23 15:42:28 +00:00
|
|
|
/// Number of positions assigned by the mux.
|
|
|
|
|
pub fn assigned(&self) -> u64 {
|
|
|
|
|
self.mux.assigned()
|
2026-06-06 17:53:25 +00:00
|
|
|
}
|
|
|
|
|
|
2026-06-23 15:42:28 +00:00
|
|
|
/// Number of frames dropped by the mux on overflow.
|
|
|
|
|
pub fn dropped(&self) -> u64 {
|
|
|
|
|
self.mux.dropped()
|
2026-06-09 09:29:07 +00:00
|
|
|
}
|
2026-06-06 17:53:25 +00:00
|
|
|
|
2026-07-05 09:59:51 +00:00
|
|
|
/// Enable or disable optional sidecar timing samples for newly submitted
|
|
|
|
|
/// frames from this emitter and its cloned submit handles.
|
|
|
|
|
pub fn set_frame_timing_enabled(&self, enabled: bool) {
|
|
|
|
|
self.mux.set_frame_timing_enabled(enabled);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Whether this emitter's mux currently emits sidecar frame timing samples.
|
|
|
|
|
pub fn frame_timing_enabled(&self) -> bool {
|
|
|
|
|
self.mux.frame_timing_enabled()
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-23 15:42:28 +00:00
|
|
|
/// Submit a typed record defined by the caller's crate.
|
|
|
|
|
pub fn submit_record<R: Record>(&self, record: &R) -> Position {
|
|
|
|
|
self.mux.submit(R::channel(), record.encode())
|
2026-06-09 09:29:07 +00:00
|
|
|
}
|
|
|
|
|
|
2026-06-23 15:42:28 +00:00
|
|
|
/// Submit UTF-8/text bytes on a caller-owned channel.
|
|
|
|
|
pub fn submit_text(&self, channel: impl Into<ChannelId>, text: impl AsRef<[u8]>) -> Position {
|
|
|
|
|
self.mux.submit(channel, text.as_ref().to_vec())
|
2026-06-09 09:29:07 +00:00
|
|
|
}
|
|
|
|
|
|
2026-06-23 15:42:28 +00:00
|
|
|
/// Submit arbitrary bytes on a caller-owned channel.
|
|
|
|
|
pub fn submit_bytes(&self, channel: impl Into<ChannelId>, bytes: Vec<u8>) -> Position {
|
|
|
|
|
self.mux.submit(channel, bytes)
|
2026-06-21 19:54:41 +00:00
|
|
|
}
|
|
|
|
|
|
2026-06-23 15:42:28 +00:00
|
|
|
/// An observer that taps managed-process output onto this node's stream.
|
|
|
|
|
/// The caller supplies the channel naming convention.
|
|
|
|
|
pub fn process_observer_with<F>(&self, channel_for: F) -> Arc<dyn ProcessOutputObserver>
|
|
|
|
|
where
|
|
|
|
|
F: Fn(&str, bool) -> ChannelId + Send + Sync + 'static,
|
|
|
|
|
{
|
|
|
|
|
Arc::new(MuxProcObserver {
|
|
|
|
|
mux: self.mux.clone(),
|
|
|
|
|
channel_for: Arc::new(channel_for),
|
|
|
|
|
})
|
2026-06-21 19:54:41 +00:00
|
|
|
}
|
|
|
|
|
|
2026-06-23 15:42:28 +00:00
|
|
|
/// Drain the mux and ship every ordered frame.
|
|
|
|
|
pub fn tick(&mut self) {
|
|
|
|
|
for frame in self.mux.drain() {
|
|
|
|
|
self.sink.ship(&self.stream_id, &frame);
|
|
|
|
|
}
|
2026-06-09 09:29:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// A cheap, cloneable handle for submitting event-driven frames onto this
|
2026-06-23 15:42:28 +00:00
|
|
|
/// node's stream from any thread.
|
2026-06-09 09:29:07 +00:00
|
|
|
pub fn event_sink(&self) -> DatastreamEventSink {
|
|
|
|
|
DatastreamEventSink {
|
|
|
|
|
mux: self.mux.clone(),
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-06-06 17:53:25 +00:00
|
|
|
}
|
|
|
|
|
|
2026-06-23 15:42:28 +00:00
|
|
|
/// A thread-safe submit handle. Holds a clone of the node's mux; submitted
|
|
|
|
|
/// frames drain in the node's main loop.
|
2026-06-09 09:29:07 +00:00
|
|
|
#[derive(Clone)]
|
|
|
|
|
pub struct DatastreamEventSink {
|
|
|
|
|
mux: Arc<Mux>,
|
2026-06-06 17:53:25 +00:00
|
|
|
}
|
|
|
|
|
|
2026-06-09 09:29:07 +00:00
|
|
|
impl DatastreamEventSink {
|
2026-06-23 15:42:28 +00:00
|
|
|
pub fn submit_record<R: Record>(&self, record: &R) -> Position {
|
|
|
|
|
self.mux.submit(R::channel(), record.encode())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn submit_text(&self, channel: impl Into<ChannelId>, text: impl AsRef<[u8]>) -> Position {
|
|
|
|
|
self.mux.submit(channel, text.as_ref().to_vec())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn submit_bytes(&self, channel: impl Into<ChannelId>, bytes: Vec<u8>) -> Position {
|
|
|
|
|
self.mux.submit(channel, bytes)
|
2026-06-06 17:53:25 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-23 15:42:28 +00:00
|
|
|
/// A sink that drops everything. Used when a node has no collector yet, so the
|
|
|
|
|
/// mux can still drain and stay bounded.
|
2026-06-09 09:29:07 +00:00
|
|
|
pub struct NoopSink;
|
|
|
|
|
|
|
|
|
|
impl FrameSink for NoopSink {
|
|
|
|
|
fn ship(&mut self, _stream: &StreamId, _frame: &Frame) {}
|
2026-06-06 17:53:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Ships frames over the swactor cluster to the orchestrator's `datastream-sink`
|
|
|
|
|
/// actor, reusing the exact `register_name`/`resolve_name` + transport-router
|
2026-06-23 15:42:28 +00:00
|
|
|
/// path the application already uses.
|
2026-06-06 17:53:25 +00:00
|
|
|
pub struct ClusterFrameSink {
|
|
|
|
|
rt: Arc<Runtime>,
|
|
|
|
|
sink: Arc<OnceLock<ActorAddress>>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ClusterFrameSink {
|
|
|
|
|
pub fn new(rt: Arc<Runtime>, sink: Arc<OnceLock<ActorAddress>>) -> Self {
|
|
|
|
|
Self { rt, sink }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl FrameSink for ClusterFrameSink {
|
|
|
|
|
fn ship(&mut self, stream: &StreamId, frame: &Frame) {
|
|
|
|
|
if let Some(addr) = self.sink.get() {
|
|
|
|
|
let _ = self.rt.send_to(
|
|
|
|
|
*addr,
|
|
|
|
|
DatastreamFrame {
|
|
|
|
|
payload: encode_delivery(stream, frame),
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|