1pub 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, ®istrar);
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}