use super::*;
impl GraphInner {
pub(super) fn state(&self) -> State {
*self.state.lock().expect("state lock poisoned")
}
pub(super) fn set_state(&self, s: State) {
*self.state.lock().expect("state lock poisoned") = s;
if s == State::Terminated {
self.request_wakeup();
}
}
pub(super) 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(),
));
}
let mut watermark_blocked = false;
while self.shared.over_watermark() {
if !blocking {
return Err(Error::WouldBlock);
}
if !watermark_blocked {
watermark_blocked = true;
self.begin_watermark_block(edge);
}
if self.shared.is_cancelled() {
self.finish_watermark_block(edge, false);
return Err(Error::Cancelled);
}
if let Some(e) = self.shared.first_error() {
self.finish_watermark_block(edge, false);
return Err(e);
}
let before = self.activity_gen();
if self.pump_step() {
continue; }
if self.workers_idle() {
self.finish_watermark_block(edge, false);
return Err(Error::WouldBlock);
}
self.wait_activity_since(before, std::time::Duration::from_millis(100));
}
if watermark_blocked {
self.finish_watermark_block(edge, true);
}
self.check_input_monotonic(edge, &pkt)?;
self.dispatch(edge, std::slice::from_ref(&pkt)); self.schedule_consumers(edge);
Ok(())
}
pub(super) 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(())
}
pub(super) fn dispatch(&self, edge_id: EdgeId, packets: &[Packet]) {
let edge = &self.edges[edge_id];
{
let pollers = edge
.pollers
.lock()
.expect("poller list lock poisoned")
.clone();
let mut any = false;
for p in &pollers {
for pkt in packets {
any |= p.push(self, pkt.clone());
}
}
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 { callback, .. } => callback(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.nodes[node].input_queue_bytes[port]
.fetch_sub(old.byte_size(), Ordering::SeqCst);
self.shared.on_dequeue(old.byte_size());
dropped += 1;
} else {
break;
}
}
}
self.shared.on_enqueue(pkt.byte_size());
self.nodes[node].input_queue_bytes[port]
.fetch_add(pkt.byte_size(), Ordering::SeqCst);
q.push_back(pkt.clone());
}
let depth = q.len();
let queued_bytes = self.nodes[node].input_queue_bytes[port].load(Ordering::SeqCst);
self.nodes[node].input_queue_stats[port]
.peak_packets
.fetch_max(depth, Ordering::Relaxed);
self.nodes[node].input_queue_stats[port]
.peak_bytes
.fetch_max(queued_bytes, Ordering::Relaxed);
if self.basic_stats() {
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);
}
if let Some(last) = packets.last() {
self.publish_bound(edge_id, last.timestamp().next_allowed_in_stream());
}
}
pub(super) 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
));
}
}
pub(super) 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
));
}
}
pub(super) 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);
}
}
pub(super) fn dispatch_task(&self, n: NodeId) {
let executor = &self.executors[self.nodes[n].executor];
if !executor.submit(n) {
self.in_flight.fetch_sub(1, Ordering::SeqCst);
} else if executor.is_delegating() {
self.request_wakeup();
}
self.notify_activity();
}
pub fn run_node_on_worker(&self, n: NodeId) {
self.run_node(n);
self.in_flight.fetch_sub(1, Ordering::SeqCst);
if self.shared.is_cancelled() || self.shared.has_error() {
self.resume_blocked_flushes();
self.finish_all_backpressure_blocks();
}
self.notify_activity();
}
pub fn wake_source_on_worker(&self, n: NodeId, generation: u64) {
let node = &self.nodes[n];
if node.source_wake_generation.load(Ordering::SeqCst) != generation {
self.notify_activity();
return;
}
node.source_waiting.store(false, Ordering::SeqCst);
node.source_wait_reason.store(0, Ordering::Relaxed);
node.source_wake_deadline_us.store(0, Ordering::Relaxed);
if matches!(self.state(), State::Running | State::Draining)
&& !self.shared.is_cancelled()
&& !self.shared.has_error()
&& !node.source_done.load(Ordering::SeqCst)
{
self.schedule_node(n);
} else {
self.maybe_close(n);
}
self.notify_activity();
}
pub fn executor_task_completed(&self) {
self.notify_activity();
self.request_wakeup();
}
pub(super) fn workers_idle(&self) -> bool {
self.in_flight.load(Ordering::SeqCst) == 0
&& !self.delegated_running.load(Ordering::Acquire)
&& self
.executors
.iter()
.all(|executor| !executor.has_pending_work())
}
pub(super) fn is_idle(&self) -> bool {
self.workers_idle()
&& self
.blocked_flush_nodes
.lock()
.expect("blocked flush lock poisoned")
.is_empty()
}
pub(super) 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();
}
}
pub(super) fn activity_gen(&self) -> u64 {
self.activity
.0
.lock()
.unwrap_or_else(|e| e.into_inner())
.gen
}
pub(super) 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);
}
pub(super) 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.blocked_flush.is_some() {
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));
self.in_flight.fetch_add(1, Ordering::SeqCst);
let ctx = unsafe { node.ctx_slot(slot) };
ctx.reset();
if let Some(plan) = &ready.batch {
for &(port, count) in &plan.take {
let remaining = {
let mut q = node.input_queues[port].lock().expect("queue lock poisoned");
for _ in 0..count {
let Some(p) = q.pop_front() else { break };
node.input_queue_bytes[port].fetch_sub(p.byte_size(), Ordering::SeqCst);
self.shared.on_dequeue(p.byte_size());
if self.basic_stats() {
node.stats.packets_in.fetch_add(1, Ordering::Relaxed);
}
ctx.input_batches[port].push(p);
}
q.len() };
node.advance_bound(port, plan.last_ts.next_allowed_in_stream());
ctx.inputs_done[port] =
node.input_closed[port].load(Ordering::SeqCst) && remaining == 0;
}
for port in 0..node.input_queues.len() {
if !node.input_is_back_edge[port] {
continue;
}
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 {
node.input_queue_bytes[port].fetch_sub(p.byte_size(), Ordering::SeqCst);
self.shared.on_dequeue(p.byte_size());
if self.basic_stats() {
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;
}
ctx.input_ts = plan.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 {
node.input_queue_bytes[port].fetch_sub(p.byte_size(), Ordering::SeqCst);
self.shared.on_dequeue(p.byte_size());
if self.basic_stats() {
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 {
node.input_queue_bytes[port].fetch_sub(p.byte_size(), Ordering::SeqCst);
self.shared.on_dequeue(p.byte_size());
if self.basic_stats() {
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
}
pub(super) fn schedule_node(&self, n: NodeId) {
while self.try_claim(n) {
self.dispatch_task(n);
self.resume_blocked_flushes();
}
}
pub(super) fn run_one_main_task(&self) -> bool {
if self
.delegated_running
.compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
.is_err()
{
return false;
}
struct RunningGuard<'a>(&'a AtomicBool);
impl Drop for RunningGuard<'_> {
fn drop(&mut self) {
self.0.store(false, Ordering::Release);
}
}
let _running = RunningGuard(&self.delegated_running);
let len = self.executors.len();
let start = self.delegated_cursor.fetch_add(1, Ordering::Relaxed) % len;
let next = (0..len).find_map(|offset| {
let index = (start + offset) % len;
self.executors[index]
.take_delegated()
.map(|node| (index, node))
});
match next {
Some((index, n)) => {
self.delegated_cursor
.store(index.wrapping_add(1), Ordering::Relaxed);
let started = self.full_stats().then(Instant::now);
self.run_node(n);
self.in_flight.fetch_sub(1, Ordering::SeqCst);
self.executors[index].complete_delegated(started.map(|started| started.elapsed()));
self.notify_activity();
true
}
None => false,
}
}
pub(super) fn pump_step(&self) -> bool {
self.run_one_main_task() || self.try_advance_closing()
}
fn delegated_tasks_pending(&self) -> bool {
self.executors
.iter()
.any(|executor| executor.is_delegating() && executor.pending() > 0)
}
pub(super) fn request_wakeup(&self) {
self.wakeup_generation.fetch_add(1, Ordering::AcqRel);
if self
.wakeup_pending
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
.is_err()
{
return;
}
let callback = self
.wakeup_callback
.lock()
.unwrap_or_else(|error| error.into_inner());
let Some(callback) = callback.as_ref() else {
self.wakeup_pending.store(false, Ordering::Release);
return;
};
if catch_unwind(AssertUnwindSafe(|| callback())).is_err() {
self.wakeup_pending.store(false, Ordering::Release);
runtime::log_warn("graph wakeup callback panicked; wakeup re-armed");
}
}
pub(super) fn set_wakeup_callback(&self, callback: Option<WakeupCallback>) {
*self
.wakeup_callback
.lock()
.unwrap_or_else(|error| error.into_inner()) = callback;
self.wakeup_pending.store(false, Ordering::Release);
if self.delegated_tasks_pending()
|| self.shared.is_cancelled()
|| self.shared.has_error()
|| self.state() == State::Terminated
{
self.request_wakeup();
}
}
pub(super) 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) => self.on_node_error(n, slot, e),
Ok(()) => {
let rc = self.call_kernel(n, slot, KernelPhase::Process);
let ctx = unsafe { node.ctx_slot(slot) };
if node.is_source() {
if ctx.source_yield.is_some() {
node.source_yield_count.fetch_add(1, Ordering::Relaxed);
}
if ctx.source_done {
node.source_done.store(true, Ordering::SeqCst);
node.source_waiting.store(false, Ordering::SeqCst);
node.source_wait_reason.store(0, Ordering::Relaxed);
node.source_wake_deadline_us.store(0, Ordering::Relaxed);
node.sched
.lock()
.expect("scheduler lock poisoned")
.source_reschedule = None;
} else {
let rate_delay = node.min_period.and_then(|period| {
let last = node.last_fire.lock().expect("last_fire lock poisoned");
last.and_then(|started| period.checked_sub(started.elapsed()))
});
let reschedule = match (ctx.source_yield, rate_delay) {
(Some(yield_delay), Some(rate_delay)) => Some(SourceReschedule {
delay: yield_delay.max(rate_delay),
reason: SourceWaitReason::RateAndYield,
}),
(Some(delay), None) => Some(SourceReschedule {
delay,
reason: SourceWaitReason::Yield,
}),
(None, Some(delay)) => Some(SourceReschedule {
delay,
reason: SourceWaitReason::Rate,
}),
(None, None) => None,
};
if let Some(reschedule) = reschedule {
node.source_waiting.store(true, Ordering::SeqCst);
node.source_wait_reason.store(
match reschedule.reason {
SourceWaitReason::Rate => 1,
SourceWaitReason::Yield => 2,
SourceWaitReason::RateAndYield => 3,
},
Ordering::Relaxed,
);
node.sched
.lock()
.expect("scheduler lock poisoned")
.source_reschedule = Some(reschedule);
}
}
} else if ctx.source_yield.is_some() && rc == 0 {
return self.complete_invocation(
n,
slot,
seq,
self.on_node_error(
n,
slot,
Error::Kernel(format!(
"[{}] source_yield is only valid for source nodes",
node.name
)),
),
);
}
if rc != 0 {
let e = unsafe { node.ctx_slot(slot) }.take_error(rc);
self.on_node_error(n, slot, e)
} else if let Err(e) = self.check_output_types(n, slot) {
self.on_node_error(n, slot, e)
} else {
if self.basic_stats() {
node.stats.processed.fetch_add(1, Ordering::Relaxed);
}
true
}
}
};
self.complete_invocation(n, slot, seq, ok);
}
pub(super) fn on_node_error(&self, n: NodeId, slot: usize, e: Error) -> bool {
let node = &self.nodes[n];
let before = node.stats.errors.fetch_add(1, Ordering::Relaxed);
match node.on_error {
OnError::Abort => {
self.shared.record_error(e);
false }
OnError::Skip => {
let after = before + 1;
if before == 0 || after.is_power_of_two() {
runtime::log_warn(&format!(
"node `{}`: skipping a failed packet (on_error=skip), {} so far: {}",
node.name, after, e
));
}
unsafe { node.ctx_slot(slot) }.discard_staging();
true
}
}
}
pub(super) fn complete_invocation(&self, n: NodeId, slot: usize, seq: u64, ok: bool) {
let node = &self.nodes[n];
unsafe { node.ctx_slot(slot) }.clear_inputs();
let mut first: Option<(usize, bool)> = None;
let be_flusher = {
let mut s = node.sched.lock().expect("scheduler lock poisoned");
if !s.flushing
&& s.blocked_flush.is_none()
&& 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 {
self.drive_invocation_flushes(n, first);
}
self.finish(n);
}
pub(super) fn schedule_source_resumption(&self, n: NodeId) {
let node = &self.nodes[n];
if !node.is_source() || !node.source_waiting.load(Ordering::SeqCst) {
return;
}
if self.shared.is_cancelled()
|| self.shared.has_error()
|| !matches!(self.state(), State::Running | State::Draining)
{
node.source_waiting.store(false, Ordering::SeqCst);
node.source_wait_reason.store(0, Ordering::Relaxed);
node.source_wake_deadline_us.store(0, Ordering::Relaxed);
node.sched
.lock()
.expect("scheduler lock poisoned")
.source_reschedule = None;
return;
}
let reschedule = {
let mut sched = node.sched.lock().expect("scheduler lock poisoned");
if sched.in_flight != 0 || sched.blocked_flush.is_some() || sched.flushing {
return;
}
sched.source_reschedule.take()
};
let Some(reschedule) = reschedule else {
return;
};
let generation = node.source_wake_generation.fetch_add(1, Ordering::SeqCst) + 1;
node.source_wake_deadline_us.store(
self.epoch_us()
.saturating_add(reschedule.delay.as_micros().min(i64::MAX as u128) as i64)
.saturating_add(1),
Ordering::Relaxed,
);
if !self.executors[node.executor].submit_source_wake(n, generation, reschedule.delay) {
node.source_waiting.store(false, Ordering::SeqCst);
node.source_wait_reason.store(0, Ordering::Relaxed);
node.source_wake_deadline_us.store(0, Ordering::Relaxed);
}
self.notify_activity();
}
pub(super) 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() {
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 == crate::packet::type_id::HOST_OBJECT {
return Err(Error::Kernel(format!(
"[{}] input port `{}` carries LMFLOW_TYPE_HOST_OBJECT, which is reserved \
and not enabled (see ADR #26); use LMFLOW_TYPE_BUFFER for numeric \
collections, or LMFLOW_TYPE_STR carrying JSON for arbitrary metadata",
node.name,
node.in_ports.name(port).unwrap_or("?"),
)));
}
if want == 0 {
continue; }
if got != want {
let hint = if got == crate::packet::type_id::NONE {
match pkt.payload() {
Some(crate::packet::Payload::Foreign(_)) => {
" (the packet carries no declared type: its type_id is \
LMFLOW_TYPE_NONE, which means \"skip type checking\"; set a real \
LMFLOW_TYPE_* on the packet you submit, or declare this port as \
any-type)"
}
_ => {
" (the packet carries no declared type because its payload is \
Rust-native, e.g. built with `Packet::new`; use \
`Packet::from_i64` / `from_f64` / `from_builtin` for built-in \
payloads, implement `InteropType` and use `Packet::from_interop` \
for a custom type, or use unsafe `Packet::new_interop` only after \
manually proving the ABI layout)"
}
}
} else {
""
};
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),
hint,
)));
}
}
Ok(())
}
pub(super) fn check_output_types(&self, n: NodeId, slot: usize) -> Result<()> {
let node = &self.nodes[n];
let ctx = unsafe { node.ctx_slot(slot) };
for (port, packets) in ctx.staging.iter().enumerate() {
let want = node.output_types[port];
for pkt in packets {
if pkt.is_empty() {
continue;
}
let got = pkt.type_id();
if got == crate::packet::type_id::HOST_OBJECT {
return Err(Error::Kernel(format!(
"[{}] output port `{}` carries LMFLOW_TYPE_HOST_OBJECT, which is reserved \
and not enabled (see ADR #26); use LMFLOW_TYPE_BUFFER for numeric \
collections, or LMFLOW_TYPE_STR carrying JSON for arbitrary metadata",
node.name,
node.out_ports.name(port).unwrap_or("?"),
)));
}
if want != crate::packet::type_id::NONE && got != want {
return Err(Error::Kernel(format!(
"[{}] output port `{}` type mismatch: contract declares {}, actual {}",
node.name,
node.out_ports.name(port).unwrap_or("?"),
crate::packet::type_name(want),
crate::packet::type_name(got),
)));
}
}
}
Ok(())
}
pub(super) 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);
}
self.publish_bound(edge, bound);
}
pub(super) fn publish_bound(&self, edge_id: EdgeId, bound: Timestamp) {
let edge = &self.edges[edge_id];
if !edge.has_timestamp_bound_subscriber.load(Ordering::Relaxed) {
return;
}
let should_publish = {
let mut last = edge
.last_published_bound
.lock()
.expect("published-bound lock poisoned");
if bound <= *last {
false
} else {
*last = bound;
true
}
};
if !should_publish {
return;
}
let event = Packet::empty().at(bound);
let pollers = edge
.pollers
.lock()
.expect("poller list lock poisoned")
.clone();
let mut any = false;
for poller in &pollers {
if poller.observe_timestamp_bounds {
any |= poller.push(self, event.clone());
}
}
if any {
self.notify_activity();
}
let observers = edge
.observers
.lock()
.expect("observer list lock poisoned")
.clone();
for observer in &observers {
match observer {
Observer::C {
cb,
user,
observe_timestamp_bounds: true,
} => {
let ffi = crate::ffi::borrow_packet(&event);
unsafe { cb(*user, ffi) };
}
Observer::Rust {
callback,
observe_timestamp_bounds: true,
} => callback(&event),
Observer::C { .. } | Observer::Rust { .. } => {}
}
}
}
pub(super) fn call_kernel(&self, n: NodeId, slot: usize, phase: KernelPhase) -> i32 {
let node = &self.nodes[n];
if self.full_stats() {
crate::packet::begin_cow_copy_scope();
}
let ctx_ptr = node.ctxs[slot].get() as *mut c_void;
let started = if self.full_stats() {
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);
}
}
if matches!(phase, KernelPhase::Process) && node.min_period.is_some() {
*node.last_fire.lock().expect("last_fire lock poisoned") = Some(Instant::now());
}
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),
}
};
if self.full_stats() {
let cow = crate::packet::end_cow_copy_scope();
if matches!(phase, KernelPhase::Process) {
node.stats
.cow_copies
.fetch_add(cow.copies, Ordering::Relaxed);
node.stats.cow_bytes.fetch_add(cow.bytes, Ordering::Relaxed);
}
}
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);
node.stats.record_latency(us.max(0) as u64);
}
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,
observe_timestamp_bounds: bool,
) -> Result<()> {
let edge = *self
.output_by_name
.get(port)
.ok_or_else(|| Error::NotFound(format!("graph output port `{port}` does not exist")))?;
if observe_timestamp_bounds {
self.edges[edge]
.has_timestamp_bound_subscriber
.store(true, Ordering::Relaxed);
}
self.edges[edge]
.observers
.lock()
.expect("observer list lock poisoned")
.push(Observer::C {
cb,
user,
observe_timestamp_bounds,
});
Ok(())
}
pub fn add_observer_fn(
&self,
port: &str,
callback: Arc<dyn Fn(&Packet) + Send + Sync>,
observe_timestamp_bounds: bool,
) -> Result<()> {
let edge = *self
.output_by_name
.get(port)
.ok_or_else(|| Error::NotFound(format!("graph output port `{port}` does not exist")))?;
if observe_timestamp_bounds {
self.edges[edge]
.has_timestamp_bound_subscriber
.store(true, Ordering::Relaxed);
}
self.edges[edge]
.observers
.lock()
.expect("observer list lock poisoned")
.push(Observer::Rust {
callback,
observe_timestamp_bounds,
});
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 node_input_ports_len(&self, node: usize) -> usize {
self.nodes.get(node).map_or(0, |value| value.in_ports.len())
}
pub fn node_input_port_name_at(&self, node: usize, port: usize) -> Option<&str> {
self.nodes.get(node)?.in_ports.name(port)
}
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(crate) fn watermark_backpressure_stats(
&self,
edge: EdgeId,
) -> WatermarkBackpressureStatsSnapshot {
let edge = &self.edges[edge];
let stats = edge.watermark_backpressure.snapshot(self.epoch_us());
WatermarkBackpressureStatsSnapshot {
port_name: edge.name.clone(),
packet_limit: self.shared.config.max_queued_packets,
total_queued_packets: self.shared.total_queued(),
blocked: stats.blocked,
active_waiters: stats.active_waiters,
blocked_for_us: stats.blocked_for_us,
block_events: stats.block_events,
total_blocked_us: stats.total_blocked_us,
}
}
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 {
let generation = self.wakeup_generation.load(Ordering::Acquire);
let progressed = self.pump_step();
if !progressed {
self.wakeup_pending.store(false, Ordering::Release);
if self.wakeup_generation.load(Ordering::Acquire) != generation
|| self.delegated_tasks_pending()
{
self.request_wakeup();
}
}
progressed
}
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(super) fn executor_stats(&self, name: &str) -> Option<ExecutorStatsSnapshot> {
let executor = self
.executors
.iter()
.find(|executor| executor.name() == name)?;
let stats = executor.stats();
let capacity = if executor.is_delegating() {
1
} else {
executor.num_threads()
};
let mut queued_counts = std::collections::BTreeMap::<NodeId, usize>::new();
for node in executor.queued_nodes() {
*queued_counts.entry(node).or_default() += 1;
}
let mut queued_nodes = queued_counts.into_iter().collect::<Vec<_>>();
queued_nodes.sort_by(|(left_node, left_count), (right_node, right_count)| {
right_count.cmp(left_count).then_with(|| {
self.nodes[*left_node]
.name
.cmp(&self.nodes[*right_node].name)
})
});
Some(ExecutorStatsSnapshot {
queued: stats.queued,
running: stats.running,
peak_queued: stats.peak_queued,
completed: stats.completed,
total_wait_us: stats.total_wait_us,
total_execution_us: stats.total_execution_us,
queued_for_us: stats.queued_for_us,
saturated: stats.queued > 0 && stats.running >= capacity,
queued_nodes: queued_nodes
.into_iter()
.take(5)
.map(|(node, count)| format!("{} ({count})", self.nodes[node].name))
.collect(),
})
}
pub(crate) fn shutdown_executors_pub(&self) {
self.shutdown_executors();
}
pub(super) fn shutdown_executors(&self) {
for p in &self.executors {
p.shutdown();
}
}
}
#[derive(Debug, Clone, Copy)]
pub(super) enum KernelPhase {
Open,
Process,
Close,
}