Skip to main content

reifydb_sub_flow/engine/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4//! Flow execution engine. Registers compiled flow definitions, evaluates each flow's operator graph against
5//! incoming change deltas, and writes the resulting outputs back through the catalog. Process drives the per-tick
6//! work; eval is where individual operators run; register is the wiring step that turns a flow definition into
7//! an executable graph.
8
9pub mod cache;
10pub mod eval;
11pub mod register;
12
13use std::{
14	collections::{BTreeMap, BTreeSet},
15	sync::Arc,
16};
17
18use dashmap::DashMap;
19use reifydb_catalog::catalog::Catalog;
20#[cfg(reifydb_target = "native")]
21use reifydb_codec::value::encode_params;
22use reifydb_core::{
23	common::CommitVersion,
24	event::EventBus,
25	interface::catalog::{
26		flow::{FlowId, FlowNodeId},
27		id::{TableId, ViewId},
28		shape::ShapeId,
29	},
30};
31use reifydb_engine::vm::executor::Executor;
32#[cfg(reifydb_target = "native")]
33use reifydb_extension::operator::ffi_loader::ffi_operator_loader;
34use reifydb_rql::flow::{
35	analyzer::{FlowDependencyGraph, FlowGraphAnalyzer, FlowSchedule},
36	flow::FlowDag,
37};
38use reifydb_runtime::{
39	context::{RuntimeContext, clock::Clock},
40	sync::rwlock::{RwLock, RwLockReadGuard, RwLockWriteGuard},
41};
42#[cfg(reifydb_target = "native")]
43use reifydb_sdk::config::Config;
44use reifydb_value::value::duration::Duration;
45#[cfg(reifydb_target = "native")]
46use reifydb_value::{Result, error::Error, params::Params, value::Value};
47use tracing::instrument;
48
49#[cfg(reifydb_target = "native")]
50use crate::error::{FlowStateError, NativeOperatorError};
51#[cfg(reifydb_target = "native")]
52use crate::operator::BoxedOperator;
53#[cfg(reifydb_target = "native")]
54use crate::operator::ffi::FFIOperator;
55#[cfg(reifydb_target = "native")]
56use crate::operator::native::native_operator_loader;
57use crate::{
58	builder::CustomOperators,
59	engine::cache::{ExecutionLevelCache, ScheduleCache},
60	operator::OperatorCell,
61	transaction::allocators::FlowAllocators,
62};
63
64pub struct FlowEngineInner {
65	pub(crate) catalog: Catalog,
66	pub(crate) executor: Executor,
67	pub(crate) operators: BTreeMap<FlowNodeId, OperatorCell>,
68	pub(crate) flows: BTreeMap<FlowId, FlowDag>,
69	pub(crate) sources: BTreeMap<ShapeId, Vec<(FlowId, FlowNodeId)>>,
70	pub(crate) sinks: BTreeMap<ShapeId, Vec<(FlowId, FlowNodeId)>>,
71	pub(crate) analyzer: FlowGraphAnalyzer,
72	pub(crate) execution_level_cache: ExecutionLevelCache,
73	pub(crate) schedule_cache: ScheduleCache,
74	#[allow(dead_code)]
75	pub(crate) event_bus: EventBus,
76	pub(crate) flow_creation_versions: BTreeMap<FlowId, CommitVersion>,
77	pub(crate) runtime_context: RuntimeContext,
78	pub(crate) custom_operators: CustomOperators,
79	operator_tick_times: DashMap<FlowNodeId, u64>,
80	pub(crate) allocators: FlowAllocators,
81}
82
83#[derive(Clone)]
84pub struct FlowEngine {
85	inner: Arc<RwLock<FlowEngineInner>>,
86}
87
88impl FlowEngine {
89	pub fn new(
90		catalog: Catalog,
91		executor: Executor,
92		event_bus: EventBus,
93		runtime_context: RuntimeContext,
94		custom_operators: CustomOperators,
95		allocators: FlowAllocators,
96	) -> Self {
97		Self {
98			inner: Arc::new(RwLock::new(FlowEngineInner::new(
99				catalog,
100				executor,
101				event_bus,
102				runtime_context,
103				custom_operators,
104				allocators,
105			))),
106		}
107	}
108
109	pub fn read(&self) -> RwLockReadGuard<'_, FlowEngineInner> {
110		self.inner.read()
111	}
112
113	pub fn read_recursive(&self) -> RwLockReadGuard<'_, FlowEngineInner> {
114		self.inner.read_recursive()
115	}
116
117	pub fn write(&self) -> RwLockWriteGuard<'_, FlowEngineInner> {
118		self.inner.write()
119	}
120}
121
122impl FlowEngineInner {
123	#[instrument(
124		name = "flow::engine::new",
125		level = "debug",
126		skip(catalog, executor, event_bus, runtime_context, custom_operators, allocators)
127	)]
128	pub fn new(
129		catalog: Catalog,
130		executor: Executor,
131		event_bus: EventBus,
132		runtime_context: RuntimeContext,
133		custom_operators: CustomOperators,
134		allocators: FlowAllocators,
135	) -> Self {
136		Self {
137			catalog,
138			executor,
139			operators: BTreeMap::new(),
140			flows: BTreeMap::new(),
141			sources: BTreeMap::new(),
142			sinks: BTreeMap::new(),
143			analyzer: FlowGraphAnalyzer::new(),
144			execution_level_cache: ExecutionLevelCache::new(),
145			schedule_cache: ScheduleCache::new(),
146			event_bus,
147			flow_creation_versions: BTreeMap::new(),
148			runtime_context,
149			custom_operators,
150			operator_tick_times: DashMap::new(),
151			allocators,
152		}
153	}
154
155	pub fn clock(&self) -> &Clock {
156		&self.runtime_context.clock
157	}
158
159	pub fn operator(&self, node_id: FlowNodeId) -> Option<OperatorCell> {
160		self.operators.get(&node_id).cloned()
161	}
162
163	pub fn insert_operator(&mut self, node_id: FlowNodeId, operator: OperatorCell) {
164		self.operators.insert(node_id, operator);
165	}
166
167	pub fn register_flow_dag(&mut self, flow: FlowDag) {
168		self.analyzer.add(flow.clone());
169		self.flows.insert(flow.id, flow);
170	}
171
172	pub fn flow_by_id(&self, flow_id: FlowId) -> Option<FlowDag> {
173		self.flows.get(&flow_id).cloned()
174	}
175
176	pub fn has_sources(&self) -> bool {
177		!self.sources.is_empty()
178	}
179
180	pub fn flows_for_source_shape(&self, shape: ShapeId) -> Option<Vec<(FlowId, FlowNodeId)>> {
181		self.sources.get(&shape).cloned()
182	}
183
184	pub(crate) fn operator_due(&self, node_id: FlowNodeId, now_nanos: u64, interval: Duration) -> bool {
185		let interval_nanos = interval.to_std().as_nanos() as u64;
186		let due = match self.operator_tick_times.get(&node_id) {
187			Some(last) => now_nanos.saturating_sub(*last) >= interval_nanos,
188			None => true,
189		};
190		if due {
191			self.operator_tick_times.insert(node_id, now_nanos);
192		}
193		due
194	}
195
196	#[cfg(reifydb_target = "native")]
197	#[instrument(name = "flow::engine::create_ffi_operator", level = "debug", skip(self, config), fields(operator = %operator, node_id = ?node_id))]
198	pub(crate) fn create_ffi_operator(
199		&self,
200		operator: &str,
201		node_id: FlowNodeId,
202		config: &BTreeMap<String, Value>,
203	) -> Result<BoxedOperator> {
204		let loader = ffi_operator_loader();
205		let mut loader_write = loader.write();
206
207		let config_params =
208			Params::Named(Arc::new(config.iter().map(|(k, v)| (k.clone(), v.clone())).collect()));
209		let config_bytes = encode_params(&config_params).map_err(|e| {
210			Error::from(FlowStateError::Encode {
211				state: "operator config",
212				cause: e.to_string(),
213			})
214		})?;
215
216		let (descriptor, instance) =
217			loader_write.create_operator_by_name(operator, node_id, &config_bytes).map_err(|e| {
218				Error::from(NativeOperatorError::CreateFailed {
219					cause: format!("{:?}", e),
220				})
221			})?;
222
223		Ok(Box::new(FFIOperator::new(descriptor, instance, node_id, self.executor.clone())))
224	}
225
226	#[cfg(reifydb_target = "native")]
227	pub(crate) fn is_ffi_operator(&self, operator: &str) -> bool {
228		let loader = ffi_operator_loader();
229		let loader_read = loader.read();
230		loader_read.has_operator(operator)
231	}
232
233	#[cfg(reifydb_target = "native")]
234	#[instrument(name = "flow::engine::create_native_operator", level = "debug", skip(self, config), fields(operator = %operator, node_id = ?node_id))]
235	pub(crate) fn create_native_operator(
236		&self,
237		operator: &str,
238		node_id: FlowNodeId,
239		config: &Config,
240	) -> Result<BoxedOperator> {
241		let loader = native_operator_loader();
242		let mut loader_write = loader.write();
243		loader_write.create_operator_by_name(operator, node_id, config)
244	}
245
246	#[cfg(reifydb_target = "native")]
247	pub(crate) fn is_native_operator(&self, operator: &str) -> bool {
248		native_operator_loader().read().has_operator(operator)
249	}
250
251	#[cfg(not(reifydb_target = "native"))]
252	#[allow(dead_code)]
253	pub(crate) fn is_ffi_operator(&self, _operator: &str) -> bool {
254		false
255	}
256
257	pub fn flow_ids(&self) -> BTreeSet<FlowId> {
258		self.flows.keys().copied().collect()
259	}
260
261	pub fn clear(&mut self) {
262		self.operators.clear();
263		self.flows.clear();
264		self.sources.clear();
265		self.sinks.clear();
266		self.analyzer.clear();
267		self.flow_creation_versions.clear();
268		self.execution_level_cache.invalidate();
269		self.schedule_cache.invalidate();
270	}
271
272	pub fn remove_flow(&mut self, flow_id: FlowId) {
273		let node_ids: Vec<FlowNodeId> =
274			self.flows.get(&flow_id).map(|flow| flow.get_node_ids().collect()).unwrap_or_default();
275
276		for node_id in node_ids {
277			self.operators.remove(&node_id);
278			self.allocators.row.evict(node_id);
279		}
280
281		for entries in self.sources.values_mut() {
282			entries.retain(|(fid, _)| *fid != flow_id);
283		}
284		self.sources.retain(|_, v| !v.is_empty());
285
286		for entries in self.sinks.values_mut() {
287			entries.retain(|(fid, _)| *fid != flow_id);
288		}
289		self.sinks.retain(|_, v| !v.is_empty());
290
291		self.flows.remove(&flow_id);
292
293		self.analyzer.remove(flow_id);
294		self.execution_level_cache.invalidate();
295		self.schedule_cache.invalidate();
296	}
297
298	pub fn get_dependency_graph(&self) -> FlowDependencyGraph {
299		self.analyzer.get_dependency_graph().clone()
300	}
301
302	pub fn get_flows_depending_on_table(&self, table_id: TableId) -> Vec<FlowId> {
303		let dependency_graph = self.analyzer.get_dependency_graph();
304		self.analyzer.get_flows_depending_on_table(dependency_graph, table_id)
305	}
306
307	pub fn get_flows_depending_on_view(&self, view_id: ViewId) -> Vec<FlowId> {
308		let dependency_graph = self.analyzer.get_dependency_graph();
309		self.analyzer.get_flows_depending_on_view(dependency_graph, view_id)
310	}
311
312	pub fn get_flow_producing_view(&self, view_id: ViewId) -> Option<FlowId> {
313		let dependency_graph = self.analyzer.get_dependency_graph();
314		self.analyzer.get_flow_producing_view(dependency_graph, view_id)
315	}
316
317	pub fn calculate_execution_levels(&self) -> Vec<Vec<FlowId>> {
318		if let Some(levels) = self.execution_level_cache.get() {
319			return levels;
320		}
321
322		let dependency_graph = self.analyzer.get_dependency_graph();
323		let levels = self.analyzer.calculate_execution_levels(dependency_graph);
324		self.execution_level_cache.set(levels.clone());
325		levels
326	}
327
328	pub fn calculate_schedule(&self) -> FlowSchedule {
329		if let Some(schedule) = self.schedule_cache.get() {
330			return schedule;
331		}
332
333		let dependency_graph = self.analyzer.get_dependency_graph();
334		let schedule = self.analyzer.calculate_schedule(dependency_graph);
335		self.schedule_cache.set(schedule.clone());
336		schedule
337	}
338}