1use super::*;
4
5impl GraphInner {
6 pub(super) fn state(&self) -> State {
7 *self.state.lock().expect("state lock poisoned")
8 }
9 pub(super) fn set_state(&self, s: State) {
10 *self.state.lock().expect("state lock poisoned") = s;
11 if s == State::Terminated {
12 self.request_wakeup();
13 }
14 }
15
16 pub(super) fn send(&self, edge: EdgeId, mut pkt: Packet, blocking: bool) -> Result<()> {
17 match self.state() {
18 State::Running => {}
19 State::Draining | State::Terminated => return Err(Error::Closed),
20 s => {
21 return Err(Error::State(format!(
22 "send requires the graph to be Running (current {s:?}); call start first"
23 )))
24 }
25 }
26 if self.shared.is_cancelled() {
27 return Err(Error::Cancelled);
28 }
29 if let Some(e) = self.shared.first_error() {
30 return Err(e);
31 }
32 if self.edges[edge].is_closed() {
33 return Err(Error::Closed);
34 }
35 if !pkt.timestamp().is_allowed_in_stream() {
37 return Err(Error::InvalidArg(
38 format!("packets on a graph input port must carry an explicit timestamp in the legal stream range (UNSET and other internal sentinels are invalid): {}", pkt.timestamp()),
39 ));
40 }
41
42 if self.edges[edge].is_ingress_owner() {
44 return Err(Error::State(
45 "cannot send to the same input from its dispatch callback".into(),
46 ));
47 }
48 let mut watermark_blocked = false;
50 while self.shared.over_watermark() {
51 if !blocking {
52 return Err(Error::WouldBlock);
53 }
54 if !watermark_blocked {
55 watermark_blocked = true;
56 self.begin_watermark_block(edge);
57 }
58 if self.shared.is_cancelled() {
60 self.finish_watermark_block(edge, false);
61 return Err(Error::Cancelled);
62 }
63 if let Some(e) = self.shared.first_error() {
64 self.finish_watermark_block(edge, false);
65 return Err(e);
66 }
67 let before = self.activity_gen();
69 if self.pump_step() {
70 continue; }
72 if self.workers_idle() {
75 self.finish_watermark_block(edge, false);
76 return Err(Error::WouldBlock);
77 }
78 self.wait_activity_since(before, std::time::Duration::from_millis(100));
79 }
80 if watermark_blocked {
81 self.finish_watermark_block(edge, true);
82 }
83
84 let mut ingress = self.edges[edge].enter_ingress(blocking)?;
87 let result = (|| {
88 if self.edges[edge].is_closed() {
89 return Err(Error::Closed);
90 }
91 if self.shared.is_cancelled() {
92 return Err(Error::Cancelled);
93 }
94 if let Some(error) = self.shared.first_error() {
95 return Err(error);
96 }
97 self.check_input_monotonic(edge, &pkt)?;
99 if self.full_stats() {
100 pkt.rebase_ingress_us(self.epoch_us());
101 }
102
103 self.dispatch(edge, std::slice::from_ref(&pkt)); self.schedule_consumers(edge);
106 Ok(())
107 })();
108 if ingress.release_unless_closing() {
109 self.close_edge_committed(edge);
110 self.set_state_draining_if_all_inputs_closed();
111 }
112 result
113 }
114
115 pub(super) fn check_input_monotonic(&self, edge: EdgeId, pkt: &Packet) -> Result<()> {
120 let e = &self.edges[edge];
121 let mut last = e.last_sent.lock().expect("timestamp lock poisoned");
122 if *last != Timestamp::unset() && pkt.timestamp() <= *last {
123 return Err(Error::InvalidArg(format!(
124 "graph input port `{}` timestamps must be strictly increasing: previous {}, this one {}",
125 e.name,
126 *last,
127 pkt.timestamp()
128 )));
129 }
130 *last = pkt.timestamp();
131 Ok(())
132 }
133
134 pub(super) fn dispatch(&self, edge_id: EdgeId, packets: &[Packet]) {
138 let edge = &self.edges[edge_id];
139 if edge.is_graph_output && self.full_stats() {
140 for pkt in packets {
141 self.record_e2e(pkt);
142 }
143 }
144
145 {
147 let pollers = edge
148 .pollers
149 .lock()
150 .expect("poller list lock poisoned")
151 .clone();
152 let mut any = false;
153 for p in &pollers {
154 for pkt in packets {
155 any |= p.push(self, pkt.clone());
156 }
157 }
158 if any {
159 self.notify_activity();
160 }
161 }
162 {
163 let observers: Vec<Observer> = {
167 let guard = edge.observers.lock().expect("observer list lock poisoned");
168 if guard.is_empty() {
169 Vec::new()
170 } else {
171 guard.clone()
172 }
173 };
174 for o in &observers {
175 for pkt in packets {
176 match o {
177 Observer::C { cb, user, .. } => {
178 let ffi = crate::ffi::borrow_packet(pkt);
179 unsafe { cb(*user, ffi) };
180 }
181 Observer::Rust { callback, .. } => callback(pkt),
182 }
183 }
184 }
185 }
186
187 for &(node, port) in &edge.consumers {
189 let cap = if self.nodes[node].input_is_back_edge[port] {
190 Some(1) } else {
192 match &self.nodes[node].policy {
193 InputPolicy::FixedSize { capacity } => Some(*capacity),
194 _ => None,
195 }
196 };
197 let mut dropped = 0u64;
198 let mut q = self.nodes[node].input_queues[port]
199 .lock()
200 .expect("queue lock poisoned");
201 if self.shared.is_cancelled() || self.shared.has_error() {
205 continue;
206 }
207 for pkt in packets {
208 if let Some(cap) = cap {
211 while q.len() >= cap {
212 if let Some(old) = q.pop_front() {
213 self.nodes[node].input_queue_bytes[port]
214 .fetch_sub(old.byte_size(), Ordering::SeqCst);
215 self.shared.on_dequeue(old.byte_size());
216 dropped += 1;
217 } else {
218 break;
219 }
220 }
221 }
222 self.shared.on_enqueue(pkt.byte_size());
223 self.nodes[node].input_queue_bytes[port]
224 .fetch_add(pkt.byte_size(), Ordering::SeqCst);
225 q.push_back(pkt.clone());
226 }
227 let depth = q.len();
228 let queued_bytes = self.nodes[node].input_queue_bytes[port].load(Ordering::SeqCst);
229 self.nodes[node].input_queue_stats[port]
230 .peak_packets
231 .fetch_max(depth, Ordering::Relaxed);
232 self.nodes[node].input_queue_stats[port]
233 .peak_bytes
234 .fetch_max(queued_bytes, Ordering::Relaxed);
235 if self.basic_stats() {
237 self.nodes[node]
238 .stats
239 .peak_queue_depth
240 .fetch_max(depth, Ordering::Relaxed);
241 }
242 drop(q);
243 if let Some(last) = packets.last() {
245 self.nodes[node].advance_bound(port, last.timestamp().next_allowed_in_stream());
246 }
247 if dropped > 0 {
248 self.note_dropped(edge_id, dropped);
249 }
250 self.warn_if_over_soft_limit(edge_id, depth);
251 }
252 if let Some(last) = packets.last() {
253 self.publish_bound(edge_id, last.timestamp().next_allowed_in_stream());
254 }
255 }
256
257 pub(super) fn note_dropped(&self, edge_id: EdgeId, n: u64) {
259 let e = &self.edges[edge_id];
260 let before = e.dropped.fetch_add(n, Ordering::Relaxed); let after = before + n;
262 if before == 0 || after.is_power_of_two() {
263 runtime::log_warn(&format!(
264 "edge `{}` has dropped {} packets total due to the fixed_size policy (consumer can't keep up; observe with dropped_count)",
265 e.name, after
266 ));
267 }
268 }
269
270 pub(super) fn warn_if_over_soft_limit(&self, edge_id: EdgeId, depth: usize) {
272 let limit = self.shared.config.max_queue_size;
273 if limit == 0 || depth <= limit {
274 return;
275 }
276 let ratio = depth / limit;
278 if ratio.is_power_of_two() {
279 runtime::log_warn(&format!(
280 "edge `{}` has {} packets backlogged (soft limit {}); consumer may not be keeping up",
281 self.edges[edge_id].name, depth, limit
282 ));
283 }
284 }
285
286 pub(super) fn schedule_consumers(&self, edge: EdgeId) {
287 let consumers: Vec<NodeId> = self.edges[edge].consumers.iter().map(|&(n, _)| n).collect();
288 for n in consumers {
289 self.schedule_node(n);
290 }
291 }
292
293 pub(super) fn dispatch_task(&self, n: NodeId) {
300 let executor = &self.executors[self.nodes[n].executor];
301 if !executor.submit(n) {
302 self.in_flight.fetch_sub(1, Ordering::SeqCst);
305 } else if executor.is_delegating() {
306 self.request_wakeup();
307 }
308 self.notify_activity();
309 }
310
311 pub fn run_node_on_worker(&self, n: NodeId) {
313 self.run_node(n);
314 self.in_flight.fetch_sub(1, Ordering::SeqCst);
315 if self.shared.is_cancelled() || self.shared.has_error() {
316 self.resume_blocked_flushes();
317 self.finish_all_backpressure_blocks();
318 }
319 self.notify_activity();
320 }
321
322 pub fn wake_source_on_worker(&self, n: NodeId, generation: u64) {
324 let node = &self.nodes[n];
325 if node.source_wake_generation.load(Ordering::SeqCst) != generation {
326 self.notify_activity();
327 return;
328 }
329 node.source_waiting.store(false, Ordering::SeqCst);
330 node.source_wait_reason.store(0, Ordering::Relaxed);
331 node.source_wake_deadline_us.store(0, Ordering::Relaxed);
332 if matches!(self.state(), State::Running | State::Draining)
333 && !self.shared.is_cancelled()
334 && !self.shared.has_error()
335 && !node.source_done.load(Ordering::SeqCst)
336 {
337 self.schedule_node(n);
338 } else {
339 self.maybe_close(n);
340 }
341 self.notify_activity();
342 }
343
344 pub fn executor_task_completed(&self) {
345 self.notify_activity();
346 self.request_wakeup();
347 }
348
349 pub(super) fn workers_idle(&self) -> bool {
354 self.in_flight.load(Ordering::SeqCst) == 0
355 && !self.delegated_running.load(Ordering::Acquire)
356 && self
357 .executors
358 .iter()
359 .all(|executor| !executor.has_pending_work())
360 }
361
362 pub(super) fn is_idle(&self) -> bool {
364 self.workers_idle()
365 && self
366 .blocked_flush_nodes
367 .lock()
368 .expect("blocked flush lock poisoned")
369 .is_empty()
370 }
371
372 pub(super) fn notify_activity(&self) {
375 let (m, cv) = &self.activity;
376 let mut a = m.lock().unwrap_or_else(|e| e.into_inner());
377 a.gen = a.gen.wrapping_add(1);
378 let wake = a.waiters > 0;
380 drop(a);
381 if wake {
382 cv.notify_all();
383 }
384 }
385
386 pub(super) fn activity_gen(&self) -> u64 {
390 self.activity
391 .0
392 .lock()
393 .unwrap_or_else(|e| e.into_inner())
394 .gen
395 }
396
397 pub(super) fn wait_activity_since(&self, before: u64, timeout: std::time::Duration) {
399 let (m, cv) = &self.activity;
400 let mut a = m.lock().unwrap_or_else(|e| e.into_inner());
401 if a.gen != before {
402 return; }
404 a.waiters += 1;
407 let (mut guard, _res) = cv
408 .wait_timeout_while(a, timeout, |x| x.gen == before)
409 .unwrap_or_else(|e| e.into_inner());
410 guard.waiters -= 1;
411 drop(guard);
412 }
413
414 pub(super) fn try_claim(&self, n: NodeId) -> bool {
419 if self.shared.has_error()
420 || self.shared.is_cancelled()
421 || self.paused.load(Ordering::SeqCst)
422 {
423 return false;
424 }
425 let node = &self.nodes[n];
426 let mut s = node.sched.lock().expect("scheduler lock poisoned");
427 if !s.opened || s.close_started {
428 return false;
429 }
430 if s.blocked_flush.is_some() {
431 return false; }
433 if s.in_flight >= node.max_in_flight {
434 return false; }
436 let Some(ready) = node.readiness() else {
438 return false;
439 };
440 let ts = ready.ts;
441 let slot = s
442 .free_slots
443 .pop()
444 .expect("a free slot must exist when in_flight < max");
445 let seq = s.next_seq;
446 s.next_seq += 1;
447 s.in_flight += 1;
448 s.ready.push_back((slot, seq));
449 self.in_flight.fetch_add(1, Ordering::SeqCst);
450
451 let ctx = unsafe { node.ctx_slot(slot) };
453 ctx.reset();
454 if let Some(plan) = &ready.batch {
457 for &(port, count) in &plan.take {
458 let remaining = {
459 let mut q = node.input_queues[port].lock().expect("queue lock poisoned");
460 for _ in 0..count {
461 let Some(p) = q.pop_front() else { break };
462 node.input_queue_bytes[port].fetch_sub(p.byte_size(), Ordering::SeqCst);
463 self.shared.on_dequeue(p.byte_size());
464 if self.basic_stats() {
465 node.stats.packets_in.fetch_add(1, Ordering::Relaxed);
466 }
467 ctx.input_batches[port].push(p);
468 }
469 q.len() };
471 node.advance_bound(port, plan.last_ts.next_allowed_in_stream());
475 ctx.inputs_done[port] =
476 node.input_closed[port].load(Ordering::SeqCst) && remaining == 0;
477 }
478 for port in 0..node.input_queues.len() {
482 if !node.input_is_back_edge[port] {
483 continue;
484 }
485 let (popped, remaining) = {
486 let mut q = node.input_queues[port].lock().expect("queue lock poisoned");
487 let p = q.pop_front();
488 (p, q.len())
489 };
490 if let Some(p) = popped {
491 node.input_queue_bytes[port].fetch_sub(p.byte_size(), Ordering::SeqCst);
492 self.shared.on_dequeue(p.byte_size());
493 if self.basic_stats() {
494 node.stats.packets_in.fetch_add(1, Ordering::Relaxed);
495 }
496 ctx.inputs[port] = Some(p);
497 }
498 ctx.inputs_done[port] =
499 node.input_closed[port].load(Ordering::SeqCst) && remaining == 0;
500 }
501 ctx.input_ts = plan.last_ts;
502 return true;
503 }
504 for port in 0..node.input_queues.len() {
505 if node.input_is_back_edge[port] {
506 let (popped, remaining) = {
509 let mut q = node.input_queues[port].lock().expect("queue lock poisoned");
510 let p = q.pop_front();
511 (p, q.len())
512 };
513 if let Some(p) = popped {
514 node.input_queue_bytes[port].fetch_sub(p.byte_size(), Ordering::SeqCst);
515 self.shared.on_dequeue(p.byte_size());
516 if self.basic_stats() {
517 node.stats.packets_in.fetch_add(1, Ordering::Relaxed);
518 }
519 ctx.inputs[port] = Some(p);
520 }
521 ctx.inputs_done[port] =
522 node.input_closed[port].load(Ordering::SeqCst) && remaining == 0;
523 continue;
524 }
525 let participates = ready.ports.as_ref().is_none_or(|set| set.contains(&port));
528 if !participates {
529 continue;
530 }
531 let (popped, remaining) = {
540 let mut q = node.input_queues[port].lock().expect("queue lock poisoned");
541 let hit = q.front().map(|p| p.timestamp()) == Some(ts);
542 let p = if hit { q.pop_front() } else { None };
543 (p, q.len())
544 };
545 if let Some(p) = popped {
546 node.input_queue_bytes[port].fetch_sub(p.byte_size(), Ordering::SeqCst);
547 self.shared.on_dequeue(p.byte_size());
548 if self.basic_stats() {
549 node.stats.packets_in.fetch_add(1, Ordering::Relaxed);
550 }
551 ctx.inputs[port] = Some(p);
552 }
553 node.advance_bound(port, ts.next_allowed_in_stream());
554 ctx.inputs_done[port] =
557 node.input_closed[port].load(Ordering::SeqCst) && remaining == 0;
558 }
559 ctx.input_ts = if node.is_source() {
561 Timestamp(seq as i64)
562 } else {
563 ts
564 };
565 true
566 }
567
568 pub(super) fn schedule_node(&self, n: NodeId) {
571 while self.try_claim(n) {
572 self.dispatch_task(n);
573 self.resume_blocked_flushes();
575 }
576 }
577
578 pub(super) fn run_one_main_task(&self) -> bool {
583 if self
584 .delegated_running
585 .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
586 .is_err()
587 {
588 return false;
589 }
590 struct RunningGuard<'a>(&'a AtomicBool);
591 impl Drop for RunningGuard<'_> {
592 fn drop(&mut self) {
593 self.0.store(false, Ordering::Release);
594 }
595 }
596 let _running = RunningGuard(&self.delegated_running);
597
598 let len = self.executors.len();
599 let start = self.delegated_cursor.fetch_add(1, Ordering::Relaxed) % len;
600 let next = (0..len).find_map(|offset| {
601 let index = (start + offset) % len;
602 self.executors[index]
603 .take_delegated()
604 .map(|node| (index, node))
605 });
606 match next {
607 Some((index, n)) => {
608 self.delegated_cursor
609 .store(index.wrapping_add(1), Ordering::Relaxed);
610 let started = self.full_stats().then(Instant::now);
611 self.run_node(n);
612 self.in_flight.fetch_sub(1, Ordering::SeqCst);
613 self.executors[index].complete_delegated(started.map(|started| started.elapsed()));
614 self.notify_activity();
615 true
616 }
617 None => false,
618 }
619 }
620
621 pub(super) fn pump_step(&self) -> bool {
623 self.run_one_main_task() || self.try_advance_closing()
624 }
625
626 pub(super) fn delegated_tasks_pending(&self) -> bool {
627 self.executors
628 .iter()
629 .any(|executor| executor.is_delegating() && executor.pending() > 0)
630 }
631
632 pub(super) fn request_wakeup(&self) {
633 self.wakeup_generation.fetch_add(1, Ordering::AcqRel);
634 if self
635 .wakeup_pending
636 .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
637 .is_err()
638 {
639 return;
640 }
641 let callback = self
642 .wakeup_callback
643 .lock()
644 .unwrap_or_else(|error| error.into_inner());
645 let Some(callback) = callback.as_ref() else {
646 self.wakeup_pending.store(false, Ordering::Release);
647 return;
648 };
649 if catch_unwind(AssertUnwindSafe(|| callback())).is_err() {
652 self.wakeup_pending.store(false, Ordering::Release);
653 runtime::log_warn("graph wakeup callback panicked; wakeup re-armed");
654 }
655 }
656
657 pub(super) fn set_wakeup_callback(&self, callback: Option<WakeupCallback>) {
658 *self
659 .wakeup_callback
660 .lock()
661 .unwrap_or_else(|error| error.into_inner()) = callback;
662 self.wakeup_pending.store(false, Ordering::Release);
663 if self.delegated_tasks_pending()
664 || self.shared.is_cancelled()
665 || self.shared.has_error()
666 || self.state() == State::Terminated
667 {
668 self.request_wakeup();
669 }
670 }
671
672 pub(super) fn run_node(&self, n: NodeId) {
673 let node = &self.nodes[n];
674 let inv = {
676 node.sched
677 .lock()
678 .expect("scheduler lock poisoned")
679 .ready
680 .pop_front()
681 };
682 let Some((slot, seq)) = inv else {
683 self.finish(n);
685 return;
686 };
687
688 if self.shared.is_cancelled() || self.shared.has_error() {
692 self.complete_invocation(n, slot, seq, false);
693 return;
694 }
695
696 let ok = match self.check_input_types(n, slot) {
698 Err(e) => self.on_node_error(n, slot, e),
699 Ok(()) => {
700 let rc = self.call_kernel(n, slot, KernelPhase::Process);
701 let ctx = unsafe { node.ctx_slot(slot) };
702 if node.is_source() {
703 if ctx.source_yield.is_some() {
704 node.source_yield_count.fetch_add(1, Ordering::Relaxed);
705 }
706 if ctx.source_done {
707 node.source_done.store(true, Ordering::SeqCst);
708 node.source_waiting.store(false, Ordering::SeqCst);
709 node.source_wait_reason.store(0, Ordering::Relaxed);
710 node.source_wake_deadline_us.store(0, Ordering::Relaxed);
711 node.sched
712 .lock()
713 .expect("scheduler lock poisoned")
714 .source_reschedule = None;
715 } else {
716 let rate_delay = node.min_period.and_then(|period| {
717 let last = node.last_fire.lock().expect("last_fire lock poisoned");
718 last.and_then(|started| period.checked_sub(started.elapsed()))
719 });
720 let reschedule = match (ctx.source_yield, rate_delay) {
721 (Some(yield_delay), Some(rate_delay)) => Some(SourceReschedule {
722 delay: yield_delay.max(rate_delay),
723 reason: SourceWaitReason::RateAndYield,
724 }),
725 (Some(delay), None) => Some(SourceReschedule {
726 delay,
727 reason: SourceWaitReason::Yield,
728 }),
729 (None, Some(delay)) => Some(SourceReschedule {
730 delay,
731 reason: SourceWaitReason::Rate,
732 }),
733 (None, None) => None,
734 };
735 if let Some(reschedule) = reschedule {
736 node.source_waiting.store(true, Ordering::SeqCst);
737 node.source_wait_reason.store(
738 match reschedule.reason {
739 SourceWaitReason::Rate => 1,
740 SourceWaitReason::Yield => 2,
741 SourceWaitReason::RateAndYield => 3,
742 },
743 Ordering::Relaxed,
744 );
745 node.sched
746 .lock()
747 .expect("scheduler lock poisoned")
748 .source_reschedule = Some(reschedule);
749 }
750 }
751 } else if ctx.source_yield.is_some() && rc == 0 {
752 return self.complete_invocation(
753 n,
754 slot,
755 seq,
756 self.on_node_error(
757 n,
758 slot,
759 Error::Kernel(format!(
760 "[{}] source_yield is only valid for source nodes",
761 node.name
762 )),
763 ),
764 );
765 }
766 if rc != 0 {
767 let e = unsafe { node.ctx_slot(slot) }.take_error(rc);
768 self.on_node_error(n, slot, e)
769 } else if let Err(e) = self.check_output_types(n, slot) {
770 self.on_node_error(n, slot, e)
771 } else {
772 if self.basic_stats() {
773 node.stats.processed.fetch_add(1, Ordering::Relaxed);
774 }
775 true
776 }
777 }
778 };
779 self.complete_invocation(n, slot, seq, ok);
780 }
781
782 pub(super) fn on_node_error(&self, n: NodeId, slot: usize, e: Error) -> bool {
792 let node = &self.nodes[n];
793 let before = node.stats.errors.fetch_add(1, Ordering::Relaxed);
794 match node.on_error {
795 OnError::Abort => {
796 self.shared.record_error(e);
797 false }
799 OnError::Skip => {
800 let after = before + 1;
803 if before == 0 || after.is_power_of_two() {
804 runtime::log_warn(&format!(
805 "node `{}`: skipping a failed packet (on_error=skip), {} so far: {}",
806 node.name, after, e
807 ));
808 }
809 unsafe { node.ctx_slot(slot) }.discard_staging();
811 true
812 }
813 }
814 }
815
816 pub(super) fn complete_invocation(&self, n: NodeId, slot: usize, seq: u64, ok: bool) {
817 let node = &self.nodes[n];
818 if self.full_stats() {
819 self.propagate_output_ingress(n, slot);
820 }
821 unsafe { node.ctx_slot(slot) }.clear_inputs();
825 let mut first: Option<(usize, bool)> = None;
832 let be_flusher = {
833 let mut s = node.sched.lock().expect("scheduler lock poisoned");
834 if !s.flushing
835 && s.blocked_flush.is_none()
836 && s.pending_flush.is_empty()
837 && seq == s.next_flush_seq
838 {
839 s.flushing = true;
840 first = Some((slot, ok));
841 true
842 } else {
843 s.pending_flush.insert(seq, (slot, ok));
844 if s.flushing {
845 false
846 } else {
847 s.flushing = true;
848 true
849 }
850 }
851 };
852 if be_flusher {
853 self.drive_invocation_flushes(n, first);
854 }
855 self.finish(n);
856 }
857
858 pub(super) fn propagate_output_ingress(&self, n: NodeId, slot: usize) {
859 let node = &self.nodes[n];
860 let ctx = unsafe { node.ctx_slot(slot) };
861 let mut ingress = ctx
862 .inputs
863 .iter()
864 .filter_map(|pkt| pkt.as_ref().map(Packet::ingress_us))
865 .filter(|value| *value != 0)
866 .min()
867 .unwrap_or_else(|| self.epoch_us());
868 for batch in &ctx.input_batches {
869 for pkt in batch {
870 let value = pkt.ingress_us();
871 if value != 0 {
872 ingress = ingress.min(value);
873 }
874 }
875 }
876 for packets in &ctx.staging {
877 for pkt in packets {
878 pkt.mark_ingress_us(ingress);
879 }
880 }
881 }
882
883 pub(super) fn schedule_source_resumption(&self, n: NodeId) {
885 let node = &self.nodes[n];
886 if !node.is_source() || !node.source_waiting.load(Ordering::SeqCst) {
887 return;
888 }
889 if self.shared.is_cancelled()
890 || self.shared.has_error()
891 || !matches!(self.state(), State::Running | State::Draining)
892 {
893 node.source_waiting.store(false, Ordering::SeqCst);
894 node.source_wait_reason.store(0, Ordering::Relaxed);
895 node.source_wake_deadline_us.store(0, Ordering::Relaxed);
896 node.sched
897 .lock()
898 .expect("scheduler lock poisoned")
899 .source_reschedule = None;
900 return;
901 }
902 let reschedule = {
903 let mut sched = node.sched.lock().expect("scheduler lock poisoned");
904 if sched.in_flight != 0 || sched.blocked_flush.is_some() || sched.flushing {
905 return;
906 }
907 sched.source_reschedule.take()
908 };
909 let Some(reschedule) = reschedule else {
910 return;
911 };
912 let generation = node.source_wake_generation.fetch_add(1, Ordering::SeqCst) + 1;
913 node.source_wake_deadline_us.store(
914 self.epoch_us()
915 .saturating_add(reschedule.delay.as_micros().min(i64::MAX as u128) as i64)
916 .saturating_add(1),
917 Ordering::Relaxed,
918 );
919 if !self.executors[node.executor].submit_source_wake(n, generation, reschedule.delay) {
920 node.source_waiting.store(false, Ordering::SeqCst);
921 node.source_wait_reason.store(0, Ordering::Relaxed);
922 node.source_wake_deadline_us.store(0, Ordering::Relaxed);
923 }
924 self.notify_activity();
925 }
926
927 pub(super) fn check_input_types(&self, n: NodeId, slot: usize) -> Result<()> {
929 let node = &self.nodes[n];
930 let ctx = unsafe { node.ctx_slot(slot) };
931 for (port, &want) in node.input_types.iter().enumerate() {
932 let Some(pkt) = ctx.inputs.get(port).and_then(|s| s.as_ref()) else {
933 continue;
934 };
935 if pkt.is_empty() {
936 continue; }
938 let got = pkt.type_id();
939
940 crate::packet::validate_type_id(got).map_err(|error| {
941 Error::Kernel(format!(
942 "[{}] input port `{}` carries an invalid type: {error}",
943 node.name,
944 node.in_ports.name(port).unwrap_or("?"),
945 ))
946 })?;
947
948 if want == 0 {
949 continue; }
951 if got != want {
952 let hint = if got == crate::packet::type_id::NONE {
957 match pkt.payload() {
958 Some(crate::packet::Payload::Foreign(_)) => {
959 " (the packet carries no declared type: its type_id is \
960 LMFLOW_TYPE_NONE, which means \"skip type checking\"; set a real \
961 LMFLOW_TYPE_* on the packet you submit, or declare this port as \
962 any-type)"
963 }
964 _ => {
966 " (the packet carries no declared type because its payload is \
967 Rust-native, e.g. built with `Packet::new`; use \
968 `Packet::from_i64` / `from_f64` / `from_builtin` for built-in \
969 payloads, implement `InteropType` and use `Packet::from_interop` \
970 for a custom type, or use unsafe `Packet::new_interop` only after \
971 manually proving the ABI layout)"
972 }
973 }
974 } else {
975 ""
976 };
977 return Err(Error::Kernel(format!(
978 "[{}] input port `{}` type mismatch: contract declares {}, actual {}{}",
979 node.name,
980 node.in_ports.name(port).unwrap_or("?"),
981 crate::packet::type_name(want),
982 crate::packet::type_name(got),
983 hint,
984 )));
985 }
986 }
987 Ok(())
988 }
989
990 pub(super) fn check_output_types(&self, n: NodeId, slot: usize) -> Result<()> {
994 let node = &self.nodes[n];
995 let ctx = unsafe { node.ctx_slot(slot) };
996 for (port, packets) in ctx.staging.iter().enumerate() {
997 let want = node.output_types[port];
998 for pkt in packets {
999 if pkt.is_empty() {
1000 continue;
1001 }
1002 let got = pkt.type_id();
1003 crate::packet::validate_type_id(got).map_err(|error| {
1004 Error::Kernel(format!(
1005 "[{}] output port `{}` carries an invalid type: {error}",
1006 node.name,
1007 node.out_ports.name(port).unwrap_or("?"),
1008 ))
1009 })?;
1010 if want != crate::packet::type_id::NONE && got != want {
1011 return Err(Error::Kernel(format!(
1012 "[{}] output port `{}` type mismatch: contract declares {}, actual {}",
1013 node.name,
1014 node.out_ports.name(port).unwrap_or("?"),
1015 crate::packet::type_name(want),
1016 crate::packet::type_name(got),
1017 )));
1018 }
1019 }
1020 }
1021 Ok(())
1022 }
1023
1024 pub(super) fn propagate_bound(&self, edge: EdgeId, bound: Timestamp) {
1026 let consumers: Vec<(NodeId, usize)> = self.edges[edge].consumers.clone();
1027 for (node, port) in consumers {
1028 self.nodes[node].advance_bound(port, bound);
1029 self.schedule_node(node);
1030 }
1031 self.publish_bound(edge, bound);
1032 }
1033
1034 pub(super) fn publish_bound(&self, edge_id: EdgeId, bound: Timestamp) {
1037 let edge = &self.edges[edge_id];
1038 if !edge.has_timestamp_bound_subscriber.load(Ordering::Relaxed) {
1039 return;
1040 }
1041 let should_publish = {
1042 let mut last = edge
1043 .last_published_bound
1044 .lock()
1045 .expect("published-bound lock poisoned");
1046 if bound <= *last {
1047 false
1048 } else {
1049 *last = bound;
1050 true
1051 }
1052 };
1053 if !should_publish {
1054 return;
1055 }
1056
1057 let event = Packet::empty().at(bound);
1058 let pollers = edge
1059 .pollers
1060 .lock()
1061 .expect("poller list lock poisoned")
1062 .clone();
1063 let mut any = false;
1064 for poller in &pollers {
1065 if poller.observe_timestamp_bounds {
1066 any |= poller.push(self, event.clone());
1067 }
1068 }
1069 if any {
1070 self.notify_activity();
1071 }
1072
1073 let observers = edge
1074 .observers
1075 .lock()
1076 .expect("observer list lock poisoned")
1077 .clone();
1078 for observer in &observers {
1079 match observer {
1080 Observer::C {
1081 cb,
1082 user,
1083 observe_timestamp_bounds: true,
1084 } => {
1085 let ffi = crate::ffi::borrow_packet(&event);
1086 unsafe { cb(*user, ffi) };
1087 }
1088 Observer::Rust {
1089 callback,
1090 observe_timestamp_bounds: true,
1091 } => callback(&event),
1092 Observer::C { .. } | Observer::Rust { .. } => {}
1093 }
1094 }
1095 }
1096
1097 pub(super) fn call_kernel(&self, n: NodeId, slot: usize, phase: KernelPhase) -> i32 {
1100 let node = &self.nodes[n];
1101 if self.full_stats() {
1102 crate::packet::begin_cow_copy_scope();
1103 }
1104 let ctx_cell = node.ctxs[slot].get();
1107 let ctx_ptr = ctx_cell as *mut c_void;
1108 let started = if self.full_stats() {
1115 Some(Instant::now())
1116 } else {
1117 None
1118 };
1119 if node.stats.in_flight.fetch_add(1, Ordering::Relaxed) == 0 {
1120 if let Some(t) = started {
1121 let since_epoch = t.saturating_duration_since(self.epoch).as_micros() as i64;
1122 node.stats.started_us.store(since_epoch, Ordering::Relaxed);
1123 }
1124 }
1125
1126 if matches!(phase, KernelPhase::Process) && node.min_period.is_some() {
1128 *node.last_fire.lock().expect("last_fire lock poisoned") = Some(Instant::now());
1129 }
1130
1131 let _buffer_pool = crate::buffer_pool::enter(self.shared.buffer_pool.clone());
1133 let rc = unsafe {
1134 match phase {
1135 KernelPhase::Open => node.kernel.open(ctx_ptr),
1136 KernelPhase::Process => node.kernel.process(ctx_ptr),
1137 KernelPhase::Close => node.kernel.close(ctx_ptr),
1138 }
1139 };
1140 if self.full_stats() {
1141 let cow = crate::packet::end_cow_copy_scope();
1142 if matches!(phase, KernelPhase::Process) {
1143 node.stats
1144 .cow_copies
1145 .fetch_add(cow.copies, Ordering::Relaxed);
1146 node.stats.cow_bytes.fetch_add(cow.bytes, Ordering::Relaxed);
1147 }
1148 }
1149
1150 node.stats.in_flight.fetch_sub(1, Ordering::Relaxed);
1153 let Some(t0) = started else { return rc }; let us = t0.elapsed().as_micros() as i64;
1155 if self.shared.trace_enabled() {
1158 let start_us = t0.saturating_duration_since(self.epoch).as_micros() as i64;
1159 let input_ts = unsafe { (*ctx_cell).input_ts.0 };
1162 let tphase = match phase {
1163 KernelPhase::Open => crate::trace::TracePhase::Open,
1164 KernelPhase::Process => crate::trace::TracePhase::Process,
1165 KernelPhase::Close => crate::trace::TracePhase::Close,
1166 };
1167 self.shared
1168 .record_trace(n as u32, tphase, start_us, us, input_ts);
1169 }
1170 if matches!(phase, KernelPhase::Process) {
1171 node.stats.total_us.fetch_add(us, Ordering::Relaxed);
1172 node.stats.max_us.fetch_max(us, Ordering::Relaxed);
1173 node.stats.record_latency(us.max(0) as u64);
1174 }
1175 let wd = self.shared.config.watchdog_ms;
1176 if wd > 0 && us as u64 > wd * 1000 {
1177 runtime::log_warn(&format!(
1178 "node `{}`: one {:?} took {} ms, exceeding watchdog {} ms",
1179 node.name,
1180 phase,
1181 us / 1000,
1182 wd
1183 ));
1184 }
1185 rc
1186 }
1187}
1188
1189#[derive(Debug, Clone, Copy)]
1190pub(super) enum KernelPhase {
1191 Open,
1192 Process,
1193 Close,
1194}