1use std::collections::{BTreeMap, HashMap, HashSet};
18use std::sync::Arc;
19use std::time::Duration;
20
21use api::v1::flow::DirtyWindowRequests;
22use catalog::CatalogManagerRef;
23use common_error::ext::BoxedError;
24use common_meta::ddl::create_flow::{
25 FLOW_EXPERIMENTAL_ENABLE_INCREMENTAL_READ_KEY,
26 FLOW_EXPERIMENTAL_ENABLE_INCREMENTAL_READ_SEQUENCE_RANGE, FlowType,
27};
28use common_meta::key::TableMetadataManagerRef;
29use common_meta::key::flow::FlowMetadataManagerRef;
30use common_meta::key::flow::flow_state::FlowStat;
31use common_meta::key::table_info::{TableInfoManager, TableInfoValue};
32use common_runtime::JoinHandle;
33use common_telemetry::tracing::warn;
34use common_telemetry::{debug, info};
35use common_time::TimeToLive;
36use datafusion_common::tree_node::{TreeNodeRecursion, TreeNodeVisitor};
37use datafusion_expr::LogicalPlan;
38use datatypes::prelude::ConcreteDataType;
39use query::QueryEngineRef;
40use session::context::QueryContext;
41use snafu::{OptionExt, ResultExt, ensure};
42use sql::parsers::utils::is_tql;
43use store_api::metric_engine_consts::is_metric_engine_internal_column;
44use store_api::mito_engine_options::APPEND_MODE_KEY;
45use store_api::storage::{RegionId, TableId};
46use table::table_reference::TableReference;
47use tokio::sync::{RwLock, oneshot};
48
49use crate::batching_mode::BatchingModeOptions;
50use crate::batching_mode::eval_schedule::EvalSchedule;
51use crate::batching_mode::frontend_client::FrontendClient;
52use crate::batching_mode::state::DirtyTimeWindows;
53use crate::batching_mode::task::{BatchingTask, TaskArgs};
54use crate::batching_mode::time_window::{TimeWindowExpr, find_time_window_expr};
55use crate::batching_mode::utils::sql_to_df_plan;
56use crate::engine::{FlowEngine, FlowStatProvider};
57use crate::error::{
58 CreateFlowSnafu, DatafusionSnafu, ExternalSnafu, FlowAlreadyExistSnafu, FlowNotFoundSnafu,
59 InvalidQuerySnafu, JoinTaskSnafu, TableNotFoundMetaSnafu, UnexpectedSnafu, UnsupportedSnafu,
60};
61use crate::metrics::METRIC_FLOW_BATCHING_ENGINE_BULK_MARK_TIME_WINDOW;
62use crate::{CreateFlowArgs, Error, FlowId, TableName};
63
64pub struct BatchingEngine {
68 runtime: RwLock<FlowRuntimeRegistry>,
69 pub(crate) frontend_client: Arc<FrontendClient>,
71 flow_metadata_manager: FlowMetadataManagerRef,
72 table_meta: TableMetadataManagerRef,
73 catalog_manager: CatalogManagerRef,
74 query_engine: QueryEngineRef,
75 pub(crate) batch_opts: Arc<BatchingModeOptions>,
78 execution_factory: Option<Arc<dyn crate::BatchingExecutionFactory>>,
79}
80
81#[derive(Default)]
82struct FlowRuntimeRegistry {
83 tasks: BTreeMap<FlowId, BatchingTask>,
84 shutdown_txs: BTreeMap<FlowId, oneshot::Sender<()>>,
85}
86
87impl FlowRuntimeRegistry {
88 fn insert(
89 &mut self,
90 flow_id: FlowId,
91 task: BatchingTask,
92 shutdown_tx: oneshot::Sender<()>,
93 ) -> (Option<BatchingTask>, Option<oneshot::Sender<()>>) {
94 (
95 self.tasks.insert(flow_id, task),
96 self.shutdown_txs.insert(flow_id, shutdown_tx),
97 )
98 }
99
100 fn remove(&mut self, flow_id: FlowId) -> Option<(BatchingTask, Option<oneshot::Sender<()>>)> {
101 let task = self.tasks.remove(&flow_id)?;
102 let shutdown_tx = self.shutdown_txs.remove(&flow_id);
103 Some((task, shutdown_tx))
104 }
105
106 fn remove_if_current(
107 &mut self,
108 flow_id: FlowId,
109 task: &BatchingTask,
110 ) -> (Option<BatchingTask>, Option<oneshot::Sender<()>>) {
111 if self
112 .tasks
113 .get(&flow_id)
114 .is_some_and(|current| Arc::ptr_eq(¤t.state, &task.state))
115 {
116 let Some((removed_task, removed_shutdown_tx)) = self.remove(flow_id) else {
117 return (None, None);
118 };
119 (Some(removed_task), removed_shutdown_tx)
120 } else {
121 (None, None)
122 }
123 }
124}
125
126impl BatchingEngine {
127 pub fn new(
128 frontend_client: Arc<FrontendClient>,
129 query_engine: QueryEngineRef,
130 flow_metadata_manager: FlowMetadataManagerRef,
131 table_meta: TableMetadataManagerRef,
132 catalog_manager: CatalogManagerRef,
133 batch_opts: BatchingModeOptions,
134 ) -> Self {
135 Self::new_with_execution(
136 frontend_client,
137 query_engine,
138 flow_metadata_manager,
139 table_meta,
140 catalog_manager,
141 batch_opts,
142 None,
143 )
144 }
145
146 pub fn new_with_execution(
147 frontend_client: Arc<FrontendClient>,
148 query_engine: QueryEngineRef,
149 flow_metadata_manager: FlowMetadataManagerRef,
150 table_meta: TableMetadataManagerRef,
151 catalog_manager: CatalogManagerRef,
152 batch_opts: BatchingModeOptions,
153 execution_factory: Option<Arc<dyn crate::BatchingExecutionFactory>>,
154 ) -> Self {
155 Self {
156 runtime: Default::default(),
157 frontend_client,
158 flow_metadata_manager,
159 table_meta,
160 catalog_manager,
161 query_engine,
162 batch_opts: Arc::new(batch_opts),
163 execution_factory,
164 }
165 }
166
167 pub async fn get_last_exec_time_map(&self) -> BTreeMap<FlowId, i64> {
169 let runtime = self.runtime.read().await;
170 runtime
171 .tasks
172 .iter()
173 .filter_map(|(flow_id, task)| {
174 task.last_execution_time_millis()
175 .map(|timestamp| (*flow_id, timestamp))
176 })
177 .collect()
178 }
179
180 pub async fn handle_mark_dirty_time_window(
186 &self,
187 reqs: DirtyWindowRequests,
188 ) -> Result<(), Error> {
189 let table_info_mgr = self.table_meta.table_info_manager();
190
191 let mut group_by_table_id: HashMap<u32, (Vec<i64>, Vec<api::v1::flow::TimeRange>)> =
192 HashMap::new();
193 for r in reqs.requests {
194 let tid = TableId::from(r.table_id);
195 let entry = group_by_table_id.entry(tid).or_default();
196 entry.0.extend(r.timestamps);
197 entry.1.extend(r.time_ranges);
198 }
199 let tids = group_by_table_id.keys().cloned().collect::<Vec<TableId>>();
200 let table_infos =
201 table_info_mgr
202 .batch_get(&tids)
203 .await
204 .with_context(|_| TableNotFoundMetaSnafu {
205 msg: format!("Failed to get table info for table ids: {:?}", tids),
206 })?;
207
208 let group_by_table_name = group_by_table_id
209 .into_iter()
210 .filter_map(|(id, (timestamps, time_ranges))| {
211 let table_name = table_infos.get(&id).map(|info| info.table_name());
212 let Some(table_name) = table_name else {
213 warn!("Failed to get table infos for table id: {:?}", id);
214 return None;
215 };
216 let table_name = [
217 table_name.catalog_name,
218 table_name.schema_name,
219 table_name.table_name,
220 ];
221 let schema = &table_infos.get(&id).unwrap().table_info.meta.schema;
222 let time_index_unit = schema.column_schemas()[schema.timestamp_index().unwrap()]
223 .data_type
224 .as_timestamp()
225 .unwrap()
226 .unit();
227 Some((table_name, (timestamps, time_ranges, time_index_unit)))
228 })
229 .collect::<HashMap<_, _>>();
230
231 let group_by_table_name = Arc::new(group_by_table_name);
232
233 let tasks = self
234 .runtime
235 .read()
236 .await
237 .tasks
238 .values()
239 .cloned()
240 .collect::<Vec<_>>();
241 let mut handles = Vec::new();
242
243 for task in tasks {
244 let src_table_names = &task.config.source_table_names;
245
246 if src_table_names
247 .iter()
248 .all(|name| !group_by_table_name.contains_key(name))
249 {
250 continue;
251 }
252
253 let group_by_table_name = group_by_table_name.clone();
254 let task = task.clone();
255 let handle: JoinHandle<Result<(), Error>> = tokio::spawn(async move {
256 let src_table_names = &task.config.source_table_names;
257 let mut all_dirty_windows = HashSet::new();
258 let mut all_dirty_ranges = Vec::new();
259 let mut is_dirty = false;
260 for src_table_name in src_table_names {
261 if let Some((timestamps, time_ranges, unit)) =
262 group_by_table_name.get(src_table_name)
263 {
264 let Some(expr) = &task.config.time_window_expr else {
265 is_dirty = true;
266 continue;
267 };
268 for timestamp in timestamps {
269 let align_start = expr
270 .eval(common_time::Timestamp::new(*timestamp, *unit))?
271 .0
272 .context(UnexpectedSnafu {
273 reason: format!(
274 "Failed to align dirty timestamp {timestamp}: missing window lower bound"
275 ),
276 })?;
277 all_dirty_windows.insert(align_start);
278 }
279 for time_range in time_ranges {
280 if time_range.end_exclusive <= time_range.start_inclusive {
281 warn!(
282 "Ignoring invalid dirty time range with start_inclusive={} >= end_exclusive={}",
283 time_range.start_inclusive, time_range.end_exclusive
284 );
285 continue;
286 }
287 let (align_start, align_end) = DirtyTimeWindows::align_time_window(
288 common_time::Timestamp::new(time_range.start_inclusive, *unit),
289 Some(common_time::Timestamp::new(time_range.end_exclusive, *unit)),
290 expr,
291 )?;
292 all_dirty_ranges.push((align_start, align_end));
293 }
294 }
295 }
296 let mut state = task.state.write().unwrap();
297 if is_dirty {
298 state.dirty_time_windows.set_dirty();
299 }
300 let flow_id_label = task.config.flow_id.to_string();
301 for timestamp in all_dirty_windows {
302 state.dirty_time_windows.add_window(timestamp, None);
303 }
304 for (start, end) in all_dirty_ranges {
305 state.dirty_time_windows.add_window(start, end);
306 }
307
308 METRIC_FLOW_BATCHING_ENGINE_BULK_MARK_TIME_WINDOW
309 .with_label_values(&[&flow_id_label])
310 .set(state.dirty_time_windows.len() as f64);
311 Ok(())
312 });
313 handles.push(handle);
314 }
315 for handle in handles {
316 handle.await.context(JoinTaskSnafu)??;
317 }
318
319 Ok(())
320 }
321
322 pub async fn handle_inserts_inner(
323 &self,
324 request: api::v1::region::InsertRequests,
325 ) -> Result<(), Error> {
326 let table_info_mgr = self.table_meta.table_info_manager();
327 let mut group_by_table_id: HashMap<TableId, Vec<api::v1::Rows>> = HashMap::new();
328
329 for r in request.requests {
330 let tid = RegionId::from(r.region_id).table_id();
331 let entry = group_by_table_id.entry(tid).or_default();
332 if let Some(rows) = r.rows {
333 entry.push(rows);
334 }
335 }
336
337 let tids = group_by_table_id.keys().cloned().collect::<Vec<TableId>>();
338 let table_infos =
339 table_info_mgr
340 .batch_get(&tids)
341 .await
342 .with_context(|_| TableNotFoundMetaSnafu {
343 msg: format!("Failed to get table info for table ids: {:?}", tids),
344 })?;
345
346 let missing_tids = tids
347 .iter()
348 .filter(|id| !table_infos.contains_key(id))
349 .collect::<Vec<_>>();
350 if !missing_tids.is_empty() {
351 warn!(
352 "Failed to get all the table info for table ids, expected table ids: {:?}, those table doesn't exist: {:?}",
353 tids, missing_tids
354 );
355 }
356
357 let group_by_table_name = group_by_table_id
358 .into_iter()
359 .filter_map(|(id, rows)| {
360 let table_name = table_infos.get(&id).map(|info| info.table_name());
361 let Some(table_name) = table_name else {
362 warn!("Failed to get table infos for table id: {:?}", id);
363 return None;
364 };
365 let table_name = [
366 table_name.catalog_name,
367 table_name.schema_name,
368 table_name.table_name,
369 ];
370 Some((table_name, rows))
371 })
372 .collect::<HashMap<_, _>>();
373
374 let group_by_table_name = Arc::new(group_by_table_name);
375
376 let tasks = self
377 .runtime
378 .read()
379 .await
380 .tasks
381 .values()
382 .cloned()
383 .collect::<Vec<_>>();
384 let mut handles = Vec::new();
385 for task in tasks {
386 let src_table_names = &task.config.source_table_names;
387
388 if src_table_names
389 .iter()
390 .all(|name| !group_by_table_name.contains_key(name))
391 {
392 continue;
393 }
394
395 let group_by_table_name = group_by_table_name.clone();
396 let task = task.clone();
397
398 let handle: JoinHandle<Result<(), Error>> = tokio::spawn(async move {
399 let src_table_names = &task.config.source_table_names;
400
401 let mut is_dirty = false;
402
403 for src_table_name in src_table_names {
404 if let Some(entry) = group_by_table_name.get(src_table_name) {
405 let Some(expr) = &task.config.time_window_expr else {
406 is_dirty = true;
407 continue;
408 };
409 let involved_time_windows = expr.handle_rows(entry.clone()).await?;
410 let mut state = task.state.write().unwrap();
411 state
412 .dirty_time_windows
413 .add_lower_bounds(involved_time_windows.into_iter());
414 }
415 }
416 if is_dirty {
417 task.state.write().unwrap().dirty_time_windows.set_dirty();
418 }
419
420 Ok(())
421 });
422 handles.push(handle);
423 }
424
425 for handle in handles {
426 match handle.await {
427 Err(e) => {
428 warn!("Failed to handle inserts: {e}");
429 }
430 Ok(Ok(())) => (),
431 Ok(Err(e)) => {
432 warn!("Failed to handle inserts: {e}");
433 }
434 }
435 }
436 Ok(())
437 }
438}
439
440impl FlowStatProvider for BatchingEngine {
441 async fn flow_stat(&self) -> FlowStat {
442 let runtime = self.runtime.read().await;
443 let mut last_exec_time_map = BTreeMap::new();
444 let mut start_time_map = BTreeMap::new();
445
446 for (flow_id, task) in runtime.tasks.iter() {
447 let id = *flow_id as u32;
448 if let Some(ts) = task.last_execution_time_millis() {
449 last_exec_time_map.insert(id, ts);
450 }
451 if let Some(ts) = task.start_time_millis() {
452 start_time_map.insert(id, ts);
453 }
454 }
455
456 FlowStat {
457 state_size: BTreeMap::new(),
458 last_exec_time_map,
459 start_time_map,
460 }
461 }
462}
463
464async fn get_table_name(
465 table_info: &TableInfoManager,
466 table_id: &TableId,
467) -> Result<TableName, Error> {
468 get_table_info(table_info, table_id).await.map(|info| {
469 let name = info.table_name();
470 [name.catalog_name, name.schema_name, name.table_name]
471 })
472}
473
474async fn get_table_info(
475 table_info: &TableInfoManager,
476 table_id: &TableId,
477) -> Result<TableInfoValue, Error> {
478 table_info
479 .get(*table_id)
480 .await
481 .map_err(BoxedError::new)
482 .context(ExternalSnafu)?
483 .with_context(|| UnexpectedSnafu {
484 reason: format!("Table id = {:?}, couldn't found table name", table_id),
485 })
486 .map(|info| info.into_inner())
487}
488
489impl BatchingEngine {
490 fn batch_opts_for_flow_options(
491 &self,
492 flow_options: &HashMap<String, String>,
493 exact_sequence_range_required: bool,
494 ) -> Result<Arc<BatchingModeOptions>, Error> {
495 let mut batch_opts = (*self.batch_opts).clone();
496 if let Some(enable_incremental_read) =
497 flow_options.get(FLOW_EXPERIMENTAL_ENABLE_INCREMENTAL_READ_KEY)
498 {
499 batch_opts.experimental_enable_incremental_read = if exact_sequence_range_required {
500 true
501 } else {
502 enable_incremental_read.parse::<bool>().map_err(|_| {
503 InvalidQuerySnafu {
504 reason: format!(
505 "Invalid flow option {FLOW_EXPERIMENTAL_ENABLE_INCREMENTAL_READ_KEY}: {enable_incremental_read}"
506 ),
507 }
508 .build()
509 })?
510 };
511 }
512
513 Ok(Arc::new(batch_opts))
514 }
515
516 fn table_options_enable_append_mode(extra_options: &HashMap<String, String>) -> bool {
517 extra_options
518 .get(APPEND_MODE_KEY)
519 .is_some_and(|value| value.eq_ignore_ascii_case("true"))
520 }
521
522 fn ensure_sql_flow_has_twe_or_eval_interval(
525 eval_interval: Option<i64>,
526 has_time_window_expr: bool,
527 ) -> Result<(), Error> {
528 ensure!(
529 eval_interval.is_some() || has_time_window_expr,
530 InvalidQuerySnafu {
531 reason: "SQL batching flow without a time-window expression must specify EVAL INTERVAL to run as an explicit full-query flow"
532 .to_string(),
533 }
534 );
535 Ok(())
536 }
537
538 fn ensure_incremental_source_append_only(
539 batch_opts: &BatchingModeOptions,
540 table_name: &[String; 3],
541 extra_options: &HashMap<String, String>,
542 ) -> Result<(), Error> {
543 if batch_opts.experimental_enable_incremental_read {
544 ensure!(
545 Self::table_options_enable_append_mode(extra_options),
546 UnsupportedSnafu {
547 reason: format!(
548 "Flow incremental read requires append-only source table, but source table `{}` is not append-only. Consider setting append_mode='true' on the source table or disabling experimental_enable_incremental_read",
549 table_name.join(".")
550 ),
551 }
552 );
553 }
554
555 Ok(())
556 }
557
558 pub async fn create_flow_inner(&self, args: CreateFlowArgs) -> Result<Option<FlowId>, Error> {
559 let CreateFlowArgs {
560 flow_id,
561 sink_table_name,
562 source_table_ids,
563 create_if_not_exists,
564 or_replace,
565 expire_after,
566 eval_interval,
567 comment: _,
568 sql,
569 flow_options,
570 query_ctx,
571 eval_schedule: eval_schedule_config,
572 } = args;
573
574 {
576 let is_exist = self.runtime.read().await.tasks.contains_key(&flow_id);
577 match (create_if_not_exists, or_replace, is_exist) {
578 (_, true, true) => {
580 info!("Replacing flow with id={}", flow_id);
581 }
582 (false, false, true) => FlowAlreadyExistSnafu { id: flow_id }.fail()?,
583 (true, false, true) => {
585 info!("Flow with id={} already exists, do nothing", flow_id);
586 return Ok(None);
587 }
588
589 (_, _, false) => (),
591 }
592 }
593
594 let query_ctx = query_ctx.context({
595 UnexpectedSnafu {
596 reason: "Query context is None".to_string(),
597 }
598 })?;
599 let query_ctx = Arc::new(query_ctx);
600 let is_tql = is_tql(query_ctx.sql_dialect(), &sql)
601 .map_err(BoxedError::new)
602 .context(CreateFlowSnafu { sql: &sql })?;
603
604 if eval_interval.is_none() && is_tql {
606 InvalidQuerySnafu {
607 reason: "TQL query requires EVAL INTERVAL to be set".to_string(),
608 }
609 .fail()?;
610 }
611
612 let flow_type = flow_options.get(FlowType::FLOW_TYPE_KEY);
613
614 ensure!(
615 match flow_type {
616 None => true,
617 Some(ty) if ty == FlowType::BATCHING => true,
618 _ => false,
619 },
620 UnexpectedSnafu {
621 reason: format!("Flow type is not batching nor None, got {flow_type:?}")
622 }
623 );
624
625 let exact_sequence_range_required = flow_options
629 .get(FLOW_EXPERIMENTAL_ENABLE_INCREMENTAL_READ_KEY)
630 .is_some_and(|value| value == FLOW_EXPERIMENTAL_ENABLE_INCREMENTAL_READ_SEQUENCE_RANGE);
631 let batch_opts =
632 self.batch_opts_for_flow_options(&flow_options, exact_sequence_range_required)?;
633
634 let mut source_table_names = Vec::with_capacity(2);
635 for src_id in source_table_ids {
636 let table_name = get_table_name(self.table_meta.table_info_manager(), &src_id).await?;
638 let table_info = get_table_info(self.table_meta.table_info_manager(), &src_id).await?;
639 ensure!(
640 table_info.table_info.meta.options.ttl != Some(TimeToLive::Instant),
641 UnsupportedSnafu {
642 reason: format!(
643 "Source table `{}`(id={}) has instant TTL, Instant TTL is not supported under batching mode. Consider using a TTL longer than flush interval",
644 table_name.join("."),
645 src_id
646 ),
647 }
648 );
649 Self::ensure_incremental_source_append_only(
650 &batch_opts,
651 &table_name,
652 &table_info.table_info.meta.options.extra_options,
653 )?;
654
655 source_table_names.push(table_name);
656 }
657
658 let (tx, rx) = oneshot::channel();
659
660 let plan = sql_to_df_plan(query_ctx.clone(), self.query_engine.clone(), &sql, true).await?;
661
662 if is_tql {
663 self.check_is_tql_table(&plan, &query_ctx).await?;
664 }
665
666 let phy_expr = if !is_tql {
667 let (column_name, time_window_expr, _, df_schema) = find_time_window_expr(
668 &plan,
669 self.query_engine.engine_state().catalog_manager().clone(),
670 query_ctx.clone(),
671 )
672 .await?;
673 time_window_expr
674 .map(|expr| {
675 TimeWindowExpr::from_expr(
676 &expr,
677 &column_name,
678 &df_schema,
679 &self.query_engine.engine_state().session_state(),
680 )
681 })
682 .transpose()?
683 } else {
684 None
686 };
687
688 debug!(
689 "Flow id={}, found time window expr={}",
690 flow_id,
691 phy_expr
692 .as_ref()
693 .map(|phy_expr| phy_expr.to_string())
694 .unwrap_or("None".to_string())
695 );
696
697 if !is_tql {
698 Self::ensure_sql_flow_has_twe_or_eval_interval(eval_interval, phy_expr.is_some())?;
699 }
700
701 let eval_schedule = {
703 let interval = eval_interval;
704 let config = eval_schedule_config.as_ref();
705 match EvalSchedule::from_config(interval, config) {
706 Ok(s) => s,
707 Err(e) => {
708 return UnexpectedSnafu {
709 reason: format!(
710 "Failed to build eval schedule for flow {}: {}",
711 flow_id, e
712 ),
713 }
714 .fail();
715 }
716 }
717 };
718
719 let task_args = TaskArgs {
720 flow_id,
721 query: &sql,
722 plan,
723 time_window_expr: phy_expr,
724 expire_after,
725 sink_table_name,
726 source_table_names,
727 query_ctx,
728 catalog_manager: self.catalog_manager.clone(),
729 shutdown_rx: rx,
730 batch_opts,
731 flow_eval_interval: eval_interval.map(|secs| Duration::from_secs(secs as u64)),
732 eval_schedule,
733 };
734
735 let task = BatchingTask::try_new_with_exact_sequence_range_required(
736 task_args,
737 exact_sequence_range_required,
738 )?;
739
740 if task.config.exact_sequence_range_required {
741 ensure!(
742 task.sequence_range_capable().await?,
743 UnsupportedSnafu {
744 reason: format!(
745 "Flow {flow_id} requires exact sequence-range reads, but a source table lacks the Mito preserve_row_sequence capability"
746 ),
747 }
748 );
749 }
750
751 let engine = self.query_engine.clone();
752 let frontend = self.frontend_client.clone();
753
754 let table = task.check_or_create_sink_table(&engine, &frontend).await?;
757 let execution = if let Some(factory) = &self.execution_factory {
758 factory.create(&task, table, &engine, &frontend).await?
759 } else {
760 None
761 };
762 if execution.is_none() {
763 task.validate_sink_table_schema(&engine).await?;
764 }
765 let task = task.with_execution(execution);
766 let task_inner = task.clone();
767
768 let (start_tx, start_rx) = oneshot::channel();
769
770 let handle = common_runtime::spawn_global(async move {
772 if start_rx.await.is_ok() {
773 task_inner.start_executing_loop(engine, frontend).await;
774 }
775 });
776 task.state.write().unwrap().task_handle = Some(handle);
777 let task_for_rollback = task.clone();
778
779 let (replaced_old_task_opt, replaced_old_shutdown_tx) = {
784 let mut runtime = self.runtime.write().await;
785
786 let is_exist = runtime.tasks.contains_key(&flow_id);
787 match (create_if_not_exists, or_replace, is_exist) {
788 (_, true, true) => {
789 info!(
790 "Replacing flow with id={} after final registry check",
791 flow_id
792 );
793 }
794 (false, false, true) => {
795 abort_flow_task(flow_id, Some(task), "unregistered");
796 return FlowAlreadyExistSnafu { id: flow_id }.fail();
797 }
798 (true, false, true) => {
799 info!(
800 "Flow with id={} already exists at final registry check, do nothing",
801 flow_id
802 );
803 abort_flow_task(flow_id, Some(task), "unregistered");
804 return Ok(None);
805 }
806 (_, _, false) => (),
807 }
808
809 runtime.insert(flow_id, task, tx)
810 };
811
812 notify_flow_shutdown(flow_id, replaced_old_shutdown_tx, "replaced");
813 abort_flow_task(flow_id, replaced_old_task_opt, "replaced");
814 if start_tx.send(()).is_err() {
815 self.rollback_flow_runtime_if_current(flow_id, &task_for_rollback)
816 .await;
817 UnexpectedSnafu {
818 reason: format!("Failed to start flow {flow_id} due to task already dropped"),
819 }
820 .fail()?;
821 }
822
823 Ok(Some(flow_id))
824 }
825
826 async fn check_is_tql_table(
827 &self,
828 query: &LogicalPlan,
829 query_ctx: &QueryContext,
830 ) -> Result<(), Error> {
831 struct CollectTableRef {
832 table_refs: HashSet<datafusion_common::TableReference>,
833 }
834
835 impl TreeNodeVisitor<'_> for CollectTableRef {
836 type Node = LogicalPlan;
837 fn f_down(
838 &mut self,
839 node: &Self::Node,
840 ) -> datafusion_common::Result<TreeNodeRecursion> {
841 if let LogicalPlan::TableScan(scan) = node {
842 self.table_refs.insert(scan.table_name.clone());
843 }
844 Ok(TreeNodeRecursion::Continue)
845 }
846 }
847 let mut table_refs = CollectTableRef {
848 table_refs: HashSet::new(),
849 };
850 query
851 .visit_with_subqueries(&mut table_refs)
852 .context(DatafusionSnafu {
853 context: "Checking if all source tables are TQL tables",
854 })?;
855
856 let default_catalog = query_ctx.current_catalog();
857 let default_schema = query_ctx.current_schema();
858 let default_schema = &default_schema;
859
860 for table_ref in table_refs.table_refs {
861 let table_ref = match &table_ref {
862 datafusion_common::TableReference::Bare { table } => {
863 TableReference::full(default_catalog, default_schema, table)
864 }
865 datafusion_common::TableReference::Partial { schema, table } => {
866 TableReference::full(default_catalog, schema, table)
867 }
868 datafusion_common::TableReference::Full {
869 catalog,
870 schema,
871 table,
872 } => TableReference::full(catalog, schema, table),
873 };
874
875 let table_id = self
876 .table_meta
877 .table_name_manager()
878 .get(table_ref.into())
879 .await
880 .map_err(BoxedError::new)
881 .context(ExternalSnafu)?
882 .with_context(|| UnexpectedSnafu {
883 reason: format!("Failed to get table id for table: {}", table_ref),
884 })?
885 .table_id();
886 let table_info =
887 get_table_info(self.table_meta.table_info_manager(), &table_id).await?;
888 let value_cols = table_info
890 .table_info
891 .meta
892 .schema
893 .column_schemas()
894 .iter()
895 .filter(|col| col.data_type == ConcreteDataType::float64_datatype())
896 .collect::<Vec<_>>();
897 ensure!(
898 value_cols.len() == 1,
899 InvalidQuerySnafu {
900 reason: format!(
901 "TQL query only supports one f64 value column, table `{}`(id={}) has {} f64 value columns, columns are: {:?}",
902 table_ref,
903 table_id,
904 value_cols.len(),
905 value_cols
906 ),
907 }
908 );
909 let pk_idxs = table_info
911 .table_info
912 .meta
913 .primary_key_indices
914 .iter()
915 .collect::<HashSet<_>>();
916
917 for (idx, col) in table_info
918 .table_info
919 .meta
920 .schema
921 .column_schemas()
922 .iter()
923 .enumerate()
924 {
925 if is_metric_engine_internal_column(&col.name) {
926 continue;
927 }
928 let is_pk: bool = pk_idxs.contains(&&idx);
934
935 ensure!(
936 col.data_type == ConcreteDataType::float64_datatype()
937 || col.data_type.is_timestamp()
938 || (col.data_type == ConcreteDataType::string_datatype() && is_pk),
939 InvalidQuerySnafu {
940 reason: format!(
941 "TQL query only supports f64 value column, timestamp column and string tag columns, table `{}`(id={}) has column `{}` with type {:?} which is not supported",
942 table_ref, table_id, col.name, col.data_type
943 ),
944 }
945 );
946 }
947 }
948 Ok(())
949 }
950
951 pub async fn remove_flow_inner(&self, flow_id: FlowId) -> Result<(), Error> {
952 let (task, shutdown_tx) = {
953 let mut runtime = self.runtime.write().await;
954 let Some((task, shutdown_tx)) = runtime.remove(flow_id) else {
955 warn!("Flow {flow_id} not found in tasks");
956 FlowNotFoundSnafu { id: flow_id }.fail()?
957 };
958 (task, shutdown_tx)
959 };
960
961 let had_shutdown_tx = notify_flow_shutdown(flow_id, shutdown_tx, "removed");
962 abort_flow_task(flow_id, Some(task), "removed");
963
964 if !had_shutdown_tx {
965 UnexpectedSnafu {
966 reason: format!("Can't found shutdown tx for flow {flow_id}"),
967 }
968 .fail()?
969 }
970
971 Ok(())
972 }
973
974 pub async fn flush_flow_inner(&self, flow_id: FlowId) -> Result<usize, Error> {
977 debug!("Try flush flow {flow_id}");
978 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
982 let task = self.runtime.read().await.tasks.get(&flow_id).cloned();
983 let task = task.with_context(|| FlowNotFoundSnafu { id: flow_id })?;
984
985 let time_window_size = task
986 .config
987 .time_window_expr
988 .as_ref()
989 .and_then(|expr| *expr.time_window_size());
990
991 let cur_dirty_window_cnt = time_window_size.map(|time_window_size| {
992 task.state
993 .read()
994 .unwrap()
995 .dirty_time_windows
996 .effective_count(&time_window_size)
997 });
998
999 let res = task
1000 .execute_once_serialized(
1001 &self.query_engine,
1002 &self.frontend_client,
1003 cur_dirty_window_cnt,
1004 )
1005 .await?;
1006
1007 let affected_rows = res.map(|(r, _)| r).unwrap_or_default();
1008 debug!(
1009 "Successfully flush flow {flow_id}, affected rows={}",
1010 affected_rows
1011 );
1012 Ok(affected_rows)
1013 }
1014
1015 pub async fn flow_exist_inner(&self, flow_id: FlowId) -> bool {
1017 self.runtime.read().await.tasks.contains_key(&flow_id)
1018 }
1019
1020 async fn rollback_flow_runtime_if_current(&self, flow_id: FlowId, task: &BatchingTask) {
1021 let (removed_task, removed_shutdown_tx) = {
1022 let mut runtime = self.runtime.write().await;
1023 runtime.remove_if_current(flow_id, task)
1024 };
1025
1026 notify_flow_shutdown(flow_id, removed_shutdown_tx, "rolled back");
1027 abort_flow_task(flow_id, removed_task, "rolled back");
1028 }
1029}
1030
1031fn notify_flow_shutdown(flow_id: FlowId, tx: Option<oneshot::Sender<()>>, action: &str) -> bool {
1032 let Some(tx) = tx else {
1033 return false;
1034 };
1035
1036 if tx.send(()).is_err() {
1037 warn!(
1038 "Fail to shutdown {action} flow {flow_id} due to receiver already dropped, maybe flow {flow_id} is already dropped?"
1039 );
1040 }
1041
1042 true
1043}
1044
1045fn abort_flow_task(flow_id: FlowId, task: Option<BatchingTask>, action: &str) -> bool {
1046 let Some(task) = task else {
1047 return false;
1048 };
1049
1050 task.stop_execution();
1051
1052 if let Some(handle) = task.state.write().unwrap().task_handle.take() {
1053 handle.abort();
1054 debug!("Aborted {action} flow task {flow_id}");
1055 return true;
1056 }
1057
1058 false
1059}
1060
1061impl FlowEngine for BatchingEngine {
1062 async fn create_flow(&self, args: CreateFlowArgs) -> Result<Option<FlowId>, Error> {
1063 self.create_flow_inner(args).await
1064 }
1065 async fn remove_flow(&self, flow_id: FlowId) -> Result<(), Error> {
1066 self.remove_flow_inner(flow_id).await
1067 }
1068 async fn flush_flow(&self, flow_id: FlowId) -> Result<usize, Error> {
1069 self.flush_flow_inner(flow_id).await
1070 }
1071 async fn flow_exist(&self, flow_id: FlowId) -> Result<bool, Error> {
1072 Ok(self.flow_exist_inner(flow_id).await)
1073 }
1074 async fn list_flows(&self) -> Result<impl IntoIterator<Item = FlowId>, Error> {
1075 Ok(self
1076 .runtime
1077 .read()
1078 .await
1079 .tasks
1080 .keys()
1081 .cloned()
1082 .collect::<Vec<_>>())
1083 }
1084 async fn handle_flow_inserts(
1085 &self,
1086 request: api::v1::region::InsertRequests,
1087 ) -> Result<(), Error> {
1088 self.handle_inserts_inner(request).await
1089 }
1090 async fn handle_mark_window_dirty(
1091 &self,
1092 req: api::v1::flow::DirtyWindowRequests,
1093 ) -> Result<(), Error> {
1094 self.handle_mark_dirty_time_window(req).await
1095 }
1096}
1097
1098#[cfg(test)]
1099mod tests {
1100 use api::v1::flow::{DirtyWindowRequest, TimeRange};
1101 use catalog::RegisterTableRequest;
1102 use catalog::memory::{MemoryCatalogManager, new_memory_catalog_manager};
1103 use common_meta::key::TableMetadataManager;
1104 use common_meta::key::flow::FlowMetadataManager;
1105 use common_meta::key::table_route::TableRouteValue;
1106 use common_meta::key::test_utils::new_test_table_info_with_name;
1107 use common_meta::kv_backend::memory::MemoryKvBackend;
1108 use common_recordbatch::RecordBatch;
1109 use common_time::timestamp::TimeUnit;
1110 use datatypes::schema::{ColumnSchema, Schema};
1111 use datatypes::vectors::{TimestampMillisecondVector, UInt32Vector, VectorRef};
1112 use query::options::QueryOptions;
1113 use session::context::QueryContext;
1114 use tokio::sync::Notify;
1115
1116 use super::*;
1117 use crate::ExecuteOnceOutcome;
1118 use crate::test_utils::create_test_query_engine;
1119
1120 struct DropNotify(Option<oneshot::Sender<()>>);
1121
1122 impl Drop for DropNotify {
1123 fn drop(&mut self) {
1124 if let Some(tx) = self.0.take() {
1125 let _ = tx.send(());
1126 }
1127 }
1128 }
1129
1130 async fn new_test_engine() -> BatchingEngine {
1131 let kv_backend = Arc::new(MemoryKvBackend::new());
1132 let table_meta = Arc::new(TableMetadataManager::new(kv_backend.clone()));
1133 table_meta.init().await.unwrap();
1134 let flow_meta = Arc::new(FlowMetadataManager::new(kv_backend));
1135 let catalog_manager = new_memory_catalog_manager().unwrap();
1136 let query_engine = create_test_query_engine();
1137 let (frontend_client, _handler) =
1138 FrontendClient::from_empty_grpc_handler(QueryOptions::default());
1139
1140 BatchingEngine::new(
1141 Arc::new(frontend_client),
1142 query_engine,
1143 flow_meta,
1144 table_meta,
1145 catalog_manager,
1146 BatchingModeOptions::default(),
1147 )
1148 }
1149
1150 async fn new_test_engine_with_execution(
1151 execution_factory: Option<Arc<dyn crate::BatchingExecutionFactory>>,
1152 ) -> BatchingEngine {
1153 let kv_backend = Arc::new(MemoryKvBackend::new());
1154 let table_meta = Arc::new(TableMetadataManager::new(kv_backend.clone()));
1155 table_meta.init().await.unwrap();
1156 let flow_meta = Arc::new(FlowMetadataManager::new(kv_backend));
1157 let query_engine = create_test_query_engine();
1158 let catalog_manager = query_engine.engine_state().catalog_manager().clone();
1159 let (frontend_client, _handler) =
1160 FrontendClient::from_empty_grpc_handler(QueryOptions::default());
1161
1162 let engine = BatchingEngine::new_with_execution(
1163 Arc::new(frontend_client),
1164 query_engine,
1165 flow_meta,
1166 table_meta,
1167 catalog_manager,
1168 BatchingModeOptions::default(),
1169 execution_factory,
1170 );
1171 engine
1172 .table_meta
1173 .create_table_metadata(
1174 new_test_table_info_with_name(1, "numbers_with_ts"),
1175 TableRouteValue::physical(vec![]),
1176 HashMap::new(),
1177 )
1178 .await
1179 .unwrap();
1180 engine
1181 }
1182
1183 fn register_sink_with_schema(engine: &BatchingEngine, name: &str) {
1184 let schema = Arc::new(Schema::new(vec![
1185 ColumnSchema::new("number", ConcreteDataType::uint32_datatype(), false),
1186 ColumnSchema::new(
1187 "ts",
1188 ConcreteDataType::timestamp_millisecond_datatype(),
1189 false,
1190 )
1191 .with_time_index(true),
1192 ]));
1193 let recordbatch = RecordBatch::new(
1194 schema,
1195 vec![
1196 Arc::new(UInt32Vector::from_slice([1_u32])) as VectorRef,
1197 Arc::new(TimestampMillisecondVector::from_slice([0_i64])) as VectorRef,
1198 ],
1199 )
1200 .unwrap();
1201 engine
1202 .catalog_manager
1203 .as_any()
1204 .downcast_ref::<MemoryCatalogManager>()
1205 .unwrap()
1206 .register_table_sync(RegisterTableRequest {
1207 catalog: "greptime".to_string(),
1208 schema: "public".to_string(),
1209 table_name: name.to_string(),
1210 table_id: 9000,
1211 table: table::test_util::MemTable::table(name, recordbatch),
1212 })
1213 .unwrap();
1214 }
1215
1216 fn register_number_only_sink(engine: &BatchingEngine, name: &str) {
1217 let schema = Arc::new(Schema::new(vec![ColumnSchema::new(
1218 "number",
1219 ConcreteDataType::uint32_datatype(),
1220 false,
1221 )]));
1222 let recordbatch = RecordBatch::new(
1223 schema,
1224 vec![Arc::new(UInt32Vector::from_slice([1_u32])) as VectorRef],
1225 )
1226 .unwrap();
1227 engine
1228 .catalog_manager
1229 .as_any()
1230 .downcast_ref::<MemoryCatalogManager>()
1231 .unwrap()
1232 .register_table_sync(RegisterTableRequest {
1233 catalog: "greptime".to_string(),
1234 schema: "public".to_string(),
1235 table_name: name.to_string(),
1236 table_id: 9001,
1237 table: table::test_util::MemTable::table(name, recordbatch),
1238 })
1239 .unwrap();
1240 }
1241
1242 fn flow_create_args(flow_id: FlowId, sink: &str) -> CreateFlowArgs {
1243 CreateFlowArgs {
1244 flow_id,
1245 sink_table_name: [
1246 "greptime".to_string(),
1247 "public".to_string(),
1248 sink.to_string(),
1249 ],
1250 source_table_ids: vec![1],
1251 create_if_not_exists: false,
1252 or_replace: false,
1253 expire_after: None,
1254 eval_interval: Some(10),
1255 comment: None,
1256 sql: "SELECT number, ts FROM numbers_with_ts".to_string(),
1257 flow_options: HashMap::new(),
1258 query_ctx: Some(QueryContext::arc().as_ref().clone()),
1259 eval_schedule: None,
1260 }
1261 }
1262
1263 struct TestExecution {
1264 manual_calls: std::sync::atomic::AtomicUsize,
1265 stops: std::sync::atomic::AtomicUsize,
1266 }
1267
1268 #[async_trait::async_trait]
1269 impl crate::BatchingExecution for TestExecution {
1270 async fn execute_once(
1271 self: Arc<Self>,
1272 _guard: crate::BatchingExecutionGuard,
1273 task: &BatchingTask,
1274 _engine: &QueryEngineRef,
1275 _frontend: &Arc<FrontendClient>,
1276 _max_window_cnt: Option<usize>,
1277 ) -> ExecuteOnceOutcome {
1278 if task
1279 .state
1280 .read()
1281 .unwrap()
1282 .query_ctx
1283 .extension(query::options::FLOW_SCHEDULED_TIME_MILLIS)
1284 .is_none()
1285 {
1286 self.manual_calls
1287 .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1288 }
1289 ExecuteOnceOutcome {
1290 new_query: None,
1291 result: Ok(None),
1292 }
1293 }
1294
1295 fn stop(&self) {
1296 self.stops.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1297 }
1298 }
1299
1300 type TestExecutionResult = crate::Result<Option<Arc<dyn crate::BatchingExecution>>>;
1301
1302 struct TestExecutionFactory {
1303 entered: Option<Arc<Notify>>,
1304 release: Option<Arc<Notify>>,
1305 result: std::sync::Mutex<Option<TestExecutionResult>>,
1306 }
1307
1308 #[async_trait::async_trait]
1309 impl crate::BatchingExecutionFactory for TestExecutionFactory {
1310 async fn create(
1311 &self,
1312 _task: &BatchingTask,
1313 _sink: table::TableRef,
1314 _engine: &QueryEngineRef,
1315 _frontend: &Arc<FrontendClient>,
1316 ) -> crate::Result<Option<Arc<dyn crate::BatchingExecution>>> {
1317 if let Some(entered) = &self.entered {
1318 entered.notify_one();
1319 }
1320 if let Some(release) = &self.release {
1321 release.notified().await;
1322 }
1323 self.result
1324 .lock()
1325 .unwrap()
1326 .take()
1327 .expect("execution factory should only be called once")
1328 }
1329 }
1330
1331 #[tokio::test]
1332 async fn test_execution_factory_finishes_before_task_publication() {
1333 const GATE_TIMEOUT: Duration = Duration::from_secs(1);
1334
1335 let execution = Arc::new(TestExecution {
1336 manual_calls: Default::default(),
1337 stops: Default::default(),
1338 });
1339 let entered = Arc::new(Notify::new());
1340 let release = Arc::new(Notify::new());
1341 let factory = Arc::new(TestExecutionFactory {
1342 entered: Some(entered.clone()),
1343 release: Some(release.clone()),
1344 result: std::sync::Mutex::new(Some(Ok(Some(execution.clone())))),
1345 });
1346 let engine = Arc::new(new_test_engine_with_execution(Some(factory)).await);
1347 register_sink_with_schema(&engine, "factory_sink");
1348
1349 let entered_wait = entered.notified();
1350 let mut args = flow_create_args(6, "factory_sink");
1351 args.eval_interval = Some(86_400);
1352 let mut create = tokio::spawn({
1353 let engine = engine.clone();
1354 async move { engine.create_flow_inner(args).await }
1355 });
1356 if tokio::time::timeout(GATE_TIMEOUT, entered_wait)
1357 .await
1358 .is_err()
1359 {
1360 release.notify_one();
1361 if tokio::time::timeout(GATE_TIMEOUT, &mut create)
1362 .await
1363 .is_err()
1364 {
1365 create.abort();
1366 }
1367 panic!("execution factory should be entered");
1368 }
1369 let unpublished = {
1370 let runtime = engine.runtime.read().await;
1371 !runtime.tasks.contains_key(&6) && !runtime.shutdown_txs.contains_key(&6)
1372 };
1373
1374 release.notify_one();
1375 assert!(unpublished);
1376 let created = tokio::time::timeout(GATE_TIMEOUT, &mut create)
1377 .await
1378 .expect("flow creation should finish after factory release");
1379 created.unwrap().unwrap();
1380 assert!(engine.flow_exist_inner(6).await);
1381 let calls_before_flush = execution
1382 .manual_calls
1383 .load(std::sync::atomic::Ordering::SeqCst);
1384 assert_eq!(engine.flush_flow_inner(6).await.unwrap(), 0);
1385 assert_eq!(
1386 execution
1387 .manual_calls
1388 .load(std::sync::atomic::Ordering::SeqCst),
1389 calls_before_flush + 1
1390 );
1391 engine.remove_flow_inner(6).await.unwrap();
1392 }
1393
1394 #[tokio::test]
1395 async fn test_execution_factory_result_controls_sink_validation_and_publication() {
1396 let no_factory_engine = new_test_engine_with_execution(None).await;
1397 register_number_only_sink(&no_factory_engine, "no_factory_sink");
1398 assert!(
1399 no_factory_engine
1400 .create_flow_inner(flow_create_args(7, "no_factory_sink"))
1401 .await
1402 .is_err()
1403 );
1404 assert!(!no_factory_engine.flow_exist_inner(7).await);
1405
1406 let declined_engine =
1407 new_test_engine_with_execution(Some(Arc::new(TestExecutionFactory {
1408 entered: None,
1409 release: None,
1410 result: std::sync::Mutex::new(Some(Ok(None))),
1411 })))
1412 .await;
1413 register_number_only_sink(&declined_engine, "declined_factory_sink");
1414 assert!(
1415 declined_engine
1416 .create_flow_inner(flow_create_args(8, "declined_factory_sink"))
1417 .await
1418 .is_err()
1419 );
1420 assert!(!declined_engine.flow_exist_inner(8).await);
1421
1422 let accepted_engine =
1423 new_test_engine_with_execution(Some(Arc::new(TestExecutionFactory {
1424 entered: None,
1425 release: None,
1426 result: std::sync::Mutex::new(Some(Ok(Some(Arc::new(TestExecution {
1427 manual_calls: Default::default(),
1428 stops: Default::default(),
1429 }))))),
1430 })))
1431 .await;
1432 register_number_only_sink(&accepted_engine, "accepted_factory_sink");
1433 accepted_engine
1434 .create_flow_inner(flow_create_args(9, "accepted_factory_sink"))
1435 .await
1436 .unwrap();
1437 assert!(accepted_engine.flow_exist_inner(9).await);
1438 accepted_engine.remove_flow_inner(9).await.unwrap();
1439
1440 let error_engine = new_test_engine_with_execution(Some(Arc::new(TestExecutionFactory {
1441 entered: None,
1442 release: None,
1443 result: std::sync::Mutex::new(Some(
1444 UnexpectedSnafu {
1445 reason: "test execution factory failure".to_string(),
1446 }
1447 .fail(),
1448 )),
1449 })))
1450 .await;
1451 register_sink_with_schema(&error_engine, "error_factory_sink");
1452 assert!(
1453 error_engine
1454 .create_flow_inner(flow_create_args(10, "error_factory_sink"))
1455 .await
1456 .is_err()
1457 );
1458 assert!(!error_engine.flow_exist_inner(10).await);
1459 }
1460
1461 #[tokio::test]
1462 async fn test_flow_option_overrides_incremental_read_switch() {
1463 let engine = new_test_engine().await;
1464
1465 let default_opts = engine
1466 .batch_opts_for_flow_options(&HashMap::new(), false)
1467 .unwrap();
1468 assert!(!default_opts.experimental_enable_incremental_read);
1469
1470 let enabled_opts = engine
1471 .batch_opts_for_flow_options(
1472 &HashMap::from([(
1473 FLOW_EXPERIMENTAL_ENABLE_INCREMENTAL_READ_KEY.to_string(),
1474 "true".to_string(),
1475 )]),
1476 false,
1477 )
1478 .unwrap();
1479 assert!(enabled_opts.experimental_enable_incremental_read);
1480
1481 let exact_opts = engine
1482 .batch_opts_for_flow_options(
1483 &HashMap::from([(
1484 FLOW_EXPERIMENTAL_ENABLE_INCREMENTAL_READ_KEY.to_string(),
1485 FLOW_EXPERIMENTAL_ENABLE_INCREMENTAL_READ_SEQUENCE_RANGE.to_string(),
1486 )]),
1487 true,
1488 )
1489 .unwrap();
1490 assert!(exact_opts.experimental_enable_incremental_read);
1491 }
1492
1493 #[test]
1494 fn test_table_options_enable_append_mode() {
1495 assert!(!BatchingEngine::table_options_enable_append_mode(
1496 &HashMap::new()
1497 ));
1498 assert!(!BatchingEngine::table_options_enable_append_mode(
1499 &HashMap::from([(APPEND_MODE_KEY.to_string(), "false".to_string())])
1500 ));
1501 assert!(BatchingEngine::table_options_enable_append_mode(
1502 &HashMap::from([(APPEND_MODE_KEY.to_string(), "TRUE".to_string())])
1503 ));
1504 }
1505
1506 #[test]
1507 fn test_sql_flow_requires_time_window_or_eval_interval() {
1508 BatchingEngine::ensure_sql_flow_has_twe_or_eval_interval(None, true)
1509 .expect("SQL flow with a time-window expression should be accepted");
1510 BatchingEngine::ensure_sql_flow_has_twe_or_eval_interval(Some(10), false).expect(
1511 "SQL flow with EVAL INTERVAL should be accepted as an explicit full-query flow",
1512 );
1513
1514 let err = BatchingEngine::ensure_sql_flow_has_twe_or_eval_interval(None, false)
1515 .expect_err("SQL flow without a time-window expression or EVAL INTERVAL should fail");
1516 assert!(matches!(err, Error::InvalidQuery { .. }), "{err}");
1517 assert!(
1518 err.to_string().contains("must specify EVAL INTERVAL"),
1519 "{err}"
1520 );
1521 }
1522
1523 #[tokio::test]
1524 async fn test_complex_sql_without_eval_interval_is_rejected_as_no_twe() {
1525 let query_engine = create_test_query_engine();
1526 let ctx = QueryContext::arc();
1527 let plan = sql_to_df_plan(
1528 ctx.clone(),
1529 query_engine.clone(),
1530 r#"
1531SELECT
1532 l.number,
1533 date_bin('5 minutes', l.ts) AS time_window
1534FROM numbers_with_ts l
1535JOIN numbers_with_ts r ON l.number = r.number
1536GROUP BY l.number, time_window
1537"#,
1538 true,
1539 )
1540 .await
1541 .unwrap();
1542
1543 let (_, time_window_expr, _, _) = find_time_window_expr(
1544 &plan,
1545 query_engine.engine_state().catalog_manager().clone(),
1546 ctx,
1547 )
1548 .await
1549 .unwrap();
1550 assert!(
1551 time_window_expr.is_none(),
1552 "complex SQL should be classified as having no safe TWE"
1553 );
1554
1555 BatchingEngine::ensure_sql_flow_has_twe_or_eval_interval(Some(10), false)
1556 .expect("complex SQL can run as an explicit full-query flow when EVAL INTERVAL is set");
1557 let err = BatchingEngine::ensure_sql_flow_has_twe_or_eval_interval(None, false)
1558 .expect_err("complex SQL without EVAL INTERVAL should fail creation");
1559 assert!(matches!(err, Error::InvalidQuery { .. }), "{err}");
1560 }
1561
1562 #[test]
1563 fn test_incremental_source_append_only_enforcement() {
1564 let table_name = [
1565 "greptime".to_string(),
1566 "public".to_string(),
1567 "numbers".to_string(),
1568 ];
1569 let disabled_opts = BatchingModeOptions::default();
1570 let enabled_opts = BatchingModeOptions {
1571 experimental_enable_incremental_read: true,
1572 ..Default::default()
1573 };
1574 let non_append_options = HashMap::new();
1575 let append_options = HashMap::from([(APPEND_MODE_KEY.to_string(), "true".to_string())]);
1576
1577 BatchingEngine::ensure_incremental_source_append_only(
1578 &disabled_opts,
1579 &table_name,
1580 &non_append_options,
1581 )
1582 .expect("disabled incremental read should not require append-only source");
1583 BatchingEngine::ensure_incremental_source_append_only(
1584 &enabled_opts,
1585 &table_name,
1586 &append_options,
1587 )
1588 .expect("append-only source should be accepted when incremental read is enabled");
1589
1590 let err = BatchingEngine::ensure_incremental_source_append_only(
1591 &enabled_opts,
1592 &table_name,
1593 &non_append_options,
1594 )
1595 .expect_err("non-append source should be rejected when incremental read is enabled");
1596 assert!(
1597 err.to_string()
1598 .contains("Flow incremental read requires append-only source table"),
1599 "{err}"
1600 );
1601 }
1602
1603 async fn new_test_task(flow_id: FlowId) -> (BatchingTask, oneshot::Sender<()>) {
1604 new_test_task_for_source(flow_id, "numbers_with_ts", None).await
1605 }
1606
1607 async fn new_test_task_with_time_window_expr(
1608 flow_id: FlowId,
1609 time_window_expr: Option<TimeWindowExpr>,
1610 ) -> (BatchingTask, oneshot::Sender<()>) {
1611 new_test_task_for_source(flow_id, "numbers_with_ts", time_window_expr).await
1612 }
1613
1614 fn test_table_info_with_ts_unit(
1615 table_id: TableId,
1616 table_name: &str,
1617 unit: TimeUnit,
1618 ) -> table::metadata::TableInfo {
1619 use datatypes::schema::{ColumnSchema, SchemaBuilder};
1620 use table::metadata::{TableInfoBuilder, TableMetaBuilder};
1621
1622 let ts_type = match unit {
1623 TimeUnit::Second => ConcreteDataType::timestamp_second_datatype(),
1624 TimeUnit::Millisecond => ConcreteDataType::timestamp_millisecond_datatype(),
1625 TimeUnit::Microsecond => ConcreteDataType::timestamp_microsecond_datatype(),
1626 TimeUnit::Nanosecond => ConcreteDataType::timestamp_nanosecond_datatype(),
1627 };
1628 let column_schemas = vec![
1629 ColumnSchema::new("col1", ConcreteDataType::int32_datatype(), true),
1630 ColumnSchema::new("ts", ts_type, false).with_time_index(true),
1631 ];
1632 let schema = SchemaBuilder::try_from(column_schemas)
1633 .unwrap()
1634 .build()
1635 .unwrap();
1636 let meta = TableMetaBuilder::empty()
1637 .schema(Arc::new(schema))
1638 .primary_key_indices(vec![0])
1639 .engine("engine")
1640 .next_column_id(3)
1641 .build()
1642 .unwrap();
1643 TableInfoBuilder::default()
1644 .table_id(table_id)
1645 .table_version(0)
1646 .name(table_name)
1647 .catalog_name("greptime")
1648 .schema_name("public")
1649 .meta(meta)
1650 .build()
1651 .unwrap()
1652 }
1653
1654 async fn test_time_window_expr() -> TimeWindowExpr {
1656 let query_engine = create_test_query_engine();
1657 let ctx = QueryContext::arc();
1658 let plan = sql_to_df_plan(
1659 ctx.clone(),
1660 query_engine.clone(),
1661 "SELECT date_bin(INTERVAL '5 second', ts) AS time_window FROM numbers_with_ts GROUP BY time_window",
1662 true,
1663 )
1664 .await
1665 .unwrap();
1666 let (column_name, time_window_expr, _, df_schema) = find_time_window_expr(
1667 &plan,
1668 query_engine.engine_state().catalog_manager().clone(),
1669 ctx,
1670 )
1671 .await
1672 .unwrap();
1673 TimeWindowExpr::from_expr(
1674 &time_window_expr.unwrap(),
1675 &column_name,
1676 &df_schema,
1677 &query_engine.engine_state().session_state(),
1678 )
1679 .unwrap()
1680 }
1681
1682 async fn new_test_task_for_source(
1683 flow_id: FlowId,
1684 source_table_name: &str,
1685 time_window_expr: Option<TimeWindowExpr>,
1686 ) -> (BatchingTask, oneshot::Sender<()>) {
1687 let query_engine = create_test_query_engine();
1688 let ctx = QueryContext::arc();
1689 let plan = sql_to_df_plan(
1690 ctx.clone(),
1691 query_engine.clone(),
1692 "SELECT number, ts FROM numbers_with_ts",
1693 true,
1694 )
1695 .await
1696 .unwrap();
1697 let (tx, rx) = oneshot::channel();
1698
1699 let task = BatchingTask::try_new(TaskArgs {
1700 flow_id,
1701 query: "SELECT number, ts FROM numbers_with_ts",
1702 plan,
1703 time_window_expr,
1704 expire_after: None,
1705 sink_table_name: [
1706 "greptime".to_string(),
1707 "public".to_string(),
1708 "sink".to_string(),
1709 ],
1710 source_table_names: vec![[
1711 "greptime".to_string(),
1712 "public".to_string(),
1713 source_table_name.to_string(),
1714 ]],
1715 query_ctx: ctx,
1716 catalog_manager: query_engine.engine_state().catalog_manager().clone(),
1717 shutdown_rx: rx,
1718 batch_opts: Arc::new(BatchingModeOptions::default()),
1719 flow_eval_interval: None,
1720 eval_schedule: None,
1721 })
1722 .unwrap();
1723
1724 (task, tx)
1725 }
1726
1727 #[tokio::test]
1728 async fn test_handle_mark_dirty_time_window_with_time_ranges() {
1729 let engine = new_test_engine().await;
1730
1731 let mut table_info = new_test_table_info_with_name(1, "numbers_with_ts");
1734 table_info.catalog_name = "greptime".to_string();
1735 table_info.schema_name = "public".to_string();
1736 engine
1737 .table_meta
1738 .create_table_metadata(
1739 table_info,
1740 TableRouteValue::physical(vec![]),
1741 HashMap::new(),
1742 )
1743 .await
1744 .unwrap();
1745
1746 let (task, shutdown_tx) =
1748 new_test_task_with_time_window_expr(1, Some(test_time_window_expr().await)).await;
1749 let task_identity = task.clone();
1750 engine.runtime.write().await.insert(1, task, shutdown_tx);
1751
1752 engine
1753 .handle_mark_dirty_time_window(DirtyWindowRequests {
1754 requests: vec![DirtyWindowRequest {
1755 table_id: 1,
1756 timestamps: vec![],
1757 time_ranges: vec![
1758 TimeRange {
1760 start_inclusive: 3_000,
1761 end_exclusive: 11_000,
1762 },
1763 TimeRange {
1765 start_inclusive: 5_000,
1766 end_exclusive: 5_000,
1767 },
1768 TimeRange {
1769 start_inclusive: 9_000,
1770 end_exclusive: 4_000,
1771 },
1772 ],
1773 }],
1774 })
1775 .await
1776 .unwrap();
1777
1778 let state = task_identity.state.read().unwrap();
1779 assert_eq!(1, state.dirty_time_windows.len());
1780 assert_eq!(
1781 Duration::from_secs(15),
1782 state.dirty_time_windows.window_size()
1783 );
1784 }
1785
1786 #[tokio::test]
1791 async fn test_handle_mark_dirty_time_window_time_index_units() {
1792 let engine = new_test_engine().await;
1793
1794 let cases = [
1795 (TimeUnit::Second, 1u32, "t_sec", 3i64, 11i64),
1796 (TimeUnit::Millisecond, 2, "t_ms", 3_000, 11_000),
1797 (TimeUnit::Microsecond, 3, "t_us", 3_000_000, 11_000_000),
1798 (
1799 TimeUnit::Nanosecond,
1800 4,
1801 "t_ns",
1802 3_000_000_000,
1803 11_000_000_000,
1804 ),
1805 ];
1806
1807 let mut task_identities = vec![];
1808 let mut requests = vec![];
1809 for (unit, table_id, table_name, start_inclusive, end_exclusive) in cases {
1810 engine
1811 .table_meta
1812 .create_table_metadata(
1813 test_table_info_with_ts_unit(table_id, table_name, unit),
1814 TableRouteValue::physical(vec![]),
1815 HashMap::new(),
1816 )
1817 .await
1818 .unwrap();
1819
1820 let (task, shutdown_tx) = new_test_task_for_source(
1821 table_id as FlowId,
1822 table_name,
1823 Some(test_time_window_expr().await),
1824 )
1825 .await;
1826 task_identities.push((table_id, task.clone()));
1827 engine
1828 .runtime
1829 .write()
1830 .await
1831 .insert(table_id as FlowId, task, shutdown_tx);
1832
1833 requests.push(DirtyWindowRequest {
1834 table_id,
1835 timestamps: vec![],
1836 time_ranges: vec![TimeRange {
1837 start_inclusive,
1838 end_exclusive,
1839 }],
1840 });
1841 }
1842
1843 engine
1844 .handle_mark_dirty_time_window(DirtyWindowRequests { requests })
1845 .await
1846 .unwrap();
1847
1848 for (table_id, task) in task_identities {
1849 let state = task.state.read().unwrap();
1850 assert_eq!(1, state.dirty_time_windows.len(), "table id = {table_id}");
1851 assert_eq!(
1852 Duration::from_secs(15),
1853 state.dirty_time_windows.window_size(),
1854 "table id = {table_id}"
1855 );
1856 }
1857 }
1858
1859 #[tokio::test]
1860 async fn test_handle_mark_dirty_time_window_returns_error_on_alignment_failure() {
1861 let engine = new_test_engine().await;
1862 let table_id = 10;
1863 let table_name = "t_bad_timestamp";
1864
1865 engine
1866 .table_meta
1867 .create_table_metadata(
1868 test_table_info_with_ts_unit(table_id, table_name, TimeUnit::Second),
1869 TableRouteValue::physical(vec![]),
1870 HashMap::new(),
1871 )
1872 .await
1873 .unwrap();
1874
1875 let (task, shutdown_tx) = new_test_task_for_source(
1876 table_id as FlowId,
1877 table_name,
1878 Some(test_time_window_expr().await),
1879 )
1880 .await;
1881 engine
1882 .runtime
1883 .write()
1884 .await
1885 .insert(table_id as FlowId, task, shutdown_tx);
1886
1887 let result = engine
1888 .handle_mark_dirty_time_window(DirtyWindowRequests {
1889 requests: vec![DirtyWindowRequest {
1890 table_id,
1891 timestamps: vec![i64::MAX],
1892 time_ranges: vec![],
1893 }],
1894 })
1895 .await;
1896
1897 assert!(
1898 result.is_err(),
1899 "invalid timestamp alignment should be returned to the caller"
1900 );
1901 }
1902
1903 async fn install_abort_observed_handle(task: &BatchingTask) -> oneshot::Receiver<()> {
1904 let (drop_tx, drop_rx) = oneshot::channel();
1905 let (entered_tx, entered_rx) = oneshot::channel();
1906 let handle = tokio::spawn(async move {
1907 let _guard = DropNotify(Some(drop_tx));
1908 let _ = entered_tx.send(());
1909 std::future::pending::<()>().await;
1910 });
1911 task.state.write().unwrap().task_handle = Some(handle);
1912 tokio::time::timeout(Duration::from_secs(1), entered_rx)
1913 .await
1914 .expect("test task handle should start")
1915 .expect("test task handle should report start");
1916 drop_rx
1917 }
1918
1919 #[tokio::test]
1920 async fn test_notify_flow_shutdown_sends_signal() {
1921 let (tx, rx) = oneshot::channel();
1922
1923 assert!(notify_flow_shutdown(42, Some(tx), "test"));
1924
1925 rx.await.expect("replaced flow should receive shutdown");
1926 }
1927
1928 #[test]
1929 fn test_notify_flow_shutdown_accepts_missing_sender() {
1930 assert!(!notify_flow_shutdown(42, None, "test"));
1931 }
1932
1933 #[tokio::test]
1934 async fn test_abort_flow_task_stops_execution_without_loop_handle() {
1935 let (task, _shutdown_tx) = new_test_task(42).await;
1936 let execution = Arc::new(TestExecution {
1937 manual_calls: Default::default(),
1938 stops: Default::default(),
1939 });
1940 let task = task.with_execution(Some(execution.clone()));
1941
1942 assert!(!abort_flow_task(42, Some(task), "test"));
1943 assert_eq!(execution.stops.load(std::sync::atomic::Ordering::SeqCst), 1);
1944 }
1945
1946 #[tokio::test]
1947 async fn test_abort_flow_task_stops_execution_before_aborting_handle() {
1948 let (task, _shutdown_tx) = new_test_task(42).await;
1949 let execution = Arc::new(TestExecution {
1950 manual_calls: Default::default(),
1951 stops: Default::default(),
1952 });
1953 let task = task.with_execution(Some(execution.clone()));
1954 let drop_rx = install_abort_observed_handle(&task).await;
1955
1956 assert!(abort_flow_task(42, Some(task), "test"));
1957 assert_eq!(execution.stops.load(std::sync::atomic::Ordering::SeqCst), 1);
1958
1959 tokio::time::timeout(Duration::from_secs(1), drop_rx)
1960 .await
1961 .expect("aborted task should be dropped")
1962 .expect("drop notifier should fire");
1963 }
1964
1965 #[tokio::test]
1966 async fn test_remove_flow_inner_aborts_registered_task() {
1967 let engine = new_test_engine().await;
1968 let (task, shutdown_tx) = new_test_task(42).await;
1969 let drop_rx = install_abort_observed_handle(&task).await;
1970
1971 engine.runtime.write().await.insert(42, task, shutdown_tx);
1972
1973 engine.remove_flow_inner(42).await.unwrap();
1974
1975 tokio::time::timeout(Duration::from_secs(1), drop_rx)
1976 .await
1977 .expect("removed task should be dropped")
1978 .expect("drop notifier should fire");
1979 assert!(!engine.flow_exist_inner(42).await);
1980 assert!(!engine.runtime.read().await.shutdown_txs.contains_key(&42));
1981 }
1982
1983 #[tokio::test]
1984 async fn test_or_replace_flow_runtime_replaces_old_handles_and_keeps_new_task() {
1985 let engine = new_test_engine().await;
1986 let (old_task, old_shutdown_tx) = new_test_task(42).await;
1987 let old_task_identity = old_task.clone();
1988 let old_drop_rx = install_abort_observed_handle(&old_task).await;
1989 let (new_task, new_shutdown_tx) = new_test_task(42).await;
1990 let new_task_identity = new_task.clone();
1991
1992 engine
1993 .runtime
1994 .write()
1995 .await
1996 .insert(42, old_task, old_shutdown_tx);
1997 let (replaced_old_task, replaced_old_shutdown_tx) =
1998 engine
1999 .runtime
2000 .write()
2001 .await
2002 .insert(42, new_task, new_shutdown_tx);
2003
2004 let replaced_old_task = replaced_old_task.expect("old task should be returned");
2005 assert!(Arc::ptr_eq(
2006 &replaced_old_task.state,
2007 &old_task_identity.state
2008 ));
2009 assert!(notify_flow_shutdown(
2010 42,
2011 replaced_old_shutdown_tx,
2012 "replaced"
2013 ));
2014 old_task_identity
2015 .state
2016 .write()
2017 .unwrap()
2018 .shutdown_rx
2019 .try_recv()
2020 .expect("old shutdown receiver should receive signal");
2021 assert!(abort_flow_task(42, Some(replaced_old_task), "replaced"));
2022
2023 tokio::time::timeout(Duration::from_secs(1), old_drop_rx)
2024 .await
2025 .expect("replaced task should be dropped")
2026 .expect("drop notifier should fire");
2027
2028 let runtime = engine.runtime.read().await;
2029 assert_eq!(1, runtime.tasks.len());
2030 assert_eq!(1, runtime.shutdown_txs.len());
2031 let registered_task = runtime.tasks.get(&42).expect("new task should remain");
2032 assert!(Arc::ptr_eq(
2033 ®istered_task.state,
2034 &new_task_identity.state
2035 ));
2036 assert!(runtime.shutdown_txs.contains_key(&42));
2037 assert!(matches!(
2038 new_task_identity
2039 .state
2040 .write()
2041 .unwrap()
2042 .shutdown_rx
2043 .try_recv(),
2044 Err(oneshot::error::TryRecvError::Empty)
2045 ));
2046 }
2047
2048 #[tokio::test]
2049 async fn test_rollback_flow_runtime_if_current_removes_matching_task_only() {
2050 let engine = new_test_engine().await;
2051 let (old_task, _old_shutdown_tx) = new_test_task(42).await;
2052 let (current_task, current_shutdown_tx) = new_test_task(42).await;
2053 let current_task_identity = current_task.clone();
2054
2055 engine
2056 .runtime
2057 .write()
2058 .await
2059 .insert(42, current_task, current_shutdown_tx);
2060
2061 engine.rollback_flow_runtime_if_current(42, &old_task).await;
2062
2063 let registered_task = engine.runtime.read().await.tasks.get(&42).cloned().unwrap();
2064 assert!(Arc::ptr_eq(
2065 ®istered_task.state,
2066 ¤t_task_identity.state
2067 ));
2068 assert!(engine.runtime.read().await.shutdown_txs.contains_key(&42));
2069
2070 engine
2071 .rollback_flow_runtime_if_current(42, ¤t_task_identity)
2072 .await;
2073 assert!(!engine.flow_exist_inner(42).await);
2074 assert!(!engine.runtime.read().await.shutdown_txs.contains_key(&42));
2075 }
2076}