Skip to main content

reifydb_sub_flow/subsystem/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4#[cfg(all(reifydb_target = "host", not(reifydb_dst)))]
5pub mod extern_c;
6#[cfg(all(reifydb_target = "host", not(reifydb_dst)))]
7pub mod extern_rust;
8pub mod factory;
9pub mod shutdown;
10
11use std::{
12	any::Any,
13	path::PathBuf,
14	sync::{
15		Arc,
16		atomic::{AtomicBool, Ordering},
17	},
18};
19
20#[cfg(all(reifydb_target = "host", not(reifydb_dst)))]
21use extern_c::load_extern_c_operators;
22#[cfg(all(reifydb_target = "host", not(reifydb_dst)))]
23use extern_rust::load_extern_rust_operators;
24use reifydb_cdc::{
25	consume::{
26		backlog::FlowBacklog,
27		watermark::{CdcConsumerWatermark, FlowCaughtUpWatermark},
28	},
29	storage::CdcStore,
30};
31use reifydb_core::{
32	actors::flow::{FlowSupervisorHandle, FlowSupervisorMessage},
33	event::operator::OperatorLoadedEvent,
34	interface::{
35		WithEventBus,
36		catalog::{
37			config::{ConfigKey, GetConfig},
38			flow::FlowId,
39		},
40		cdc::CdcConsumerId,
41		flow::FlowWatermarkSampler,
42		version::{ComponentType, HasVersion, SystemVersion},
43	},
44	lifecycle::watermark::ConsumerPositions,
45	metrics::registry::MetricsRegistry,
46	util::ioc::IocContainer,
47};
48use reifydb_engine::{engine::StandardEngine, vm::flow_lineage::ViewLineage};
49use reifydb_flow::{
50	operator::metrics::{OperatorSampleCollector, OperatorSampleRegistry},
51	transaction::substrate::FlowSubstrate,
52};
53use reifydb_runtime::{actor::system::ActorSpawner, context::clock::Clock, shutdown::Shutdown, sync::mutex::Mutex};
54use reifydb_sub_api::subsystem::{HealthStatus, Subsystem};
55use reifydb_transaction::{
56	group::{GroupCommitBegin, GroupCommitHandle},
57	transaction::Transaction,
58};
59use reifydb_value::{
60	Result,
61	byte_size::ByteSize,
62	value::{duration::Duration, identity::IdentityId},
63};
64use tracing::warn;
65
66use crate::{
67	builder::{CustomOperators, FlowConfig},
68	catalog::FlowCatalog,
69	commit::{
70		committer::{Committer, CommitterActor, CommitterHandle},
71		quiescence::FlowMaterialization,
72	},
73	control::{
74		health::FlowHealthRegistry,
75		supervisor::{FlowSupervisor, FlowSupervisorParams},
76	},
77	discovery::loader::{LoaderActor, LoaderHandle, LoaderMetrics},
78	progress::{
79		frontier::ControlFrontier,
80		tracker::{FlowPositionTracker, ObjectVersionTracker},
81		watermark::compute_flow_watermarks,
82	},
83	subsystem::shutdown::FlowShutdownState,
84};
85
86/// Versions of in-memory skip-ahead a flow tolerates before forcing a checkpoint-only commit.
87const FLOW_CHECKPOINT_LAG: u64 = 10_000;
88const FLOW_CHECKPOINT_MAX_AGE_MS: i64 = 5_000;
89const FLOW_FRONTIER_PERSIST_MS: i64 = 5_000;
90
91pub struct FlowSubsystem {
92	flow_scope: ActorSpawner,
93	loader_handle: Mutex<Option<LoaderHandle>>,
94	committer_handle: Mutex<Option<CommitterHandle>>,
95	supervisor_handle: Mutex<Option<FlowSupervisorHandle>>,
96	view_lineage: ViewLineage,
97	health: FlowHealthRegistry,
98	shutdown_state: FlowShutdownState,
99	running: AtomicBool,
100}
101
102impl FlowSubsystem {
103	pub fn publish_operator_catalog(config: &FlowConfig, engine: &StandardEngine) {
104		Self::publish_custom_operators(&config.custom_operators, engine);
105	}
106
107	pub fn new(config: FlowConfig, engine: StandardEngine, ioc: &IocContainer) -> Result<Self> {
108		Self::maybe_load_extern_operators(&config, &engine);
109
110		let clock = ioc.resolve::<Clock>().expect("Clock must be registered");
111		let spawner = ioc.resolve::<ActorSpawner>().expect("ActorSpawner must be registered");
112		let custom_operators = config.custom_operators;
113		let substrate = FlowSubstrate::with_dictionary(engine.dictionary_allocators(), engine.operator_state());
114		let object_tracker = ObjectVersionTracker::new();
115		let flow_tracker = FlowPositionTracker::new();
116		let cdc_store = ioc.resolve::<CdcStore>().expect("CdcStore must be registered");
117
118		let flow_scope = spawner.scope();
119		let flow_catalog = FlowCatalog::new(engine.catalog());
120
121		let group_commit = ioc.try_resolve::<GroupCommitHandle>().unwrap_or_else(|| {
122			let begin_engine = engine.clone();
123			let begin: GroupCommitBegin =
124				Arc::new(move || begin_engine.begin_command(IdentityId::system()));
125			GroupCommitHandle::inline(begin)
126		});
127		let poll_frontier = CdcConsumerWatermark::default();
128		let materialization = FlowMaterialization::new(poll_frontier.clone(), flow_tracker.clone());
129		let committer = Committer::new(
130			flow_tracker.clone(),
131			materialization.clone(),
132			substrate.operators.clone().expect("the flow substrate is built with an operator store"),
133		);
134		let committer_handle =
135			flow_scope.spawn_flow("flow-committer", CommitterActor::new(committer, group_commit));
136		let committer_ref = committer_handle.actor_ref().clone();
137
138		let health = FlowHealthRegistry::new();
139		let operator_samples = OperatorSampleRegistry::new();
140		let metrics_registry = ioc.resolve::<MetricsRegistry>().expect("MetricsRegistry must be registered");
141		metrics_registry
142			.register_operator_collector(Arc::new(OperatorSampleCollector::new(operator_samples.clone())));
143		let view_lineage = engine.view_lineage();
144
145		let backlog = ioc.resolve::<FlowBacklog>().expect("FlowBacklog must be registered");
146		metrics_registry.register_collector(Arc::new(backlog.clone()));
147		let loader_metrics = LoaderMetrics::default();
148		metrics_registry.register_collector(Arc::new(loader_metrics.clone()));
149		let control = ControlFrontier::new();
150		let loader_handle =
151			flow_scope.spawn_flow("flow-loader", LoaderActor::new(cdc_store.hot_reader(), loader_metrics));
152		let pull_batch_bytes =
153			ByteSize::from_bytes(engine.catalog().get_config_uint8(ConfigKey::FlowPullBatchBytes));
154		let load_batch_bytes =
155			ByteSize::from_bytes(engine.catalog().get_config_uint8(ConfigKey::FlowLoadBatchBytes));
156
157		let flow_consumer_id = CdcConsumerId::flow_consumer();
158		let supervisor_handle = flow_scope.spawn_flow(
159			"flow-supervisor",
160			FlowSupervisor::new(FlowSupervisorParams {
161				engine: engine.clone(),
162				flow_catalog: flow_catalog.clone(),
163				committer: committer_ref,
164				backlog: backlog.clone(),
165				loader: loader_handle.actor_ref().clone(),
166				control,
167				poll_frontier: poll_frontier.clone(),
168				view_lineage: view_lineage.clone(),
169				tracker: object_tracker.clone(),
170				flow_tracker: flow_tracker.clone(),
171				health: health.clone(),
172				custom_operators: custom_operators.clone(),
173				substrate: substrate.clone(),
174				operator_samples: operator_samples.clone(),
175				clock: clock.clone(),
176				spawner: flow_scope.clone(),
177				consumer_id: flow_consumer_id,
178				pull_batch_bytes,
179				load_batch_bytes,
180				checkpoint_lag: FLOW_CHECKPOINT_LAG,
181				checkpoint_max_age: Duration::from_milliseconds(FLOW_CHECKPOINT_MAX_AGE_MS).unwrap(),
182				frontier_persist: Duration::from_milliseconds(FLOW_FRONTIER_PERSIST_MS).unwrap(),
183			}),
184		);
185
186		Self::register_watermark_sampler(
187			ioc,
188			&engine,
189			&object_tracker,
190			&flow_tracker,
191			&flow_catalog,
192			&materialization,
193		);
194
195		ioc.register_service::<FlowCaughtUpWatermark>(FlowCaughtUpWatermark::new(move || {
196			materialization.caught_up()
197		}));
198
199		ioc.register_service::<Arc<dyn ConsumerPositions>>(Arc::new(flow_tracker.clone()));
200
201		let scan_from = engine.current_version().ok();
202		let bootstrap_flows = Self::bootstrap_flows(&engine);
203		let _ = supervisor_handle.actor_ref().send(FlowSupervisorMessage::Bootstrap {
204			flows: bootstrap_flows,
205			scan_from,
206		});
207
208		let supervisor_ref = supervisor_handle.actor_ref().clone();
209		backlog.set_waker(move || {
210			let _ = supervisor_ref.send(FlowSupervisorMessage::Wake);
211		});
212		let _ = supervisor_handle.actor_ref().send(FlowSupervisorMessage::Wake);
213
214		Ok(Self {
215			flow_scope,
216			loader_handle: Mutex::new(Some(loader_handle)),
217			committer_handle: Mutex::new(Some(committer_handle)),
218			supervisor_handle: Mutex::new(Some(supervisor_handle)),
219			view_lineage,
220			health,
221			shutdown_state: FlowShutdownState::new(engine, substrate),
222			running: AtomicBool::new(true),
223		})
224	}
225
226	pub fn persist_frontiers(&self) {
227		if self.is_running() {
228			self.shutdown_state.persist_frontiers();
229		}
230	}
231
232	#[inline]
233	fn publish_custom_operators(custom_operators: &CustomOperators, engine: &StandardEngine) {
234		let event_bus = engine.event_bus();
235		for (name, entry) in custom_operators.iter() {
236			event_bus.emit(OperatorLoadedEvent::new(
237				name.clone(),
238				PathBuf::new(),
239				entry.abi,
240				entry.version.clone(),
241				entry.description.clone(),
242				entry.input.clone(),
243				entry.output.clone(),
244				entry.capabilities,
245			));
246		}
247		event_bus.wait_for_completion();
248	}
249
250	#[inline]
251	fn maybe_load_extern_operators(config: &FlowConfig, engine: &StandardEngine) {
252		#[cfg(all(reifydb_target = "host", not(reifydb_dst)))]
253		if let Some(ref operators_dir) = config.operators_dir {
254			let event_bus = engine.event_bus();
255			if let Err(e) = load_extern_rust_operators(operators_dir, event_bus) {
256				panic!("Failed to load extern-Rust operators from {:?}: {}", operators_dir, e);
257			}
258			if let Err(e) = load_extern_c_operators(operators_dir, event_bus) {
259				panic!("Failed to load extern-C operators from {:?}: {}", operators_dir, e);
260			}
261			event_bus.wait_for_completion();
262		}
263		#[cfg(not(all(reifydb_target = "host", not(reifydb_dst))))]
264		{
265			let _ = (config, engine);
266		}
267	}
268
269	#[inline]
270	fn register_watermark_sampler(
271		ioc: &IocContainer,
272		engine: &StandardEngine,
273		object_tracker: &ObjectVersionTracker,
274		flow_tracker: &FlowPositionTracker,
275		flow_catalog: &FlowCatalog,
276		materialization: &FlowMaterialization,
277	) {
278		ioc.register_service::<FlowWatermarkSampler>(FlowWatermarkSampler::new({
279			let engine = engine.clone();
280			let tracker = object_tracker.clone();
281			let flow_tracker = flow_tracker.clone();
282			let flow_catalog = flow_catalog.clone();
283			let materialization = materialization.clone();
284			move || {
285				compute_flow_watermarks(&tracker, &flow_tracker, &flow_catalog, || {
286					engine.done_until().max(materialization.output_frontier())
287				})
288			}
289		}));
290	}
291
292	#[inline]
293	fn bootstrap_flows(engine: &StandardEngine) -> Vec<FlowId> {
294		let mut bootstrap_flows = Vec::new();
295		if let Ok(mut query) = engine.begin_query(IdentityId::system()) {
296			match engine.catalog().list_flows_all(&mut Transaction::Query(&mut query)) {
297				Ok(existing_flows) => {
298					bootstrap_flows.extend(existing_flows.into_iter().map(|existing| existing.id));
299				}
300				Err(e) => warn!(error = %e, "failed to list flows during bootstrap"),
301			}
302		}
303		bootstrap_flows
304	}
305}
306
307impl Shutdown for FlowSubsystem {
308	fn shutdown(&self) {
309		if self.running.compare_exchange(true, false, Ordering::AcqRel, Ordering::Acquire).is_err() {
310			return;
311		}
312
313		self.flow_scope.shutdown();
314
315		if let Some(handle) = self.supervisor_handle.lock().take() {
316			let _ = handle.join();
317		}
318
319		if let Some(handle) = self.loader_handle.lock().take() {
320			let _ = handle.join();
321		}
322
323		if let Some(handle) = self.committer_handle.lock().take() {
324			let _ = handle.join();
325		}
326
327		self.view_lineage.publish(Default::default());
328	}
329}
330
331impl Subsystem for FlowSubsystem {
332	fn name(&self) -> &'static str {
333		"flow"
334	}
335
336	fn is_running(&self) -> bool {
337		self.running.load(Ordering::Acquire)
338	}
339
340	fn health_status(&self) -> HealthStatus {
341		if !self.is_running() {
342			return HealthStatus::Unknown;
343		}
344		let poisoned = self.health.poisoned();
345		if poisoned.is_empty() {
346			return HealthStatus::Healthy;
347		}
348		let flows: Vec<String> =
349			poisoned.iter().map(|(id, reason)| format!("flow {}: {}", id.0, reason)).collect();
350		HealthStatus::Degraded {
351			description: format!("{} deferred flow(s) poisoned: {}", poisoned.len(), flows.join("; ")),
352		}
353	}
354
355	fn as_any(&self) -> &dyn Any {
356		self
357	}
358}
359
360impl HasVersion for FlowSubsystem {
361	fn version(&self) -> SystemVersion {
362		SystemVersion {
363			name: env!("CARGO_PKG_NAME")
364				.strip_prefix("reifydb-")
365				.unwrap_or(env!("CARGO_PKG_NAME"))
366				.to_string(),
367			version: env!("CARGO_PKG_VERSION").to_string(),
368			description: "Data flow and stream processing subsystem".to_string(),
369			r#type: ComponentType::Subsystem,
370		}
371	}
372}
373
374impl Drop for FlowSubsystem {
375	fn drop(&mut self) {
376		self.shutdown();
377	}
378}