Skip to main content

reifydb_sub_flow/subsystem/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4pub mod factory;
5#[cfg(reifydb_target = "native")]
6pub mod ffi;
7
8use std::{
9	any::Any,
10	sync::{
11		Arc,
12		atomic::{AtomicBool, Ordering},
13	},
14};
15
16#[cfg(reifydb_target = "native")]
17use ffi::{load_ffi_operators, load_native_operators};
18use reifydb_cdc::{
19	consume::{
20		consumer::{CdcConsume, CdcConsumer},
21		poll::{PollConsumer, PollConsumerConfig},
22		wake::CdcWakeRegistry,
23		watermark::FlowConsumerWatermark,
24	},
25	storage::CdcStore,
26};
27use reifydb_core::{
28	actors::flow::{FlowCoordinatorHandle, FlowCoordinatorMessage, FlowHandle, FlowMessage, FlowPoolHandle},
29	interface::{
30		WithEventBus,
31		catalog::{
32			config::{ConfigKey, GetConfig},
33			flow::FlowId,
34		},
35		cdc::{Cdc, CdcConsumerId},
36		flow::FlowWatermarkSampler,
37		version::{ComponentType, HasVersion, SystemVersion},
38	},
39	util::ioc::IocContainer,
40};
41use reifydb_engine::engine::StandardEngine;
42use reifydb_rql::flow::loader::load_flow_dag;
43use reifydb_runtime::{
44	actor::{
45		mailbox::ActorRef,
46		system::{ActorHandle, ActorSpawner},
47	},
48	context::{RuntimeContext, clock::Clock},
49	shutdown::Shutdown,
50	sync::mutex::Mutex,
51};
52use reifydb_sub_api::subsystem::{HealthStatus, Subsystem};
53use reifydb_transaction::{
54	interceptor::interceptors::Interceptors,
55	transaction::{TestTransaction, Transaction},
56};
57use reifydb_value::{
58	Result,
59	value::{duration::Duration, identity::IdentityId},
60};
61use tracing::{info, warn};
62
63use crate::{
64	builder::{CustomOperators, FlowConfig},
65	catalog::FlowCatalog,
66	deferred::{
67		coordinator::{CoordinatorActor, FlowConsumeRef, registration::extract_new_flow_ids},
68		pool::PoolActor,
69		tracker::{FlowPositionTracker, ShapeVersionTracker},
70		watermark::compute_flow_watermarks,
71		worker::FlowWorkerActor,
72	},
73	engine::{FlowEngine, FlowEngineInner},
74	transaction::allocators::FlowAllocators,
75	transactional::{
76		interceptor::{TransactionalFlowPostCommitInterceptor, TransactionalFlowPreCommitInterceptor},
77		registry::TransactionalFlowRegistry,
78		tick::{TransactionalTickActor, TransactionalTickMessage},
79	},
80};
81
82struct FlowConsumeDispatcher {
83	coordinator: FlowConsumeRef,
84	registrar: TransactionalFlowRegistry,
85	flow_catalog: FlowCatalog,
86	engine: StandardEngine,
87}
88
89impl CdcConsume for FlowConsumeDispatcher {
90	fn consume(&self, cdcs: Vec<Cdc>, reply: Box<dyn FnOnce(Result<()>) + Send>) {
91		let new_flow_ids = extract_new_flow_ids(&cdcs);
92		if !new_flow_ids.is_empty()
93			&& let Ok(mut query) = self.engine.begin_query(IdentityId::system())
94		{
95			for flow_id in new_flow_ids {
96				match self.flow_catalog.get_or_load_flow(&mut Transaction::Query(&mut query), flow_id) {
97					Ok((flow, true)) => match self.registrar.try_register(flow, &mut query) {
98						Ok(true) => {}
99						Ok(false) => {
100							self.flow_catalog.remove(flow_id);
101						}
102						Err(e) => {
103							self.flow_catalog.remove(flow_id);
104							warn!(
105								flow_id = flow_id.0,
106								error = %e,
107								"failed to register transactional flow"
108							);
109						}
110					},
111					Ok((_, false)) => {}
112					Err(e) => {
113						warn!(
114							flow_id = flow_id.0,
115							error = %e,
116							"failed to load flow for transactional check"
117						);
118					}
119				}
120			}
121		}
122
123		self.coordinator.consume(cdcs, reply);
124	}
125}
126
127pub struct FlowSubsystem {
128	consumer: Mutex<PollConsumer<StandardEngine, FlowConsumeDispatcher>>,
129	flow_scope: ActorSpawner,
130	worker_handles: Mutex<Vec<FlowHandle>>,
131	pool_handle: Mutex<Option<FlowPoolHandle>>,
132	coordinator_handle: Mutex<Option<FlowCoordinatorHandle>>,
133	transactional_tick_handle: Mutex<Option<ActorHandle<TransactionalTickMessage>>>,
134	transactional_flow_engine: FlowEngine,
135	running: AtomicBool,
136}
137
138impl FlowSubsystem {
139	pub fn new(config: FlowConfig, engine: StandardEngine, ioc: &IocContainer) -> Result<Self> {
140		Self::maybe_load_ffi_operators(&config, &engine);
141
142		let clock = ioc.resolve::<Clock>().expect("Clock must be registered");
143		let spawner = ioc.resolve::<ActorSpawner>().expect("ActorSpawner must be registered");
144		let custom_operators = CustomOperators::new(config.custom_operators);
145		let allocators = FlowAllocators::with_dictionary(engine.dictionary_allocators());
146		let primitive_tracker = ShapeVersionTracker::new();
147		let flow_tracker = FlowPositionTracker::new();
148		let cdc_store = ioc.resolve::<CdcStore>().expect("CdcStore must be registered");
149
150		let flow_scope = spawner.scope();
151		let configured_workers = engine.catalog().get_config_uint2(ConfigKey::FlowWorkerThreads) as usize;
152		let num_workers = if configured_workers == 0 {
153			spawner.pools().system_thread_count()
154		} else {
155			configured_workers
156		}
157		.max(2);
158		info!(num_workers, "initializing flow coordinator with {} workers", num_workers);
159
160		let flow_catalog = FlowCatalog::new(engine.catalog());
161
162		let (worker_refs, worker_handles) = Self::spawn_flow_workers(
163			&flow_scope,
164			num_workers,
165			&engine,
166			&flow_catalog,
167			&clock,
168			&custom_operators,
169			&allocators,
170		);
171
172		let pool_handle = flow_scope.spawn_system("flow-pool", PoolActor::new(worker_refs, clock.clone()));
173		let pool_ref = pool_handle.actor_ref().clone();
174
175		let flow_consumer_id = CdcConsumerId::flow_consumer();
176		let coordinator_handle = flow_scope.spawn_system(
177			"flow-coordinator",
178			CoordinatorActor::new(
179				engine.clone(),
180				flow_catalog.clone(),
181				pool_ref,
182				primitive_tracker.clone(),
183				flow_tracker.clone(),
184				cdc_store.clone(),
185				num_workers,
186				clock.clone(),
187				flow_consumer_id.clone(),
188			),
189		);
190		let consume_ref = FlowConsumeRef {
191			actor_ref: coordinator_handle.actor_ref().clone(),
192		};
193
194		let transactional_flow_engine =
195			Self::build_transactional_engine(&engine, &clock, &custom_operators, &allocators);
196
197		let registrar = TransactionalFlowRegistry {
198			flow_engine: transactional_flow_engine.clone(),
199			engine: engine.clone(),
200			catalog: engine.catalog(),
201		};
202
203		Self::register_flow_interceptors(&engine, &transactional_flow_engine, &clock, &custom_operators);
204
205		let transactional_tick_handle = flow_scope.spawn_system(
206			"transactional-flow-tick",
207			TransactionalTickActor::new(
208				transactional_flow_engine.clone(),
209				engine.clone(),
210				engine.catalog(),
211				clock.clone(),
212			),
213		);
214
215		Self::register_watermark_sampler(ioc, &primitive_tracker, &flow_tracker, &flow_catalog);
216
217		let cdc_wake_registry = ioc.resolve::<CdcWakeRegistry>().expect("CdcWakeRegistry must be registered");
218		let flow_consumer_watermark = FlowConsumerWatermark::default();
219		ioc.register_service::<FlowConsumerWatermark>(flow_consumer_watermark.clone());
220		let poll_config = PollConsumerConfig::new(
221			flow_consumer_id,
222			"flow-cdc-poll",
223			Duration::from_seconds(1).unwrap(),
224			Some(100),
225		)
226		.with_wake_registry(cdc_wake_registry)
227		.with_consumer_watermark(flow_consumer_watermark.0.clone());
228
229		let bootstrap_flows = Self::bootstrap_flows(&engine, &flow_catalog, &registrar);
230		let _ = coordinator_handle.actor_ref().send(FlowCoordinatorMessage::Bootstrap {
231			flows: bootstrap_flows,
232		});
233
234		let dispatcher = FlowConsumeDispatcher {
235			coordinator: consume_ref,
236			registrar,
237			flow_catalog,
238			engine: engine.clone(),
239		};
240		let mut consumer = PollConsumer::new(poll_config, engine, dispatcher, cdc_store, flow_scope.clone());
241		consumer.start()?;
242
243		Ok(Self {
244			consumer: Mutex::new(consumer),
245			flow_scope,
246			worker_handles: Mutex::new(worker_handles),
247			pool_handle: Mutex::new(Some(pool_handle)),
248			coordinator_handle: Mutex::new(Some(coordinator_handle)),
249			transactional_tick_handle: Mutex::new(Some(transactional_tick_handle)),
250			transactional_flow_engine,
251			running: AtomicBool::new(true),
252		})
253	}
254
255	#[inline]
256	fn maybe_load_ffi_operators(config: &FlowConfig, engine: &StandardEngine) {
257		#[cfg(reifydb_target = "native")]
258		if let Some(ref operators_dir) = config.operators_dir {
259			let event_bus = engine.event_bus();
260			if let Err(e) = load_native_operators(operators_dir, event_bus) {
261				panic!("Failed to load native operators from {:?}: {}", operators_dir, e);
262			}
263			if let Err(e) = load_ffi_operators(operators_dir, event_bus) {
264				panic!("Failed to load FFI operators from {:?}: {}", operators_dir, e);
265			}
266			event_bus.wait_for_completion();
267		}
268		#[cfg(not(reifydb_target = "native"))]
269		{
270			let _ = (config, engine);
271		}
272	}
273
274	#[inline]
275	fn spawn_flow_workers(
276		spawner: &ActorSpawner,
277		num_workers: usize,
278		engine: &StandardEngine,
279		flow_catalog: &FlowCatalog,
280		clock: &Clock,
281		custom_operators: &CustomOperators,
282		allocators: &FlowAllocators,
283	) -> (Vec<ActorRef<FlowMessage>>, Vec<FlowHandle>) {
284		let mut worker_refs = Vec::with_capacity(num_workers);
285		let mut worker_handles = Vec::with_capacity(num_workers);
286
287		for i in 0..num_workers {
288			let cat = engine.catalog();
289			let exec = engine.executor();
290			let bus = engine.event_bus().clone();
291			let rc = RuntimeContext::with_clock(clock.clone());
292			let co = custom_operators.clone();
293			let alloc = allocators.clone();
294			let worker_factory = move || FlowEngineInner::new(cat, exec, bus, rc, co, alloc);
295
296			let worker = FlowWorkerActor::new(
297				worker_factory,
298				engine.clone(),
299				engine.catalog(),
300				flow_catalog.clone(),
301			);
302			let handle = spawner.spawn_system(&format!("flow-worker-{}", i), worker);
303			worker_refs.push(handle.actor_ref().clone());
304			worker_handles.push(handle);
305		}
306
307		(worker_refs, worker_handles)
308	}
309
310	#[inline]
311	fn build_transactional_engine(
312		engine: &StandardEngine,
313		clock: &Clock,
314		custom_operators: &CustomOperators,
315		allocators: &FlowAllocators,
316	) -> FlowEngine {
317		FlowEngine::new(
318			engine.catalog(),
319			engine.executor(),
320			engine.event_bus().clone(),
321			RuntimeContext::with_clock(clock.clone()),
322			custom_operators.clone(),
323			allocators.clone(),
324		)
325	}
326
327	#[inline]
328	fn register_watermark_sampler(
329		ioc: &IocContainer,
330		primitive_tracker: &ShapeVersionTracker,
331		flow_tracker: &FlowPositionTracker,
332		flow_catalog: &FlowCatalog,
333	) {
334		ioc.register_service::<FlowWatermarkSampler>(FlowWatermarkSampler::new({
335			let tracker = primitive_tracker.clone();
336			let flow_tracker = flow_tracker.clone();
337			let flow_catalog = flow_catalog.clone();
338			move || compute_flow_watermarks(&tracker, &flow_tracker, &flow_catalog)
339		}));
340	}
341
342	#[inline]
343	fn bootstrap_flows(
344		engine: &StandardEngine,
345		flow_catalog: &FlowCatalog,
346		registrar: &TransactionalFlowRegistry,
347	) -> Vec<(FlowId, bool)> {
348		let mut bootstrap_flows = Vec::new();
349		if let Ok(mut query) = engine.begin_query(IdentityId::system()) {
350			match engine.catalog().list_flows_all(&mut Transaction::Query(&mut query)) {
351				Ok(existing_flows) => {
352					for existing in existing_flows {
353						match flow_catalog.get_or_load_flow(
354							&mut Transaction::Query(&mut query),
355							existing.id,
356						) {
357							Ok((flow, _)) => match registrar.try_register(flow, &mut query)
358							{
359								Ok(is_transactional) => bootstrap_flows
360									.push((existing.id, !is_transactional)),
361								Err(e) => warn!(
362									flow_id = existing.id.0,
363									error = %e,
364									"failed to register transactional flow during bootstrap"
365								),
366							},
367							Err(e) => warn!(
368								flow_id = existing.id.0,
369								error = %e,
370								"failed to load flow during bootstrap"
371							),
372						}
373					}
374				}
375				Err(e) => warn!(error = %e, "failed to list flows during bootstrap"),
376			}
377		}
378		bootstrap_flows
379	}
380
381	#[inline]
382	fn register_flow_interceptors(
383		engine: &StandardEngine,
384		transactional_flow_engine: &FlowEngine,
385		clock: &Clock,
386		custom_operators: &CustomOperators,
387	) {
388		let flow_engine_for_pre = transactional_flow_engine.clone();
389		let engine_for_pre = engine.clone();
390		let catalog_for_pre = engine.catalog();
391
392		let flow_engine_for_post = transactional_flow_engine.clone();
393		let engine_for_post = engine.clone();
394		let catalog_for_post = engine.catalog();
395
396		let test_flow_engine = transactional_flow_engine.clone();
397		let test_engine = engine.clone();
398		let test_catalog = engine.catalog();
399		let test_event_bus = engine.event_bus().clone();
400		let test_runtime_context = RuntimeContext::with_clock(clock.clone());
401		let test_custom_operators = custom_operators.clone();
402
403		engine.add_interceptor_factory(Arc::new(move |interceptors: &mut Interceptors| {
404			interceptors.pre_commit.add(Arc::new(TransactionalFlowPreCommitInterceptor {
405				flow_engine: flow_engine_for_pre.clone(),
406				engine: engine_for_pre.clone(),
407				catalog: catalog_for_pre.clone(),
408			}));
409			interceptors.post_commit.add(Arc::new(TransactionalFlowPostCommitInterceptor {
410				registrar: TransactionalFlowRegistry {
411					flow_engine: flow_engine_for_post.clone(),
412					engine: engine_for_post.clone(),
413					catalog: catalog_for_post.clone(),
414				},
415			}));
416
417			let hook_flow_engine = test_flow_engine.clone();
418			let hook_engine = test_engine.clone();
419			let hook_catalog = test_catalog.clone();
420			let hook_event_bus = test_event_bus.clone();
421			let hook_runtime_context = test_runtime_context.clone();
422			let hook_custom_operators = test_custom_operators.clone();
423
424			interceptors.set_test_pre_commit(Arc::new(move |test_txn: &mut TestTransaction<'_>| {
425				let mut fresh_engine = FlowEngineInner::new(
426					hook_catalog.clone(),
427					hook_engine.executor(),
428					hook_event_bus.clone(),
429					hook_runtime_context.clone(),
430					hook_custom_operators.clone(),
431					FlowAllocators::with_dictionary(hook_engine.dictionary_allocators()),
432				);
433
434				let flows = hook_catalog
435					.list_flows_all(&mut Transaction::Test(Box::new(test_txn.reborrow())))?;
436
437				for flow in flows {
438					let dag = load_flow_dag(
439						&mut Transaction::Test(Box::new(test_txn.reborrow())),
440						flow.id,
441					)?;
442					fresh_engine.register_with_transaction(
443						&mut Transaction::Test(Box::new(test_txn.reborrow())),
444						dag,
445					)?;
446				}
447
448				*hook_flow_engine.write() = fresh_engine;
449				Ok(())
450			}));
451		}));
452	}
453}
454
455impl Shutdown for FlowSubsystem {
456	fn shutdown(&self) {
457		if self.running.compare_exchange(true, false, Ordering::AcqRel, Ordering::Acquire).is_err() {
458			return;
459		}
460
461		if let Err(e) = self.consumer.lock().stop() {
462			warn!(error = %e, "flow consumer stop failed during shutdown");
463		}
464
465		self.flow_scope.shutdown();
466
467		if let Some(handle) = self.coordinator_handle.lock().take() {
468			let _ = handle.join();
469		}
470
471		if let Some(handle) = self.pool_handle.lock().take() {
472			let _ = handle.join();
473		}
474
475		if let Some(handle) = self.transactional_tick_handle.lock().take() {
476			let _ = handle.join();
477		}
478
479		let workers: Vec<_> = self.worker_handles.lock().drain(..).collect();
480		for handle in workers {
481			let _ = handle.join();
482		}
483
484		self.transactional_flow_engine.write().clear();
485	}
486}
487
488impl Subsystem for FlowSubsystem {
489	fn name(&self) -> &'static str {
490		"sub-flow"
491	}
492
493	fn is_running(&self) -> bool {
494		self.running.load(Ordering::Acquire)
495	}
496
497	fn health_status(&self) -> HealthStatus {
498		if self.is_running() {
499			HealthStatus::Healthy
500		} else {
501			HealthStatus::Unknown
502		}
503	}
504
505	fn as_any(&self) -> &dyn Any {
506		self
507	}
508}
509
510impl HasVersion for FlowSubsystem {
511	fn version(&self) -> SystemVersion {
512		SystemVersion {
513			name: env!("CARGO_PKG_NAME")
514				.strip_prefix("reifydb-")
515				.unwrap_or(env!("CARGO_PKG_NAME"))
516				.to_string(),
517			version: env!("CARGO_PKG_VERSION").to_string(),
518			description: "Data flow and stream processing subsystem".to_string(),
519			r#type: ComponentType::Subsystem,
520		}
521	}
522}
523
524impl Drop for FlowSubsystem {
525	fn drop(&mut self) {
526		self.shutdown();
527	}
528}