use std::cell::UnsafeCell;
use std::collections::{BTreeMap, VecDeque};
use std::ffi::c_void;
use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU64, AtomicUsize, Ordering};
use std::sync::{Arc, Condvar, Mutex};
use std::time::Instant;
use crate::config::GraphConfig;
use crate::context::Context;
use crate::executor::ThreadPool;
use crate::kernel::{KernelInstance, PortTable};
use crate::packet::Packet;
use crate::runtime::{self, GraphShared};
use crate::status::{Error, Result};
use crate::timestamp::Timestamp;
mod build;
mod dot;
mod introspect;
pub type NodeId = usize;
pub type EdgeId = usize;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum InputPolicy {
Sync,
Immediate,
FixedSize { capacity: usize },
SyncSet { sets: Vec<Vec<usize>> },
Batch { size: usize },
}
impl InputPolicy {
fn from_config(
c: &crate::config::InputPolicyConfig,
ins: &crate::kernel::PortTable,
) -> Result<Self> {
Ok(match c.r#type.as_str() {
"immediate" => InputPolicy::Immediate,
"fixed_size" => InputPolicy::FixedSize {
capacity: c.capacity.max(1),
},
"batch" => InputPolicy::Batch {
size: c.capacity.max(1),
},
"sync_set" => {
if c.sets.is_empty() {
return Err(Error::InvalidArg(
"sync_set policy must provide sets (input port groups)".into(),
));
}
let mut resolved: Vec<Vec<usize>> = Vec::new();
let mut seen = vec![false; ins.len()];
for set in &c.sets {
if set.is_empty() {
return Err(Error::InvalidArg("sync_set group must not be empty".into()));
}
let mut group = Vec::with_capacity(set.len());
for name in set {
let idx = ins.index_by_name(name).ok_or_else(|| {
Error::InvalidArg(format!(
"sync_set references nonexistent input port `{name}`"
))
})?;
if seen[idx] {
return Err(Error::InvalidArg(format!(
"sync_set input port `{name}` appears in multiple groups (groups must be disjoint)"
)));
}
seen[idx] = true;
group.push(idx);
}
resolved.push(group);
}
if let Some(miss) = (0..ins.len()).find(|&i| !seen[i]) {
return Err(Error::InvalidArg(format!(
"sync_set must cover all input ports; at least `{}` is missing (put it in its own group to keep it independent)",
ins.name(miss).unwrap_or("?")
)));
}
InputPolicy::SyncSet { sets: resolved }
}
_ => InputPolicy::Sync,
})
}
}
struct Ready {
ts: Timestamp,
ports: Option<Vec<usize>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum State {
Created = 0,
Initialized = 1,
Running = 2,
Draining = 3,
Terminated = 4,
}
pub struct Edge {
pub name: String,
pub producer: Option<NodeId>,
pub consumers: Vec<(NodeId, usize)>,
pub is_graph_input: bool,
pub is_graph_output: bool,
closed: AtomicBool,
dropped: AtomicU64,
last_sent: Mutex<Timestamp>,
pollers: Mutex<Vec<Arc<PollerInner>>>,
observers: Mutex<Vec<Observer>>,
}
impl Edge {
fn new(name: String) -> Self {
Self {
name,
producer: None,
consumers: Vec::new(),
is_graph_input: false,
is_graph_output: false,
closed: AtomicBool::new(false),
dropped: AtomicU64::new(0),
last_sent: Mutex::new(Timestamp::unset()),
pollers: Mutex::new(Vec::new()),
observers: Mutex::new(Vec::new()),
}
}
pub fn is_closed(&self) -> bool {
self.closed.load(Ordering::SeqCst)
}
pub fn dropped_count(&self) -> u64 {
self.dropped.load(Ordering::Relaxed)
}
}
#[derive(Clone)]
enum Observer {
C {
cb: unsafe extern "C" fn(*mut c_void, crate::ffi::LMFlowPacket),
user: *mut c_void,
},
Rust(Arc<dyn Fn(&Packet) + Send + Sync>),
}
unsafe impl Send for Observer {}
pub struct PollerInner {
edge: EdgeId,
queue: Mutex<VecDeque<Packet>>,
closed: AtomicBool,
}
impl PollerInner {
fn push(&self, p: Packet) {
self.queue
.lock()
.expect("poller lock poisoned")
.push_back(p);
}
fn pop(&self) -> Option<Packet> {
self.queue.lock().expect("poller lock poisoned").pop_front()
}
fn is_empty(&self) -> bool {
self.queue.lock().expect("poller lock poisoned").is_empty()
}
}
pub struct Poller {
graph: Arc<GraphInner>,
inner: Arc<PollerInner>,
}
impl Poller {
pub fn next(&self) -> Option<Packet> {
self.next_deadline(None).ok().flatten()
}
pub fn next_timeout(&self, timeout: std::time::Duration) -> Result<Option<Packet>> {
self.next_deadline(Some(std::time::Instant::now() + timeout))
}
fn next_deadline(&self, deadline: Option<std::time::Instant>) -> Result<Option<Packet>> {
loop {
if let Some(p) = self.inner.pop() {
return Ok(Some(p));
}
if self.inner.closed.load(Ordering::SeqCst) || self.graph.shared.has_error() {
return Ok(self.inner.pop());
}
if self.graph.pump_step() {
continue;
}
let before = self.graph.activity_gen_pub();
if self.graph.is_idle_pub() {
return Ok(self.inner.pop());
}
match self.graph.remaining_for_poller(deadline) {
Some(d) => {
self.graph.wait_activity_since_pub(before, d);
}
None => return Err(Error::Timeout),
}
}
}
pub fn try_next(&self) -> Option<Packet> {
self.inner.pop()
}
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
}
#[derive(Debug)]
struct NodeSched {
opened: bool,
close_started: bool,
closed: bool,
in_flight: usize,
free_slots: Vec<usize>,
ready: VecDeque<(usize, u64)>,
next_seq: u64,
next_flush_seq: u64,
pending_flush: BTreeMap<u64, (usize, bool)>,
flushing: bool,
}
impl NodeSched {
fn new(max_in_flight: usize) -> Self {
Self {
opened: false,
close_started: false,
closed: false,
in_flight: 0,
free_slots: (0..max_in_flight).collect(),
ready: VecDeque::new(),
next_seq: 0,
next_flush_seq: 0,
pending_flush: BTreeMap::new(),
flushing: false,
}
}
}
#[derive(Debug, Default)]
struct NodeStats {
processed: AtomicU64,
errors: AtomicU64,
total_us: AtomicI64,
max_us: AtomicI64,
packets_in: AtomicU64,
packets_out: AtomicU64,
peak_queue_depth: AtomicUsize,
in_flight: AtomicUsize,
started_us: AtomicI64,
}
pub struct Node {
pub name: String,
pub kernel_name: String,
pub inputs: Vec<EdgeId>,
pub outputs: Vec<EdgeId>,
pub in_ports: Arc<PortTable>,
pub out_ports: Arc<PortTable>,
executor: Option<usize>,
policy: InputPolicy,
input_types: Vec<u64>,
kernel: KernelInstance,
ctxs: Vec<UnsafeCell<Context>>,
max_in_flight: usize,
sched: Mutex<NodeSched>,
stats: NodeStats,
input_queues: Vec<Mutex<VecDeque<Packet>>>,
input_closed: Vec<AtomicBool>,
input_is_back_edge: Vec<bool>,
source_done: AtomicBool,
input_bounds: Vec<Mutex<Timestamp>>,
}
unsafe impl Sync for Node {}
impl Node {
#[allow(clippy::mut_from_ref)]
unsafe fn ctx_slot(&self, slot: usize) -> &mut Context {
&mut *self.ctxs[slot].get()
}
fn queue_len(&self, port: usize) -> usize {
self.input_queues[port]
.lock()
.expect("queue lock poisoned")
.len()
}
fn bound(&self, port: usize) -> Timestamp {
*self.input_bounds[port].lock().expect("bound lock poisoned")
}
fn advance_bound(&self, port: usize, to: Timestamp) {
let mut b = self.input_bounds[port].lock().expect("bound lock poisoned");
if to > *b {
*b = to;
}
}
fn front_ts(&self, port: usize) -> Option<Timestamp> {
self.input_queues[port]
.lock()
.expect("queue lock poisoned")
.front()
.map(|p| p.timestamp())
}
fn is_source(&self) -> bool {
self.input_queues.is_empty()
}
fn forward_ports(&self) -> impl Iterator<Item = usize> + '_ {
(0..self.input_queues.len()).filter(move |&i| !self.input_is_back_edge[i])
}
fn readiness(&self) -> Option<Ready> {
let n = self.input_queues.len();
if n == 0 {
return (!self.source_done.load(Ordering::SeqCst)).then_some(Ready {
ts: Timestamp::unset(),
ports: None,
});
}
match &self.policy {
InputPolicy::Immediate => {
let mut min = Timestamp::done();
let mut any = false;
for i in self.forward_ports() {
if let Some(ts) = self.front_ts(i) {
any = true;
min = min.min(ts);
}
}
any.then_some(Ready {
ts: min,
ports: None,
})
}
InputPolicy::Sync | InputPolicy::FixedSize { .. } => self
.sync_align(self.forward_ports())
.map(|ts| Ready { ts, ports: None }),
InputPolicy::SyncSet { sets } => {
let mut best: Option<Ready> = None;
for set in sets {
if let Some(ts) = self.sync_align(set.iter().copied()) {
if best.as_ref().is_none_or(|b| ts < b.ts) {
best = Some(Ready {
ts,
ports: Some(set.clone()),
});
}
}
}
best
}
InputPolicy::Batch { size } => {
let have = self.queue_len(0);
let fire =
have >= *size || (have > 0 && self.input_closed[0].load(Ordering::SeqCst));
fire.then(|| Ready {
ts: self.front_ts(0).unwrap_or_else(Timestamp::unset),
ports: None,
})
}
}
}
fn sync_align(&self, ports: impl Iterator<Item = usize>) -> Option<Timestamp> {
let mut min_packet = Timestamp::done();
let mut min_bound = Timestamp::done();
for i in ports {
match self.front_ts(i) {
Some(ts) => min_packet = min_packet.min(ts),
None => min_bound = min_bound.min(self.bound(i)),
}
}
if min_packet == Timestamp::done() {
return None; }
if min_bound > min_packet {
Some(min_packet)
} else {
None }
}
fn all_inputs_closed_and_drained(&self) -> bool {
if self.is_source() {
return self.source_done.load(Ordering::SeqCst);
}
self.forward_ports()
.all(|i| self.input_closed[i].load(Ordering::SeqCst) && self.queue_len(i) == 0)
}
}
#[derive(Debug, Default)]
struct Activity {
gen: u64,
waiters: usize,
}
pub struct GraphInner {
pub shared: Arc<GraphShared>,
nodes: Vec<Node>,
edges: Vec<Edge>,
graph_inputs: Vec<EdgeId>,
graph_outputs: Vec<EdgeId>,
input_by_name: BTreeMap<String, EdgeId>,
output_by_name: BTreeMap<String, EdgeId>,
edge_by_name: BTreeMap<String, EdgeId>,
state: Mutex<State>,
main_queue: Mutex<VecDeque<NodeId>>,
executors: Vec<ThreadPool>,
in_flight: AtomicUsize,
activity: (Mutex<Activity>, Condvar),
paused: AtomicBool,
side_packets: Mutex<BTreeMap<String, Packet>>,
required_side_packets: Vec<(String, String)>,
epoch: Instant,
timing: bool,
}
pub struct Graph {
inner: Arc<GraphInner>,
}
impl Graph {
pub fn from_yaml(text: &str) -> Result<Self> {
let cfg = GraphConfig::from_yaml(text)?;
Self::from_config(cfg)
}
pub fn from_yaml_file(path: &str) -> Result<Self> {
let cfg = GraphConfig::from_yaml_file(path)?;
Self::from_config(cfg)
}
pub fn from_config(cfg: GraphConfig) -> Result<Self> {
crate::builtin::register_defaults();
let inner = GraphInner::build(cfg)?;
Ok(Self {
inner: Arc::new(inner),
})
}
pub fn state(&self) -> State {
*self.inner.state.lock().expect("state lock poisoned")
}
pub fn inner(&self) -> &Arc<GraphInner> {
&self.inner
}
pub fn set_side_packet(&self, name: &str, pkt: Packet) -> Result<()> {
if self.state() != State::Initialized {
return Err(Error::State(
"side packets must be injected before start".into(),
));
}
self.inner
.side_packets
.lock()
.expect("side packet lock poisoned")
.insert(name.to_string(), pkt);
Ok(())
}
pub fn add_poller(&self, port: &str) -> Result<Poller> {
let st = self.state();
if st != State::Initialized {
return Err(Error::State(format!(
"add_poller must be called before start (current state {st:?})"
)));
}
let edge =
*self.inner.output_by_name.get(port).ok_or_else(|| {
Error::NotFound(format!("graph output port `{port}` does not exist"))
})?;
let inner = Arc::new(PollerInner {
edge,
queue: Mutex::new(VecDeque::new()),
closed: AtomicBool::new(false),
});
self.inner.edges[edge]
.pollers
.lock()
.expect("poller list lock poisoned")
.push(inner.clone());
Ok(Poller {
graph: self.inner.clone(),
inner,
})
}
pub fn start(&self) -> Result<()> {
self.inner.start()
}
pub fn input(&self, port: &str) -> Result<Input> {
let edge =
*self.inner.input_by_name.get(port).ok_or_else(|| {
Error::NotFound(format!("graph input port `{port}` does not exist"))
})?;
Ok(Input {
graph: self.inner.clone(),
edge,
})
}
pub fn close_input(&self, port: &str) -> Result<()> {
let edge =
*self.inner.input_by_name.get(port).ok_or_else(|| {
Error::NotFound(format!("graph input port `{port}` does not exist"))
})?;
self.inner.close_edge(edge);
self.inner.set_state_draining_if_all_inputs_closed();
Ok(())
}
pub fn close_all_inputs(&self) {
for &e in &self.inner.graph_inputs {
self.inner.close_edge(e);
}
self.inner.set_state_draining_if_all_inputs_closed();
}
pub fn cancel(&self) {
self.inner.shared.cancel();
}
pub fn wait_done(&self) -> Result<()> {
self.inner.wait_done(None)
}
pub fn wait_done_timeout(&self, timeout: std::time::Duration) -> Result<()> {
self.inner
.wait_done(Some(std::time::Instant::now() + timeout))
}
pub fn wait_until_idle(&self) -> Result<()> {
self.inner.wait_until_idle(None)
}
pub fn wait_until_idle_timeout(&self, timeout: std::time::Duration) -> Result<()> {
self.inner
.wait_until_idle(Some(std::time::Instant::now() + timeout))
}
pub fn pause(&self) {
self.inner.pause();
}
pub fn resume(&self) {
self.inner.resume();
}
pub fn is_paused(&self) -> bool {
self.inner.is_paused()
}
pub fn executor_names(&self) -> Vec<&str> {
self.inner.executor_names()
}
pub fn is_idle(&self) -> bool {
self.inner.is_idle_pub()
}
pub fn observe<F>(&self, port: &str, f: F) -> Result<()>
where
F: Fn(&Packet) + Send + Sync + 'static,
{
if self.state() != State::Initialized {
return Err(Error::State(
"observe must be called before start, otherwise already-produced packets are missed".into(),
));
}
self.inner.add_observer_fn(port, Arc::new(f))
}
pub fn counter_value(&self, name: &str) -> i64 {
self.inner.shared.counter_value(name)
}
pub fn counter_names(&self) -> Vec<String> {
self.inner.shared.counter_names()
}
pub fn shared_for_inspection(&self) -> Arc<crate::runtime::GraphShared> {
self.inner.shared.clone()
}
pub fn node_count(&self) -> usize {
self.inner.nodes.len()
}
pub fn node_name(&self, i: usize) -> Option<&str> {
self.inner.nodes.get(i).map(|n| n.name.as_str())
}
pub fn input_port_names(&self) -> Vec<&str> {
self.inner
.graph_inputs
.iter()
.map(|&e| self.inner.edges[e].name.as_str())
.collect()
}
pub fn output_port_names(&self) -> Vec<&str> {
self.inner
.graph_outputs
.iter()
.map(|&e| self.inner.edges[e].name.as_str())
.collect()
}
pub fn queue_depth(&self, port: &str) -> Option<usize> {
let e = *self.inner.edge_by_name.get(port)?;
Some(self.inner.queue_depth(e))
}
pub fn dropped_count(&self, port: &str) -> Option<u64> {
let e = *self.inner.edge_by_name.get(port)?;
Some(self.inner.edges[e].dropped_count())
}
pub fn dump(&self) -> String {
self.inner.dump()
}
pub fn to_dot(&self) -> String {
self.inner.to_dot(false)
}
pub fn to_dot_with_stats(&self) -> String {
self.inner.to_dot(true)
}
pub fn node_stats(&self, i: usize) -> Option<NodeStatsSnapshot> {
self.inner.node_stats(i)
}
}
impl Drop for Graph {
fn drop(&mut self) {
self.inner.shutdown_executors_pub();
}
}
impl std::fmt::Debug for Graph {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Graph{{state:{:?}, nodes:{}, inputs:{:?}, outputs:{:?}}}",
self.state(),
self.inner.nodes.len(),
self.input_port_names(),
self.output_port_names()
)
}
}
pub struct Input {
graph: Arc<GraphInner>,
edge: EdgeId,
}
impl Input {
pub fn send(&self, pkt: Packet) -> Result<()> {
self.graph.send(self.edge, pkt, true)
}
pub fn try_send(&self, pkt: Packet) -> Result<()> {
self.graph.send(self.edge, pkt, false)
}
pub fn close(&self) {
self.graph.close_edge(self.edge);
self.graph.set_state_draining_if_all_inputs_closed();
}
}
impl std::fmt::Debug for Input {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Input{{port:`{}`}}", self.graph.edges[self.edge].name)
}
}
impl std::fmt::Debug for Poller {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Poller{{port:`{}`, pending:{}}}",
self.graph.edges[self.inner.edge].name,
self.inner.queue.lock().map(|q| q.len()).unwrap_or(0)
)
}
}
#[derive(Debug, Clone)]
pub struct NodeStatsSnapshot {
pub node_name: String,
pub kernel_name: String,
pub running: bool,
pub running_for_us: i64,
pub processed: u64,
pub errors: u64,
pub total_process_us: i64,
pub max_process_us: i64,
pub packets_in: u64,
pub packets_out: u64,
pub peak_queue_depth: usize,
pub queued: usize,
}
impl GraphInner {
fn state(&self) -> State {
*self.state.lock().expect("state lock poisoned")
}
fn set_state(&self, s: State) {
*self.state.lock().expect("state lock poisoned") = s;
}
fn start(self: &Arc<Self>) -> Result<()> {
let st = self.state();
if st != State::Initialized {
return Err(Error::State(format!(
"start can only be called in Initialized (current {st:?})"
)));
}
let provided = self.side_packets.lock().expect("side packet lock poisoned");
for (need, who) in &self.required_side_packets {
if !provided.contains_key(need) {
return Err(Error::InvalidArg(format!(
"missing required side packet `{need}` -- node `{who}` declared it in GetContract; \
inject it with set_side_packet before start"
)));
}
}
let sp = Arc::new(provided.clone());
drop(provided);
for (i, node) in self.nodes.iter().enumerate() {
for slot in 0..node.max_in_flight {
let ctx = unsafe { node.ctx_slot(slot) };
ctx.side_packets = sp.clone();
ctx.reset();
ctx.input_ts = Timestamp::unstarted();
}
let rc = self.call_kernel(i, 0, KernelPhase::Open);
if rc != 0 {
let e = unsafe { node.ctx_slot(0) }.take_error(rc);
self.shared.record_error(e.clone());
return Err(e);
}
node.sched.lock().expect("scheduler lock poisoned").opened = true;
}
self.set_state(State::Running);
let weak = Arc::downgrade(self);
for pool in &self.executors {
pool.start(weak.clone());
}
for i in 0..self.nodes.len() {
if self.nodes[i].is_source() {
self.schedule_node(i);
}
}
Ok(())
}
fn send(&self, edge: EdgeId, pkt: Packet, blocking: bool) -> Result<()> {
match self.state() {
State::Running => {}
State::Draining | State::Terminated => return Err(Error::Closed),
s => {
return Err(Error::State(format!(
"send requires the graph to be Running (current {s:?}); call start first"
)))
}
}
if self.shared.is_cancelled() {
return Err(Error::Cancelled);
}
if let Some(e) = self.shared.first_error() {
return Err(e);
}
if self.edges[edge].is_closed() {
return Err(Error::Closed);
}
if pkt.timestamp() == Timestamp::unset() {
return Err(Error::InvalidArg(
"packets on a graph input port must carry an explicit timestamp (UNSET is invalid)"
.into(),
));
}
while self.shared.over_watermark() {
if !blocking {
return Err(Error::WouldBlock);
}
if self.shared.is_cancelled() {
return Err(Error::Cancelled);
}
if let Some(e) = self.shared.first_error() {
return Err(e);
}
let before = self.activity_gen();
if self.pump_step() {
continue; }
if self.is_idle() {
return Err(Error::WouldBlock);
}
self.wait_activity_since(before, std::time::Duration::from_millis(100));
}
self.check_input_monotonic(edge, &pkt)?;
self.dispatch(edge, std::slice::from_ref(&pkt)); self.schedule_consumers(edge);
Ok(())
}
fn check_input_monotonic(&self, edge: EdgeId, pkt: &Packet) -> Result<()> {
let e = &self.edges[edge];
let mut last = e.last_sent.lock().expect("timestamp lock poisoned");
if *last != Timestamp::unset() && pkt.timestamp() <= *last {
return Err(Error::InvalidArg(format!(
"graph input port `{}` timestamps must be strictly increasing: previous {}, this one {}",
e.name,
*last,
pkt.timestamp()
)));
}
*last = pkt.timestamp();
Ok(())
}
fn dispatch(&self, edge_id: EdgeId, packets: &[Packet]) {
let edge = &self.edges[edge_id];
{
let pollers = edge.pollers.lock().expect("poller list lock poisoned");
let mut any = false;
for p in pollers.iter() {
for pkt in packets {
p.push(pkt.clone());
any = true;
}
}
if any {
self.notify_activity();
}
}
{
let observers: Vec<Observer> = {
let guard = edge.observers.lock().expect("observer list lock poisoned");
if guard.is_empty() {
Vec::new()
} else {
guard.clone()
}
};
for o in &observers {
for pkt in packets {
match o {
Observer::C { cb, user } => {
let ffi = crate::ffi::borrow_packet(pkt);
unsafe { cb(*user, ffi) };
}
Observer::Rust(f) => f(pkt),
}
}
}
}
for &(node, port) in &edge.consumers {
let cap = if self.nodes[node].input_is_back_edge[port] {
Some(1) } else {
match &self.nodes[node].policy {
InputPolicy::FixedSize { capacity } => Some(*capacity),
_ => None,
}
};
let mut dropped = 0u64;
let mut q = self.nodes[node].input_queues[port]
.lock()
.expect("queue lock poisoned");
for pkt in packets {
if let Some(cap) = cap {
while q.len() >= cap {
if let Some(old) = q.pop_front() {
self.shared.on_dequeue(old.byte_size());
dropped += 1;
} else {
break;
}
}
}
self.shared.on_enqueue(pkt.byte_size());
q.push_back(pkt.clone());
}
let depth = q.len();
self.nodes[node]
.stats
.peak_queue_depth
.fetch_max(depth, Ordering::Relaxed);
drop(q);
if let Some(last) = packets.last() {
self.nodes[node].advance_bound(port, last.timestamp().next_allowed_in_stream());
}
if dropped > 0 {
self.note_dropped(edge_id, dropped);
}
self.warn_if_over_soft_limit(edge_id, depth);
}
}
fn note_dropped(&self, edge_id: EdgeId, n: u64) {
let e = &self.edges[edge_id];
let before = e.dropped.fetch_add(n, Ordering::Relaxed); let after = before + n;
if before == 0 || after.is_power_of_two() {
runtime::log_warn(&format!(
"edge `{}` has dropped {} packets total due to the fixed_size policy (consumer can't keep up; observe with dropped_count)",
e.name, after
));
}
}
fn warn_if_over_soft_limit(&self, edge_id: EdgeId, depth: usize) {
let limit = self.shared.config.max_queue_size;
if limit == 0 || depth <= limit {
return;
}
let ratio = depth / limit;
if ratio.is_power_of_two() {
runtime::log_warn(&format!(
"edge `{}` has {} packets backlogged (soft limit {}); consumer may not be keeping up",
self.edges[edge_id].name, depth, limit
));
}
}
fn schedule_consumers(&self, edge: EdgeId) {
let consumers: Vec<NodeId> = self.edges[edge].consumers.iter().map(|&(n, _)| n).collect();
for n in consumers {
self.schedule_node(n);
}
}
fn dispatch_task(&self, n: NodeId) {
match self.nodes[n].executor {
None => {
self.main_queue
.lock()
.expect("main queue lock poisoned")
.push_back(n);
self.notify_activity();
}
Some(i) => {
self.in_flight.fetch_add(1, Ordering::SeqCst);
if !self.executors[i].submit(n) {
self.in_flight.fetch_sub(1, Ordering::SeqCst);
}
self.notify_activity();
}
}
}
pub fn run_node_on_worker(&self, n: NodeId) {
self.run_node(n);
self.in_flight.fetch_sub(1, Ordering::SeqCst);
self.notify_activity();
}
fn is_idle(&self) -> bool {
self.in_flight.load(Ordering::SeqCst) == 0
&& self
.main_queue
.lock()
.expect("main queue lock poisoned")
.is_empty()
}
fn notify_activity(&self) {
let (m, cv) = &self.activity;
let mut a = m.lock().unwrap_or_else(|e| e.into_inner());
a.gen = a.gen.wrapping_add(1);
let wake = a.waiters > 0;
drop(a);
if wake {
cv.notify_all();
}
}
fn activity_gen(&self) -> u64 {
self.activity
.0
.lock()
.unwrap_or_else(|e| e.into_inner())
.gen
}
fn wait_activity_since(&self, before: u64, timeout: std::time::Duration) {
let (m, cv) = &self.activity;
let mut a = m.lock().unwrap_or_else(|e| e.into_inner());
if a.gen != before {
return; }
a.waiters += 1;
let (mut guard, _res) = cv
.wait_timeout_while(a, timeout, |x| x.gen == before)
.unwrap_or_else(|e| e.into_inner());
guard.waiters -= 1;
drop(guard);
}
fn try_claim(&self, n: NodeId) -> bool {
if self.shared.has_error()
|| self.shared.is_cancelled()
|| self.paused.load(Ordering::SeqCst)
{
return false;
}
let node = &self.nodes[n];
let mut s = node.sched.lock().expect("scheduler lock poisoned");
if !s.opened || s.close_started {
return false;
}
if s.in_flight >= node.max_in_flight {
return false; }
let Some(ready) = node.readiness() else {
return false;
};
let ts = ready.ts;
let slot = s
.free_slots
.pop()
.expect("a free slot must exist when in_flight < max");
let seq = s.next_seq;
s.next_seq += 1;
s.in_flight += 1;
s.ready.push_back((slot, seq));
let ctx = unsafe { node.ctx_slot(slot) };
ctx.reset();
if let InputPolicy::Batch { size } = &node.policy {
let mut last_ts = ts;
let remaining = {
let mut q = node.input_queues[0].lock().expect("queue lock poisoned");
for _ in 0..*size {
let Some(p) = q.pop_front() else { break };
self.shared.on_dequeue(p.byte_size());
node.stats.packets_in.fetch_add(1, Ordering::Relaxed);
last_ts = p.timestamp();
ctx.input_batches[0].push(p);
}
q.len() };
node.advance_bound(0, last_ts.next_allowed_in_stream());
ctx.inputs_done[0] = node.input_closed[0].load(Ordering::SeqCst) && remaining == 0;
ctx.input_ts = last_ts;
return true;
}
for port in 0..node.input_queues.len() {
if node.input_is_back_edge[port] {
let (popped, remaining) = {
let mut q = node.input_queues[port].lock().expect("queue lock poisoned");
let p = q.pop_front();
(p, q.len())
};
if let Some(p) = popped {
self.shared.on_dequeue(p.byte_size());
node.stats.packets_in.fetch_add(1, Ordering::Relaxed);
ctx.inputs[port] = Some(p);
}
ctx.inputs_done[port] =
node.input_closed[port].load(Ordering::SeqCst) && remaining == 0;
continue;
}
let participates = ready.ports.as_ref().is_none_or(|set| set.contains(&port));
if !participates {
continue;
}
let (popped, remaining) = {
let mut q = node.input_queues[port].lock().expect("queue lock poisoned");
let hit = q.front().map(|p| p.timestamp()) == Some(ts);
let p = if hit { q.pop_front() } else { None };
(p, q.len())
};
if let Some(p) = popped {
self.shared.on_dequeue(p.byte_size());
node.stats.packets_in.fetch_add(1, Ordering::Relaxed);
ctx.inputs[port] = Some(p);
}
node.advance_bound(port, ts.next_allowed_in_stream());
ctx.inputs_done[port] =
node.input_closed[port].load(Ordering::SeqCst) && remaining == 0;
}
ctx.input_ts = if node.is_source() {
Timestamp(seq as i64)
} else {
ts
};
true
}
fn schedule_node(&self, n: NodeId) {
while self.try_claim(n) {
self.dispatch_task(n);
}
}
fn run_one_main_task(&self) -> bool {
let next = self
.main_queue
.lock()
.expect("main queue lock poisoned")
.pop_front();
match next {
Some(n) => {
self.run_node(n);
true
}
None => false,
}
}
fn pump_step(&self) -> bool {
self.run_one_main_task() || self.try_advance_closing()
}
fn run_node(&self, n: NodeId) {
let node = &self.nodes[n];
let inv = {
node.sched
.lock()
.expect("scheduler lock poisoned")
.ready
.pop_front()
};
let Some((slot, seq)) = inv else {
self.finish(n);
return;
};
let ok = match self.check_input_types(n, slot) {
Err(e) => {
node.stats.errors.fetch_add(1, Ordering::Relaxed);
self.shared.record_error(e);
false
}
Ok(()) => {
let rc = self.call_kernel(n, slot, KernelPhase::Process);
if node.is_source() && unsafe { node.ctx_slot(slot) }.source_done {
node.source_done.store(true, Ordering::SeqCst);
}
if rc != 0 {
let e = unsafe { node.ctx_slot(slot) }.take_error(rc);
node.stats.errors.fetch_add(1, Ordering::Relaxed);
self.shared.record_error(e);
false
} else {
node.stats.processed.fetch_add(1, Ordering::Relaxed);
true
}
}
};
self.complete_invocation(n, slot, seq, ok);
}
fn complete_invocation(&self, n: NodeId, slot: usize, seq: u64, ok: bool) {
let node = &self.nodes[n];
let mut first: Option<(usize, bool)> = None;
let be_flusher = {
let mut s = node.sched.lock().expect("scheduler lock poisoned");
if !s.flushing && s.pending_flush.is_empty() && seq == s.next_flush_seq {
s.flushing = true;
first = Some((slot, ok));
true
} else {
s.pending_flush.insert(seq, (slot, ok));
if s.flushing {
false
} else {
s.flushing = true;
true
}
}
};
if be_flusher {
loop {
let item = match first.take() {
Some(v) => Some(v),
None => {
let mut s = node.sched.lock().expect("scheduler lock poisoned");
let next = s.next_flush_seq;
match s.pending_flush.remove(&next) {
Some(v) => Some(v),
None => {
s.flushing = false;
None
}
}
}
};
let Some((fslot, fok)) = item else { break };
if fok {
self.flush_staging(n, fslot);
} else {
unsafe { node.ctx_slot(fslot) }.discard_staging();
}
unsafe { node.ctx_slot(fslot) }.clear_inputs();
{
let mut s = node.sched.lock().expect("scheduler lock poisoned");
s.next_flush_seq += 1;
s.in_flight -= 1;
s.free_slots.push(fslot);
}
}
}
self.finish(n);
}
fn check_input_types(&self, n: NodeId, slot: usize) -> Result<()> {
let node = &self.nodes[n];
let ctx = unsafe { node.ctx_slot(slot) };
for (port, &want) in node.input_types.iter().enumerate() {
if want == 0 {
continue; }
let Some(pkt) = ctx.inputs.get(port).and_then(|s| s.as_ref()) else {
continue;
};
if pkt.is_empty() {
continue; }
let got = pkt.type_id();
if got != want {
return Err(Error::Kernel(format!(
"[{}] input port `{}` type mismatch: contract declares {}, actual {}",
node.name,
node.in_ports.name(port).unwrap_or("?"),
crate::packet::type_name(want),
crate::packet::type_name(got),
)));
}
}
Ok(())
}
fn flush_staging(&self, n: NodeId, slot: usize) {
let node = &self.nodes[n];
let input_ts = unsafe { node.ctx_slot(slot) }.input_ts;
for i in 0..node.outputs.len() {
let edge = node.outputs[i];
let (mut packets, explicit_bound) = {
let ctx = unsafe { node.ctx_slot(slot) };
(
std::mem::take(&mut ctx.staging[i]),
ctx.next_bounds[i].take(),
)
};
self.flush_one(n, edge, &packets, explicit_bound, input_ts);
packets.clear();
unsafe { node.ctx_slot(slot) }.staging[i] = packets;
}
}
fn flush_one(
&self,
n: NodeId,
edge: EdgeId,
packets: &[Packet],
explicit_bound: Option<Timestamp>,
input_ts: Timestamp,
) {
if !packets.is_empty() {
self.nodes[n]
.stats
.packets_out
.fetch_add(packets.len() as u64, Ordering::Relaxed);
self.dispatch(edge, packets);
self.schedule_consumers(edge);
return;
}
let bound = match explicit_bound {
Some(b) => b,
None if input_ts.is_allowed_in_stream() => input_ts.next_allowed_in_stream(),
None => return,
};
self.propagate_bound(edge, bound);
}
fn propagate_bound(&self, edge: EdgeId, bound: Timestamp) {
let consumers: Vec<(NodeId, usize)> = self.edges[edge].consumers.clone();
for (node, port) in consumers {
self.nodes[node].advance_bound(port, bound);
self.schedule_node(node);
}
}
fn finish(&self, n: NodeId) {
self.schedule_node(n);
self.maybe_close(n);
}
fn maybe_close(&self, n: NodeId) -> bool {
let node = &self.nodes[n];
let force = self.shared.has_error() || self.shared.is_cancelled();
{
let mut s = node.sched.lock().expect("scheduler lock poisoned");
if s.close_started || s.in_flight != 0 || !s.opened {
return false;
}
if !force && !node.all_inputs_closed_and_drained() {
return false;
}
s.close_started = true;
}
{
let ctx = unsafe { node.ctx_slot(0) };
ctx.reset();
ctx.close_reason = self.shared.close_reason();
ctx.input_ts = Timestamp::done();
}
let rc = self.call_kernel(n, 0, KernelPhase::Close);
if rc != 0 {
let ctx = unsafe { node.ctx_slot(0) };
let e = ctx.take_error(rc);
ctx.discard_staging();
self.shared.record_error(e);
} else {
self.flush_staging(n, 0);
}
unsafe { node.ctx_slot(0) }.clear_inputs();
node.sched.lock().expect("scheduler lock poisoned").closed = true;
for &e in &node.outputs {
self.close_edge(e);
}
self.notify_activity();
true
}
fn close_edge(&self, edge: EdgeId) {
let e = &self.edges[edge];
if e.closed.swap(true, Ordering::SeqCst) {
return; }
for &(node, port) in &e.consumers {
self.nodes[node].input_closed[port].store(true, Ordering::SeqCst);
self.nodes[node].advance_bound(port, Timestamp::done());
self.schedule_node(node);
}
for p in e.pollers.lock().expect("poller list lock poisoned").iter() {
p.closed.store(true, Ordering::SeqCst);
}
}
fn set_state_draining_if_all_inputs_closed(&self) {
let all = self.graph_inputs.iter().all(|&e| self.edges[e].is_closed());
if all {
let mut st = self.state.lock().expect("state lock poisoned");
if *st == State::Running {
*st = State::Draining;
}
}
}
fn try_advance_closing(&self) -> bool {
let mut progressed = false;
for n in 0..self.nodes.len() {
if self.maybe_close(n) {
progressed = true;
}
}
if !progressed && self.all_nodes_closed() {
self.set_state(State::Terminated);
}
progressed
}
fn all_nodes_closed(&self) -> bool {
self.nodes
.iter()
.all(|n| n.sched.lock().expect("scheduler lock poisoned").closed)
}
fn wait_done(&self, deadline: Option<std::time::Instant>) -> Result<()> {
loop {
while self.pump_step() {}
if self.all_nodes_closed() {
break;
}
let before = self.activity_gen();
if self.is_idle() {
if self.try_advance_closing() {
continue;
}
if self.all_nodes_closed() {
break;
}
let inputs_open: Vec<&str> = self
.graph_inputs
.iter()
.filter(|&&e| !self.edges[e].is_closed())
.map(|&e| self.edges[e].name.as_str())
.collect();
if !inputs_open.is_empty() {
return Err(Error::State(format!(
"wait_done: graph input ports [{}] still open, the graph won't finish on its own -- \
call close_input/close_all_inputs first",
inputs_open.join(", ")
)));
}
let stuck: Vec<&str> = (0..self.nodes.len())
.filter(|&n| {
!self.nodes[n]
.sched
.lock()
.expect("scheduler lock poisoned")
.closed
})
.map(|n| self.nodes[n].name.as_str())
.collect();
return Err(Error::Kernel(format!(
"wait_done: all inputs closed but the graph is still idle, nodes not closed: [{}]. \
usually some kernel's output/close condition is unmet (use dump to inspect queue backlog)",
stuck.join(", ")
)));
}
match self.remaining(deadline) {
Some(d) => {
self.wait_activity_since(before, d);
}
None => return Err(Error::Timeout),
}
}
if self.all_nodes_closed() {
self.set_state(State::Terminated);
}
if self.shared.is_cancelled() {
return Err(Error::Cancelled);
}
match self.shared.first_error() {
Some(e) => Err(e),
None => Ok(()),
}
}
fn wait_until_idle(&self, deadline: Option<std::time::Instant>) -> Result<()> {
loop {
while self.run_one_main_task() {}
let before = self.activity_gen();
if self.is_idle() {
break;
}
match self.remaining(deadline) {
Some(d) => {
self.wait_activity_since(before, d);
}
None => return Err(Error::Timeout),
}
}
match self.shared.first_error() {
Some(e) => Err(e),
None => Ok(()),
}
}
fn remaining(&self, deadline: Option<std::time::Instant>) -> Option<std::time::Duration> {
const POLL_CAP: std::time::Duration = std::time::Duration::from_millis(50);
match deadline {
None => Some(POLL_CAP),
Some(d) => {
let now = std::time::Instant::now();
if now >= d {
None
} else {
Some((d - now).min(POLL_CAP))
}
}
}
}
fn call_kernel(&self, n: NodeId, slot: usize, phase: KernelPhase) -> i32 {
let node = &self.nodes[n];
let ctx_ptr = node.ctxs[slot].get() as *mut c_void;
let started = if self.timing {
Some(Instant::now())
} else {
None
};
if node.stats.in_flight.fetch_add(1, Ordering::Relaxed) == 0 {
if let Some(t) = started {
let since_epoch = t.saturating_duration_since(self.epoch).as_micros() as i64;
node.stats.started_us.store(since_epoch, Ordering::Relaxed);
}
}
let rc = unsafe {
match phase {
KernelPhase::Open => node.kernel.open(ctx_ptr),
KernelPhase::Process => node.kernel.process(ctx_ptr),
KernelPhase::Close => node.kernel.close(ctx_ptr),
}
};
node.stats.in_flight.fetch_sub(1, Ordering::Relaxed);
let Some(t0) = started else { return rc }; let us = t0.elapsed().as_micros() as i64;
if matches!(phase, KernelPhase::Process) {
node.stats.total_us.fetch_add(us, Ordering::Relaxed);
node.stats.max_us.fetch_max(us, Ordering::Relaxed);
}
let wd = self.shared.config.watchdog_ms;
if wd > 0 && us as u64 > wd * 1000 {
runtime::log_warn(&format!(
"node `{}`: one {:?} took {} ms, exceeding watchdog {} ms",
node.name,
phase,
us / 1000,
wd
));
}
rc
}
pub fn add_observer(
&self,
port: &str,
cb: unsafe extern "C" fn(*mut c_void, crate::ffi::LMFlowPacket),
user: *mut c_void,
) -> Result<()> {
let edge = *self
.output_by_name
.get(port)
.ok_or_else(|| Error::NotFound(format!("graph output port `{port}` does not exist")))?;
self.edges[edge]
.observers
.lock()
.expect("observer list lock poisoned")
.push(Observer::C { cb, user });
Ok(())
}
pub fn add_observer_fn(&self, port: &str, f: Arc<dyn Fn(&Packet) + Send + Sync>) -> Result<()> {
let edge = *self
.output_by_name
.get(port)
.ok_or_else(|| Error::NotFound(format!("graph output port `{port}` does not exist")))?;
self.edges[edge]
.observers
.lock()
.expect("observer list lock poisoned")
.push(Observer::Rust(f));
Ok(())
}
pub fn nodes_len(&self) -> usize {
self.nodes.len()
}
pub fn node_name_at(&self, i: usize) -> Option<&str> {
self.nodes.get(i).map(|n| n.name.as_str())
}
pub fn input_port_name_at(&self, i: usize) -> Option<&str> {
self.graph_inputs
.get(i)
.map(|&e| self.edges[e].name.as_str())
}
pub fn output_port_name_at(&self, i: usize) -> Option<&str> {
self.graph_outputs
.get(i)
.map(|&e| self.edges[e].name.as_str())
}
pub fn num_input_ports(&self) -> usize {
self.graph_inputs.len()
}
pub fn num_output_ports(&self) -> usize {
self.graph_outputs.len()
}
pub fn edge_id_by_name(&self, name: &str) -> Option<EdgeId> {
self.edge_by_name.get(name).copied()
}
pub fn input_edge_by_name(&self, name: &str) -> Option<EdgeId> {
self.input_by_name.get(name).copied()
}
pub fn queue_depth_by_name(&self, name: &str) -> Option<usize> {
Some(self.queue_depth(self.edge_id_by_name(name)?))
}
pub fn dropped_by_name(&self, name: &str) -> Option<u64> {
Some(self.edges[self.edge_id_by_name(name)?].dropped_count())
}
pub fn send_by_edge(&self, edge: EdgeId, pkt: Packet, blocking: bool) -> Result<()> {
self.send(edge, pkt, blocking)
}
pub fn close_edge_pub(&self, edge: EdgeId) {
self.close_edge(edge);
self.set_state_draining_if_all_inputs_closed();
}
pub fn state_pub(&self) -> State {
self.state()
}
pub fn dump_pub(&self) -> String {
self.dump()
}
pub fn node_stats_pub(&self, i: usize) -> Option<NodeStatsSnapshot> {
self.node_stats(i)
}
pub fn side_packets_mut(&self) -> &Mutex<BTreeMap<String, Packet>> {
&self.side_packets
}
pub fn start_pub(self: &Arc<Self>) -> Result<()> {
self.start()
}
pub fn wait_done_pub(&self) -> Result<()> {
self.wait_done(None)
}
pub fn pump_step_pub(&self) -> bool {
self.pump_step()
}
pub fn all_nodes_closed_pub(&self) -> bool {
self.all_nodes_closed()
}
pub fn graph_inputs(&self) -> &[EdgeId] {
&self.graph_inputs
}
pub(crate) fn remaining_for_poller(
&self,
deadline: Option<std::time::Instant>,
) -> Option<std::time::Duration> {
self.remaining(deadline)
}
pub(crate) fn wait_activity_since_pub(&self, before: u64, d: std::time::Duration) {
self.wait_activity_since(before, d);
}
pub(crate) fn activity_gen_pub(&self) -> u64 {
self.activity_gen()
}
pub(crate) fn is_idle_pub(&self) -> bool {
self.is_idle()
}
pub fn pause(&self) {
self.paused.store(true, Ordering::SeqCst);
}
pub fn resume(&self) {
self.paused.store(false, Ordering::SeqCst);
for n in 0..self.nodes.len() {
self.schedule_node(n);
}
self.notify_activity();
}
pub fn is_paused(&self) -> bool {
self.paused.load(Ordering::SeqCst)
}
pub fn executor_names(&self) -> Vec<&str> {
self.executors.iter().map(|p| p.name()).collect()
}
pub(crate) fn shutdown_executors_pub(&self) {
self.shutdown_executors();
}
fn shutdown_executors(&self) {
for p in &self.executors {
p.shutdown();
}
}
}
impl Drop for GraphInner {
fn drop(&mut self) {
self.shutdown_executors();
for n in 0..self.nodes.len() {
let need_close = {
let s = self.nodes[n].sched.lock().expect("scheduler lock poisoned");
s.opened && !s.close_started
};
if !need_close {
continue;
}
{
let ctx = unsafe { self.nodes[n].ctx_slot(0) };
ctx.reset();
ctx.close_reason = self.shared.close_reason();
ctx.input_ts = Timestamp::done();
}
let rc = self.call_kernel(n, 0, KernelPhase::Close);
if rc != 0 {
runtime::log_warn(&format!(
"node `{}`: close returned {rc} during graph destruction (ignored)",
self.nodes[n].name
));
}
unsafe { self.nodes[n].ctx_slot(0) }.clear_inputs();
self.nodes[n]
.sched
.lock()
.expect("scheduler lock poisoned")
.closed = true;
}
}
}
#[derive(Debug, Clone, Copy)]
enum KernelPhase {
Open,
Process,
Close,
}