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, mut 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)?;
if self.full_stats() {
pkt.rebase_ingress_us(self.epoch_us());
}
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];
if edge.is_graph_output && self.full_stats() {
for pkt in packets {
self.record_e2e(pkt);
}
}
{
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()
}
pub(super) 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];
if self.full_stats() {
self.propagate_output_ingress(n, slot);
}
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 propagate_output_ingress(&self, n: NodeId, slot: usize) {
let node = &self.nodes[n];
let ctx = unsafe { node.ctx_slot(slot) };
let mut ingress = ctx
.inputs
.iter()
.filter_map(|pkt| pkt.as_ref().map(Packet::ingress_us))
.filter(|value| *value != 0)
.min()
.unwrap_or_else(|| self.epoch_us());
for batch in &ctx.input_batches {
for pkt in batch {
let value = pkt.ingress_us();
if value != 0 {
ingress = ingress.min(value);
}
}
}
for packets in &ctx.staging {
for pkt in packets {
pkt.mark_ingress_us(ingress);
}
}
}
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();
crate::packet::validate_type_id(got).map_err(|error| {
Error::Kernel(format!(
"[{}] input port `{}` carries an invalid type: {error}",
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();
crate::packet::validate_type_id(got).map_err(|error| {
Error::Kernel(format!(
"[{}] output port `{}` carries an invalid type: {error}",
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_cell = node.ctxs[slot].get();
let ctx_ptr = ctx_cell 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 _buffer_pool = crate::buffer_pool::enter(self.shared.buffer_pool.clone());
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 self.shared.trace_enabled() {
let start_us = t0.saturating_duration_since(self.epoch).as_micros() as i64;
let input_ts = unsafe { (*ctx_cell).input_ts.0 };
let tphase = match phase {
KernelPhase::Open => crate::trace::TracePhase::Open,
KernelPhase::Process => crate::trace::TracePhase::Process,
KernelPhase::Close => crate::trace::TracePhase::Close,
};
self.shared
.record_trace(n as u32, tphase, start_us, us, input_ts);
}
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
}
}
#[derive(Debug, Clone, Copy)]
pub(super) enum KernelPhase {
Open,
Process,
Close,
}