use std::cell::UnsafeCell;
use std::collections::{BTreeMap, BTreeSet, VecDeque};
use std::ffi::c_void;
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU64, AtomicUsize, Ordering};
use std::sync::{Arc, Condvar, Mutex};
use std::time::Instant;
use crate::config::{GraphConfig, StatsLevel};
use crate::context::Context;
use crate::executor::Executor;
use crate::kernel::{KernelInstance, PortTable};
use crate::packet::Packet;
use crate::runtime::{self, GraphShared};
use crate::status::{Error, Result};
use crate::timestamp::Timestamp;
mod api;
mod backpressure;
mod build;
mod dot;
mod introspect;
mod lifecycle;
mod node;
mod poller;
mod scheduler;
use backpressure::*;
use node::*;
pub use node::{DotView, State};
use poller::PollerInner;
pub use poller::{
OutputEvent, Poller, PollerBackpressureStatsSnapshot, PollerOptions, PollerOverflow,
};
pub type NodeId = usize;
pub type EdgeId = usize;
#[derive(Debug, Default)]
struct Activity {
gen: u64,
waiters: usize,
}
type WakeupCallback = Arc<dyn Fn() + Send + Sync>;
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>,
executors: Vec<Executor>,
in_flight: AtomicUsize,
delegated_cursor: AtomicUsize,
delegated_running: AtomicBool,
activity: (Mutex<Activity>, Condvar),
wakeup_callback: Mutex<Option<WakeupCallback>>,
wakeup_pending: AtomicBool,
wakeup_generation: AtomicU64,
paused: AtomicBool,
blocked_flush_nodes: Mutex<BTreeSet<NodeId>>,
side_packets: Mutex<BTreeMap<String, Packet>>,
required_side_packets: Vec<(String, String)>,
epoch: Instant,
run_started_us: AtomicI64,
dot_intervals: Mutex<dot::DotIntervalBaselines>,
stats_level: StatsLevel,
e2e_stats: E2eStats,
}
impl GraphInner {
#[inline]
fn basic_stats(&self) -> bool {
self.stats_level != StatsLevel::Off
}
#[inline]
fn full_stats(&self) -> bool {
self.stats_level == StatsLevel::Full
}
#[inline]
pub(super) fn record_e2e(&self, pkt: &Packet) {
if !self.full_stats() || !pkt.mark_e2e_observed() {
return;
}
let ingress = pkt.ingress_us();
if ingress == 0 {
return;
}
self.e2e_stats
.record(self.epoch_us().saturating_sub(ingress).max(0) as u64);
}
}
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> {
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(),
));
}
crate::packet::validate_type_id(pkt.type_id()).map_err(|error| {
Error::InvalidArg(format!(
"side packet `{name}` carries an invalid type: {error}"
))
})?;
self.inner
.side_packets
.lock()
.expect("side packet lock poisoned")
.insert(name.to_string(), pkt);
Ok(())
}
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();
for node in &self.inner.nodes {
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.source_wake_generation.fetch_add(1, Ordering::SeqCst);
}
for executor in &self.inner.executors {
executor.clear_delayed();
}
self.inner.resume_blocked_flushes();
self.inner.finish_all_backpressure_blocks();
self.inner.notify_activity();
self.inner.request_wakeup();
}
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_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 pump_step(&self) -> bool {
self.inner.pump_step_pub()
}
pub fn set_wakeup_callback<F>(&self, callback: F)
where
F: Fn() + Send + Sync + 'static,
{
self.inner.set_wakeup_callback(Some(Arc::new(callback)));
}
pub fn clear_wakeup_callback(&self) {
self.inner.set_wakeup_callback(None);
}
pub fn observe<F>(&self, port: &str, f: F) -> Result<()>
where
F: Fn(&Packet) + Send + Sync + 'static,
{
self.observe_inner(port, f, false)
}
pub fn observe_with_timestamp_bounds<F>(&self, port: &str, f: F) -> Result<()>
where
F: Fn(&Packet) + Send + Sync + 'static,
{
self.observe_inner(port, f, true)
}
fn observe_inner<F>(&self, port: &str, f: F, observe_timestamp_bounds: bool) -> 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), observe_timestamp_bounds)
}
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.to_dot_with_view(DotView::Topology)
}
pub fn to_dot_with_view(&self, view: DotView) -> String {
self.inner.to_dot(view)
}
pub fn to_dot_compact(&self) -> String {
self.to_dot_with_view(DotView::Compact)
}
pub fn to_dot_with_stats(&self) -> String {
self.to_dot_with_view(DotView::Diagnostics)
}
pub fn node_stats(&self, i: usize) -> Option<NodeStatsSnapshot> {
self.inner.node_stats(i)
}
pub fn input_queue_stats(&self, node: usize, port: usize) -> Option<InputQueueStatsSnapshot> {
self.inner.input_queue_stats(node, port)
}
}
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 backpressure_stats(&self) -> WatermarkBackpressureStatsSnapshot {
self.graph.watermark_backpressure_stats(self.edge)
}
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)
}
}
#[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,
}
#[derive(Debug, Clone)]
pub(super) struct ExecutorStatsSnapshot {
pub queued: usize,
pub running: usize,
pub peak_queued: usize,
pub completed: u64,
pub total_wait_us: u64,
pub total_execution_us: u64,
pub queued_for_us: u64,
pub saturated: bool,
pub queued_nodes: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct InputQueueStatsSnapshot {
pub node_name: String,
pub port_name: String,
pub producer_name: Option<String>,
pub packet_capacity: Option<usize>,
pub queued_packets: usize,
pub queued_bytes: u64,
pub reserved_packets: usize,
pub peak_queued_packets: usize,
pub peak_queued_bytes: u64,
pub blocked: bool,
pub blocked_for_us: u64,
pub block_events: u64,
pub total_blocked_us: u64,
}
#[derive(Debug, Clone)]
pub struct WatermarkBackpressureStatsSnapshot {
pub port_name: String,
pub packet_limit: usize,
pub total_queued_packets: usize,
pub blocked: bool,
pub active_waiters: usize,
pub blocked_for_us: u64,
pub block_events: u64,
pub total_blocked_us: u64,
}