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::{FLOW_EXPERIMENTAL_ENABLE_INCREMENTAL_READ_KEY, FlowType};
25use common_meta::key::TableMetadataManagerRef;
26use common_meta::key::flow::FlowMetadataManagerRef;
27use common_meta::key::flow::flow_state::FlowStat;
28use common_meta::key::table_info::{TableInfoManager, TableInfoValue};
29use common_runtime::JoinHandle;
30use common_telemetry::tracing::warn;
31use common_telemetry::{debug, info};
32use common_time::TimeToLive;
33use datafusion_common::tree_node::{TreeNodeRecursion, TreeNodeVisitor};
34use datafusion_expr::LogicalPlan;
35use datatypes::prelude::ConcreteDataType;
36use query::QueryEngineRef;
37use session::context::QueryContext;
38use snafu::{OptionExt, ResultExt, ensure};
39use sql::parsers::utils::is_tql;
40use store_api::metric_engine_consts::is_metric_engine_internal_column;
41use store_api::mito_engine_options::APPEND_MODE_KEY;
42use store_api::storage::{RegionId, TableId};
43use table::table_reference::TableReference;
44use tokio::sync::{RwLock, oneshot};
45
46use crate::batching_mode::BatchingModeOptions;
47use crate::batching_mode::eval_schedule::EvalSchedule;
48use crate::batching_mode::frontend_client::FrontendClient;
49use crate::batching_mode::state::DirtyTimeWindows;
50use crate::batching_mode::task::{BatchingTask, TaskArgs};
51use crate::batching_mode::time_window::{TimeWindowExpr, find_time_window_expr};
52use crate::batching_mode::utils::sql_to_df_plan;
53use crate::engine::{FlowEngine, FlowStatProvider};
54use crate::error::{
55 CreateFlowSnafu, DatafusionSnafu, ExternalSnafu, FlowAlreadyExistSnafu, FlowNotFoundSnafu,
56 InvalidQuerySnafu, JoinTaskSnafu, TableNotFoundMetaSnafu, UnexpectedSnafu, UnsupportedSnafu,
57};
58use crate::metrics::METRIC_FLOW_BATCHING_ENGINE_BULK_MARK_TIME_WINDOW;
59use crate::{CreateFlowArgs, Error, FlowId, TableName};
60
61pub struct BatchingEngine {
65 runtime: RwLock<FlowRuntimeRegistry>,
66 pub(crate) frontend_client: Arc<FrontendClient>,
68 flow_metadata_manager: FlowMetadataManagerRef,
69 table_meta: TableMetadataManagerRef,
70 catalog_manager: CatalogManagerRef,
71 query_engine: QueryEngineRef,
72 pub(crate) batch_opts: Arc<BatchingModeOptions>,
75}
76
77#[derive(Default)]
78struct FlowRuntimeRegistry {
79 tasks: BTreeMap<FlowId, BatchingTask>,
80 shutdown_txs: BTreeMap<FlowId, oneshot::Sender<()>>,
81}
82
83impl FlowRuntimeRegistry {
84 fn insert(
85 &mut self,
86 flow_id: FlowId,
87 task: BatchingTask,
88 shutdown_tx: oneshot::Sender<()>,
89 ) -> (Option<BatchingTask>, Option<oneshot::Sender<()>>) {
90 (
91 self.tasks.insert(flow_id, task),
92 self.shutdown_txs.insert(flow_id, shutdown_tx),
93 )
94 }
95
96 fn remove(&mut self, flow_id: FlowId) -> Option<(BatchingTask, Option<oneshot::Sender<()>>)> {
97 let task = self.tasks.remove(&flow_id)?;
98 let shutdown_tx = self.shutdown_txs.remove(&flow_id);
99 Some((task, shutdown_tx))
100 }
101
102 fn remove_if_current(
103 &mut self,
104 flow_id: FlowId,
105 task: &BatchingTask,
106 ) -> (Option<BatchingTask>, Option<oneshot::Sender<()>>) {
107 if self
108 .tasks
109 .get(&flow_id)
110 .is_some_and(|current| Arc::ptr_eq(¤t.state, &task.state))
111 {
112 let Some((removed_task, removed_shutdown_tx)) = self.remove(flow_id) else {
113 return (None, None);
114 };
115 (Some(removed_task), removed_shutdown_tx)
116 } else {
117 (None, None)
118 }
119 }
120}
121
122impl BatchingEngine {
123 pub fn new(
124 frontend_client: Arc<FrontendClient>,
125 query_engine: QueryEngineRef,
126 flow_metadata_manager: FlowMetadataManagerRef,
127 table_meta: TableMetadataManagerRef,
128 catalog_manager: CatalogManagerRef,
129 batch_opts: BatchingModeOptions,
130 ) -> Self {
131 Self {
132 runtime: Default::default(),
133 frontend_client,
134 flow_metadata_manager,
135 table_meta,
136 catalog_manager,
137 query_engine,
138 batch_opts: Arc::new(batch_opts),
139 }
140 }
141
142 pub async fn get_last_exec_time_map(&self) -> BTreeMap<FlowId, i64> {
144 let runtime = self.runtime.read().await;
145 runtime
146 .tasks
147 .iter()
148 .filter_map(|(flow_id, task)| {
149 task.last_execution_time_millis()
150 .map(|timestamp| (*flow_id, timestamp))
151 })
152 .collect()
153 }
154
155 pub async fn handle_mark_dirty_time_window(
161 &self,
162 reqs: DirtyWindowRequests,
163 ) -> Result<(), Error> {
164 let table_info_mgr = self.table_meta.table_info_manager();
165
166 let mut group_by_table_id: HashMap<u32, (Vec<i64>, Vec<api::v1::flow::TimeRange>)> =
167 HashMap::new();
168 for r in reqs.requests {
169 let tid = TableId::from(r.table_id);
170 let entry = group_by_table_id.entry(tid).or_default();
171 entry.0.extend(r.timestamps);
172 entry.1.extend(r.time_ranges);
173 }
174 let tids = group_by_table_id.keys().cloned().collect::<Vec<TableId>>();
175 let table_infos =
176 table_info_mgr
177 .batch_get(&tids)
178 .await
179 .with_context(|_| TableNotFoundMetaSnafu {
180 msg: format!("Failed to get table info for table ids: {:?}", tids),
181 })?;
182
183 let group_by_table_name = group_by_table_id
184 .into_iter()
185 .filter_map(|(id, (timestamps, time_ranges))| {
186 let table_name = table_infos.get(&id).map(|info| info.table_name());
187 let Some(table_name) = table_name else {
188 warn!("Failed to get table infos for table id: {:?}", id);
189 return None;
190 };
191 let table_name = [
192 table_name.catalog_name,
193 table_name.schema_name,
194 table_name.table_name,
195 ];
196 let schema = &table_infos.get(&id).unwrap().table_info.meta.schema;
197 let time_index_unit = schema.column_schemas()[schema.timestamp_index().unwrap()]
198 .data_type
199 .as_timestamp()
200 .unwrap()
201 .unit();
202 Some((table_name, (timestamps, time_ranges, time_index_unit)))
203 })
204 .collect::<HashMap<_, _>>();
205
206 let group_by_table_name = Arc::new(group_by_table_name);
207
208 let tasks = self
209 .runtime
210 .read()
211 .await
212 .tasks
213 .values()
214 .cloned()
215 .collect::<Vec<_>>();
216 let mut handles = Vec::new();
217
218 for task in tasks {
219 let src_table_names = &task.config.source_table_names;
220
221 if src_table_names
222 .iter()
223 .all(|name| !group_by_table_name.contains_key(name))
224 {
225 continue;
226 }
227
228 let group_by_table_name = group_by_table_name.clone();
229 let task = task.clone();
230 let handle: JoinHandle<Result<(), Error>> = tokio::spawn(async move {
231 let src_table_names = &task.config.source_table_names;
232 let mut all_dirty_windows = HashSet::new();
233 let mut all_dirty_ranges = Vec::new();
234 let mut is_dirty = false;
235 for src_table_name in src_table_names {
236 if let Some((timestamps, time_ranges, unit)) =
237 group_by_table_name.get(src_table_name)
238 {
239 let Some(expr) = &task.config.time_window_expr else {
240 is_dirty = true;
241 continue;
242 };
243 for timestamp in timestamps {
244 let align_start = expr
245 .eval(common_time::Timestamp::new(*timestamp, *unit))?
246 .0
247 .context(UnexpectedSnafu {
248 reason: format!(
249 "Failed to align dirty timestamp {timestamp}: missing window lower bound"
250 ),
251 })?;
252 all_dirty_windows.insert(align_start);
253 }
254 for time_range in time_ranges {
255 if time_range.end_exclusive <= time_range.start_inclusive {
256 warn!(
257 "Ignoring invalid dirty time range with start_inclusive={} >= end_exclusive={}",
258 time_range.start_inclusive, time_range.end_exclusive
259 );
260 continue;
261 }
262 let (align_start, align_end) = DirtyTimeWindows::align_time_window(
263 common_time::Timestamp::new(time_range.start_inclusive, *unit),
264 Some(common_time::Timestamp::new(time_range.end_exclusive, *unit)),
265 expr,
266 )?;
267 all_dirty_ranges.push((align_start, align_end));
268 }
269 }
270 }
271 let mut state = task.state.write().unwrap();
272 if is_dirty {
273 state.dirty_time_windows.set_dirty();
274 }
275 let flow_id_label = task.config.flow_id.to_string();
276 for timestamp in all_dirty_windows {
277 state.dirty_time_windows.add_window(timestamp, None);
278 }
279 for (start, end) in all_dirty_ranges {
280 state.dirty_time_windows.add_window(start, end);
281 }
282
283 METRIC_FLOW_BATCHING_ENGINE_BULK_MARK_TIME_WINDOW
284 .with_label_values(&[&flow_id_label])
285 .set(state.dirty_time_windows.len() as f64);
286 Ok(())
287 });
288 handles.push(handle);
289 }
290 for handle in handles {
291 handle.await.context(JoinTaskSnafu)??;
292 }
293
294 Ok(())
295 }
296
297 pub async fn handle_inserts_inner(
298 &self,
299 request: api::v1::region::InsertRequests,
300 ) -> Result<(), Error> {
301 let table_info_mgr = self.table_meta.table_info_manager();
302 let mut group_by_table_id: HashMap<TableId, Vec<api::v1::Rows>> = HashMap::new();
303
304 for r in request.requests {
305 let tid = RegionId::from(r.region_id).table_id();
306 let entry = group_by_table_id.entry(tid).or_default();
307 if let Some(rows) = r.rows {
308 entry.push(rows);
309 }
310 }
311
312 let tids = group_by_table_id.keys().cloned().collect::<Vec<TableId>>();
313 let table_infos =
314 table_info_mgr
315 .batch_get(&tids)
316 .await
317 .with_context(|_| TableNotFoundMetaSnafu {
318 msg: format!("Failed to get table info for table ids: {:?}", tids),
319 })?;
320
321 let missing_tids = tids
322 .iter()
323 .filter(|id| !table_infos.contains_key(id))
324 .collect::<Vec<_>>();
325 if !missing_tids.is_empty() {
326 warn!(
327 "Failed to get all the table info for table ids, expected table ids: {:?}, those table doesn't exist: {:?}",
328 tids, missing_tids
329 );
330 }
331
332 let group_by_table_name = group_by_table_id
333 .into_iter()
334 .filter_map(|(id, rows)| {
335 let table_name = table_infos.get(&id).map(|info| info.table_name());
336 let Some(table_name) = table_name else {
337 warn!("Failed to get table infos for table id: {:?}", id);
338 return None;
339 };
340 let table_name = [
341 table_name.catalog_name,
342 table_name.schema_name,
343 table_name.table_name,
344 ];
345 Some((table_name, rows))
346 })
347 .collect::<HashMap<_, _>>();
348
349 let group_by_table_name = Arc::new(group_by_table_name);
350
351 let tasks = self
352 .runtime
353 .read()
354 .await
355 .tasks
356 .values()
357 .cloned()
358 .collect::<Vec<_>>();
359 let mut handles = Vec::new();
360 for task in tasks {
361 let src_table_names = &task.config.source_table_names;
362
363 if src_table_names
364 .iter()
365 .all(|name| !group_by_table_name.contains_key(name))
366 {
367 continue;
368 }
369
370 let group_by_table_name = group_by_table_name.clone();
371 let task = task.clone();
372
373 let handle: JoinHandle<Result<(), Error>> = tokio::spawn(async move {
374 let src_table_names = &task.config.source_table_names;
375
376 let mut is_dirty = false;
377
378 for src_table_name in src_table_names {
379 if let Some(entry) = group_by_table_name.get(src_table_name) {
380 let Some(expr) = &task.config.time_window_expr else {
381 is_dirty = true;
382 continue;
383 };
384 let involved_time_windows = expr.handle_rows(entry.clone()).await?;
385 let mut state = task.state.write().unwrap();
386 state
387 .dirty_time_windows
388 .add_lower_bounds(involved_time_windows.into_iter());
389 }
390 }
391 if is_dirty {
392 task.state.write().unwrap().dirty_time_windows.set_dirty();
393 }
394
395 Ok(())
396 });
397 handles.push(handle);
398 }
399
400 for handle in handles {
401 match handle.await {
402 Err(e) => {
403 warn!("Failed to handle inserts: {e}");
404 }
405 Ok(Ok(())) => (),
406 Ok(Err(e)) => {
407 warn!("Failed to handle inserts: {e}");
408 }
409 }
410 }
411 Ok(())
412 }
413}
414
415impl FlowStatProvider for BatchingEngine {
416 async fn flow_stat(&self) -> FlowStat {
417 let runtime = self.runtime.read().await;
418 let mut last_exec_time_map = BTreeMap::new();
419 let mut start_time_map = BTreeMap::new();
420
421 for (flow_id, task) in runtime.tasks.iter() {
422 let id = *flow_id as u32;
423 if let Some(ts) = task.last_execution_time_millis() {
424 last_exec_time_map.insert(id, ts);
425 }
426 if let Some(ts) = task.start_time_millis() {
427 start_time_map.insert(id, ts);
428 }
429 }
430
431 FlowStat {
432 state_size: BTreeMap::new(),
433 last_exec_time_map,
434 start_time_map,
435 }
436 }
437}
438
439async fn get_table_name(
440 table_info: &TableInfoManager,
441 table_id: &TableId,
442) -> Result<TableName, Error> {
443 get_table_info(table_info, table_id).await.map(|info| {
444 let name = info.table_name();
445 [name.catalog_name, name.schema_name, name.table_name]
446 })
447}
448
449async fn get_table_info(
450 table_info: &TableInfoManager,
451 table_id: &TableId,
452) -> Result<TableInfoValue, Error> {
453 table_info
454 .get(*table_id)
455 .await
456 .map_err(BoxedError::new)
457 .context(ExternalSnafu)?
458 .with_context(|| UnexpectedSnafu {
459 reason: format!("Table id = {:?}, couldn't found table name", table_id),
460 })
461 .map(|info| info.into_inner())
462}
463
464impl BatchingEngine {
465 fn batch_opts_for_flow_options(
466 &self,
467 flow_options: &HashMap<String, String>,
468 ) -> Result<Arc<BatchingModeOptions>, Error> {
469 let mut batch_opts = (*self.batch_opts).clone();
470 if let Some(enable_incremental_read) =
471 flow_options.get(FLOW_EXPERIMENTAL_ENABLE_INCREMENTAL_READ_KEY)
472 {
473 batch_opts.experimental_enable_incremental_read = enable_incremental_read
474 .parse::<bool>()
475 .map_err(|_| {
476 InvalidQuerySnafu {
477 reason: format!(
478 "Invalid flow option {FLOW_EXPERIMENTAL_ENABLE_INCREMENTAL_READ_KEY}: {enable_incremental_read}"
479 ),
480 }
481 .build()
482 })?;
483 }
484
485 Ok(Arc::new(batch_opts))
486 }
487
488 fn table_options_enable_append_mode(extra_options: &HashMap<String, String>) -> bool {
489 extra_options
490 .get(APPEND_MODE_KEY)
491 .is_some_and(|value| value.eq_ignore_ascii_case("true"))
492 }
493
494 fn ensure_sql_flow_has_twe_or_eval_interval(
497 eval_interval: Option<i64>,
498 has_time_window_expr: bool,
499 ) -> Result<(), Error> {
500 ensure!(
501 eval_interval.is_some() || has_time_window_expr,
502 InvalidQuerySnafu {
503 reason: "SQL batching flow without a time-window expression must specify EVAL INTERVAL to run as an explicit full-query flow"
504 .to_string(),
505 }
506 );
507 Ok(())
508 }
509
510 fn ensure_incremental_source_append_only(
511 batch_opts: &BatchingModeOptions,
512 table_name: &[String; 3],
513 extra_options: &HashMap<String, String>,
514 ) -> Result<(), Error> {
515 if batch_opts.experimental_enable_incremental_read {
516 ensure!(
517 Self::table_options_enable_append_mode(extra_options),
518 UnsupportedSnafu {
519 reason: format!(
520 "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",
521 table_name.join(".")
522 ),
523 }
524 );
525 }
526
527 Ok(())
528 }
529
530 pub async fn create_flow_inner(&self, args: CreateFlowArgs) -> Result<Option<FlowId>, Error> {
531 let CreateFlowArgs {
532 flow_id,
533 sink_table_name,
534 source_table_ids,
535 create_if_not_exists,
536 or_replace,
537 expire_after,
538 eval_interval,
539 comment: _,
540 sql,
541 flow_options,
542 query_ctx,
543 eval_schedule: eval_schedule_config,
544 } = args;
545
546 {
548 let is_exist = self.runtime.read().await.tasks.contains_key(&flow_id);
549 match (create_if_not_exists, or_replace, is_exist) {
550 (_, true, true) => {
552 info!("Replacing flow with id={}", flow_id);
553 }
554 (false, false, true) => FlowAlreadyExistSnafu { id: flow_id }.fail()?,
555 (true, false, true) => {
557 info!("Flow with id={} already exists, do nothing", flow_id);
558 return Ok(None);
559 }
560
561 (_, _, false) => (),
563 }
564 }
565
566 let query_ctx = query_ctx.context({
567 UnexpectedSnafu {
568 reason: "Query context is None".to_string(),
569 }
570 })?;
571 let query_ctx = Arc::new(query_ctx);
572 let is_tql = is_tql(query_ctx.sql_dialect(), &sql)
573 .map_err(BoxedError::new)
574 .context(CreateFlowSnafu { sql: &sql })?;
575
576 if eval_interval.is_none() && is_tql {
578 InvalidQuerySnafu {
579 reason: "TQL query requires EVAL INTERVAL to be set".to_string(),
580 }
581 .fail()?;
582 }
583
584 let flow_type = flow_options.get(FlowType::FLOW_TYPE_KEY);
585
586 ensure!(
587 match flow_type {
588 None => true,
589 Some(ty) if ty == FlowType::BATCHING => true,
590 _ => false,
591 },
592 UnexpectedSnafu {
593 reason: format!("Flow type is not batching nor None, got {flow_type:?}")
594 }
595 );
596
597 let batch_opts = self.batch_opts_for_flow_options(&flow_options)?;
598
599 let mut source_table_names = Vec::with_capacity(2);
600 for src_id in source_table_ids {
601 let table_name = get_table_name(self.table_meta.table_info_manager(), &src_id).await?;
603 let table_info = get_table_info(self.table_meta.table_info_manager(), &src_id).await?;
604 ensure!(
605 table_info.table_info.meta.options.ttl != Some(TimeToLive::Instant),
606 UnsupportedSnafu {
607 reason: format!(
608 "Source table `{}`(id={}) has instant TTL, Instant TTL is not supported under batching mode. Consider using a TTL longer than flush interval",
609 table_name.join("."),
610 src_id
611 ),
612 }
613 );
614 Self::ensure_incremental_source_append_only(
615 &batch_opts,
616 &table_name,
617 &table_info.table_info.meta.options.extra_options,
618 )?;
619
620 source_table_names.push(table_name);
621 }
622
623 let (tx, rx) = oneshot::channel();
624
625 let plan = sql_to_df_plan(query_ctx.clone(), self.query_engine.clone(), &sql, true).await?;
626
627 if is_tql {
628 self.check_is_tql_table(&plan, &query_ctx).await?;
629 }
630
631 let phy_expr = if !is_tql {
632 let (column_name, time_window_expr, _, df_schema) = find_time_window_expr(
633 &plan,
634 self.query_engine.engine_state().catalog_manager().clone(),
635 query_ctx.clone(),
636 )
637 .await?;
638 time_window_expr
639 .map(|expr| {
640 TimeWindowExpr::from_expr(
641 &expr,
642 &column_name,
643 &df_schema,
644 &self.query_engine.engine_state().session_state(),
645 )
646 })
647 .transpose()?
648 } else {
649 None
651 };
652
653 debug!(
654 "Flow id={}, found time window expr={}",
655 flow_id,
656 phy_expr
657 .as_ref()
658 .map(|phy_expr| phy_expr.to_string())
659 .unwrap_or("None".to_string())
660 );
661
662 if !is_tql {
663 Self::ensure_sql_flow_has_twe_or_eval_interval(eval_interval, phy_expr.is_some())?;
664 }
665
666 let eval_schedule = {
668 let interval = eval_interval;
669 let config = eval_schedule_config.as_ref();
670 match EvalSchedule::from_config(interval, config) {
671 Ok(s) => s,
672 Err(e) => {
673 return UnexpectedSnafu {
674 reason: format!(
675 "Failed to build eval schedule for flow {}: {}",
676 flow_id, e
677 ),
678 }
679 .fail();
680 }
681 }
682 };
683
684 let task_args = TaskArgs {
685 flow_id,
686 query: &sql,
687 plan,
688 time_window_expr: phy_expr,
689 expire_after,
690 sink_table_name,
691 source_table_names,
692 query_ctx,
693 catalog_manager: self.catalog_manager.clone(),
694 shutdown_rx: rx,
695 batch_opts,
696 flow_eval_interval: eval_interval.map(|secs| Duration::from_secs(secs as u64)),
697 eval_schedule,
698 };
699
700 let task = BatchingTask::try_new(task_args)?;
701
702 let task_inner = task.clone();
703 let engine = self.query_engine.clone();
704 let frontend = self.frontend_client.clone();
705
706 task.check_or_create_sink_table(&engine, &frontend).await?;
710 task.validate_sink_table_schema(&engine).await?;
711
712 let (start_tx, start_rx) = oneshot::channel();
713
714 let handle = common_runtime::spawn_global(async move {
716 if start_rx.await.is_ok() {
717 task_inner.start_executing_loop(engine, frontend).await;
718 }
719 });
720 task.state.write().unwrap().task_handle = Some(handle);
721 let task_for_rollback = task.clone();
722
723 let (replaced_old_task_opt, replaced_old_shutdown_tx) = {
728 let mut runtime = self.runtime.write().await;
729
730 let is_exist = runtime.tasks.contains_key(&flow_id);
731 match (create_if_not_exists, or_replace, is_exist) {
732 (_, true, true) => {
733 info!(
734 "Replacing flow with id={} after final registry check",
735 flow_id
736 );
737 }
738 (false, false, true) => {
739 abort_flow_task(flow_id, Some(task), "unregistered");
740 return FlowAlreadyExistSnafu { id: flow_id }.fail();
741 }
742 (true, false, true) => {
743 info!(
744 "Flow with id={} already exists at final registry check, do nothing",
745 flow_id
746 );
747 abort_flow_task(flow_id, Some(task), "unregistered");
748 return Ok(None);
749 }
750 (_, _, false) => (),
751 }
752
753 runtime.insert(flow_id, task, tx)
754 };
755
756 notify_flow_shutdown(flow_id, replaced_old_shutdown_tx, "replaced");
757 abort_flow_task(flow_id, replaced_old_task_opt, "replaced");
758 if start_tx.send(()).is_err() {
759 self.rollback_flow_runtime_if_current(flow_id, &task_for_rollback)
760 .await;
761 UnexpectedSnafu {
762 reason: format!("Failed to start flow {flow_id} due to task already dropped"),
763 }
764 .fail()?;
765 }
766
767 Ok(Some(flow_id))
768 }
769
770 async fn check_is_tql_table(
771 &self,
772 query: &LogicalPlan,
773 query_ctx: &QueryContext,
774 ) -> Result<(), Error> {
775 struct CollectTableRef {
776 table_refs: HashSet<datafusion_common::TableReference>,
777 }
778
779 impl TreeNodeVisitor<'_> for CollectTableRef {
780 type Node = LogicalPlan;
781 fn f_down(
782 &mut self,
783 node: &Self::Node,
784 ) -> datafusion_common::Result<TreeNodeRecursion> {
785 if let LogicalPlan::TableScan(scan) = node {
786 self.table_refs.insert(scan.table_name.clone());
787 }
788 Ok(TreeNodeRecursion::Continue)
789 }
790 }
791 let mut table_refs = CollectTableRef {
792 table_refs: HashSet::new(),
793 };
794 query
795 .visit_with_subqueries(&mut table_refs)
796 .context(DatafusionSnafu {
797 context: "Checking if all source tables are TQL tables",
798 })?;
799
800 let default_catalog = query_ctx.current_catalog();
801 let default_schema = query_ctx.current_schema();
802 let default_schema = &default_schema;
803
804 for table_ref in table_refs.table_refs {
805 let table_ref = match &table_ref {
806 datafusion_common::TableReference::Bare { table } => {
807 TableReference::full(default_catalog, default_schema, table)
808 }
809 datafusion_common::TableReference::Partial { schema, table } => {
810 TableReference::full(default_catalog, schema, table)
811 }
812 datafusion_common::TableReference::Full {
813 catalog,
814 schema,
815 table,
816 } => TableReference::full(catalog, schema, table),
817 };
818
819 let table_id = self
820 .table_meta
821 .table_name_manager()
822 .get(table_ref.into())
823 .await
824 .map_err(BoxedError::new)
825 .context(ExternalSnafu)?
826 .with_context(|| UnexpectedSnafu {
827 reason: format!("Failed to get table id for table: {}", table_ref),
828 })?
829 .table_id();
830 let table_info =
831 get_table_info(self.table_meta.table_info_manager(), &table_id).await?;
832 let value_cols = table_info
834 .table_info
835 .meta
836 .schema
837 .column_schemas()
838 .iter()
839 .filter(|col| col.data_type == ConcreteDataType::float64_datatype())
840 .collect::<Vec<_>>();
841 ensure!(
842 value_cols.len() == 1,
843 InvalidQuerySnafu {
844 reason: format!(
845 "TQL query only supports one f64 value column, table `{}`(id={}) has {} f64 value columns, columns are: {:?}",
846 table_ref,
847 table_id,
848 value_cols.len(),
849 value_cols
850 ),
851 }
852 );
853 let pk_idxs = table_info
855 .table_info
856 .meta
857 .primary_key_indices
858 .iter()
859 .collect::<HashSet<_>>();
860
861 for (idx, col) in table_info
862 .table_info
863 .meta
864 .schema
865 .column_schemas()
866 .iter()
867 .enumerate()
868 {
869 if is_metric_engine_internal_column(&col.name) {
870 continue;
871 }
872 let is_pk: bool = pk_idxs.contains(&&idx);
878
879 ensure!(
880 col.data_type == ConcreteDataType::float64_datatype()
881 || col.data_type.is_timestamp()
882 || (col.data_type == ConcreteDataType::string_datatype() && is_pk),
883 InvalidQuerySnafu {
884 reason: format!(
885 "TQL query only supports f64 value column, timestamp column and string tag columns, table `{}`(id={}) has column `{}` with type {:?} which is not supported",
886 table_ref, table_id, col.name, col.data_type
887 ),
888 }
889 );
890 }
891 }
892 Ok(())
893 }
894
895 pub async fn remove_flow_inner(&self, flow_id: FlowId) -> Result<(), Error> {
896 let (task, shutdown_tx) = {
897 let mut runtime = self.runtime.write().await;
898 let Some((task, shutdown_tx)) = runtime.remove(flow_id) else {
899 warn!("Flow {flow_id} not found in tasks");
900 FlowNotFoundSnafu { id: flow_id }.fail()?
901 };
902 (task, shutdown_tx)
903 };
904
905 let had_shutdown_tx = notify_flow_shutdown(flow_id, shutdown_tx, "removed");
906 abort_flow_task(flow_id, Some(task), "removed");
907
908 if !had_shutdown_tx {
909 UnexpectedSnafu {
910 reason: format!("Can't found shutdown tx for flow {flow_id}"),
911 }
912 .fail()?
913 }
914
915 Ok(())
916 }
917
918 pub async fn flush_flow_inner(&self, flow_id: FlowId) -> Result<usize, Error> {
921 debug!("Try flush flow {flow_id}");
922 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
926 let task = self.runtime.read().await.tasks.get(&flow_id).cloned();
927 let task = task.with_context(|| FlowNotFoundSnafu { id: flow_id })?;
928
929 let time_window_size = task
930 .config
931 .time_window_expr
932 .as_ref()
933 .and_then(|expr| *expr.time_window_size());
934
935 let cur_dirty_window_cnt = time_window_size.map(|time_window_size| {
936 task.state
937 .read()
938 .unwrap()
939 .dirty_time_windows
940 .effective_count(&time_window_size)
941 });
942
943 let res = task
944 .execute_once_serialized(
945 &self.query_engine,
946 &self.frontend_client,
947 cur_dirty_window_cnt,
948 )
949 .await?;
950
951 let affected_rows = res.map(|(r, _)| r).unwrap_or_default();
952 debug!(
953 "Successfully flush flow {flow_id}, affected rows={}",
954 affected_rows
955 );
956 Ok(affected_rows)
957 }
958
959 pub async fn flow_exist_inner(&self, flow_id: FlowId) -> bool {
961 self.runtime.read().await.tasks.contains_key(&flow_id)
962 }
963
964 async fn rollback_flow_runtime_if_current(&self, flow_id: FlowId, task: &BatchingTask) {
965 let (removed_task, removed_shutdown_tx) = {
966 let mut runtime = self.runtime.write().await;
967 runtime.remove_if_current(flow_id, task)
968 };
969
970 notify_flow_shutdown(flow_id, removed_shutdown_tx, "rolled back");
971 abort_flow_task(flow_id, removed_task, "rolled back");
972 }
973}
974
975fn notify_flow_shutdown(flow_id: FlowId, tx: Option<oneshot::Sender<()>>, action: &str) -> bool {
976 let Some(tx) = tx else {
977 return false;
978 };
979
980 if tx.send(()).is_err() {
981 warn!(
982 "Fail to shutdown {action} flow {flow_id} due to receiver already dropped, maybe flow {flow_id} is already dropped?"
983 );
984 }
985
986 true
987}
988
989fn abort_flow_task(flow_id: FlowId, task: Option<BatchingTask>, action: &str) -> bool {
990 let Some(task) = task else {
991 return false;
992 };
993
994 if let Some(handle) = task.state.write().unwrap().task_handle.take() {
995 handle.abort();
996 debug!("Aborted {action} flow task {flow_id}");
997 return true;
998 }
999
1000 false
1001}
1002
1003impl FlowEngine for BatchingEngine {
1004 async fn create_flow(&self, args: CreateFlowArgs) -> Result<Option<FlowId>, Error> {
1005 self.create_flow_inner(args).await
1006 }
1007 async fn remove_flow(&self, flow_id: FlowId) -> Result<(), Error> {
1008 self.remove_flow_inner(flow_id).await
1009 }
1010 async fn flush_flow(&self, flow_id: FlowId) -> Result<usize, Error> {
1011 self.flush_flow_inner(flow_id).await
1012 }
1013 async fn flow_exist(&self, flow_id: FlowId) -> Result<bool, Error> {
1014 Ok(self.flow_exist_inner(flow_id).await)
1015 }
1016 async fn list_flows(&self) -> Result<impl IntoIterator<Item = FlowId>, Error> {
1017 Ok(self
1018 .runtime
1019 .read()
1020 .await
1021 .tasks
1022 .keys()
1023 .cloned()
1024 .collect::<Vec<_>>())
1025 }
1026 async fn handle_flow_inserts(
1027 &self,
1028 request: api::v1::region::InsertRequests,
1029 ) -> Result<(), Error> {
1030 self.handle_inserts_inner(request).await
1031 }
1032 async fn handle_mark_window_dirty(
1033 &self,
1034 req: api::v1::flow::DirtyWindowRequests,
1035 ) -> Result<(), Error> {
1036 self.handle_mark_dirty_time_window(req).await
1037 }
1038}
1039
1040#[cfg(test)]
1041mod tests {
1042 use api::v1::flow::{DirtyWindowRequest, TimeRange};
1043 use catalog::memory::new_memory_catalog_manager;
1044 use common_meta::key::TableMetadataManager;
1045 use common_meta::key::flow::FlowMetadataManager;
1046 use common_meta::key::table_route::TableRouteValue;
1047 use common_meta::key::test_utils::new_test_table_info_with_name;
1048 use common_meta::kv_backend::memory::MemoryKvBackend;
1049 use common_time::timestamp::TimeUnit;
1050 use query::options::QueryOptions;
1051 use session::context::QueryContext;
1052
1053 use super::*;
1054 use crate::test_utils::create_test_query_engine;
1055
1056 struct DropNotify(Option<oneshot::Sender<()>>);
1057
1058 impl Drop for DropNotify {
1059 fn drop(&mut self) {
1060 if let Some(tx) = self.0.take() {
1061 let _ = tx.send(());
1062 }
1063 }
1064 }
1065
1066 async fn new_test_engine() -> BatchingEngine {
1067 let kv_backend = Arc::new(MemoryKvBackend::new());
1068 let table_meta = Arc::new(TableMetadataManager::new(kv_backend.clone()));
1069 table_meta.init().await.unwrap();
1070 let flow_meta = Arc::new(FlowMetadataManager::new(kv_backend));
1071 let catalog_manager = new_memory_catalog_manager().unwrap();
1072 let query_engine = create_test_query_engine();
1073 let (frontend_client, _handler) =
1074 FrontendClient::from_empty_grpc_handler(QueryOptions::default());
1075
1076 BatchingEngine::new(
1077 Arc::new(frontend_client),
1078 query_engine,
1079 flow_meta,
1080 table_meta,
1081 catalog_manager,
1082 BatchingModeOptions::default(),
1083 )
1084 }
1085
1086 #[tokio::test]
1087 async fn test_flow_option_overrides_incremental_read_switch() {
1088 let engine = new_test_engine().await;
1089
1090 let default_opts = engine.batch_opts_for_flow_options(&HashMap::new()).unwrap();
1091 assert!(!default_opts.experimental_enable_incremental_read);
1092
1093 let enabled_opts = engine
1094 .batch_opts_for_flow_options(&HashMap::from([(
1095 FLOW_EXPERIMENTAL_ENABLE_INCREMENTAL_READ_KEY.to_string(),
1096 "true".to_string(),
1097 )]))
1098 .unwrap();
1099 assert!(enabled_opts.experimental_enable_incremental_read);
1100 }
1101
1102 #[test]
1103 fn test_table_options_enable_append_mode() {
1104 assert!(!BatchingEngine::table_options_enable_append_mode(
1105 &HashMap::new()
1106 ));
1107 assert!(!BatchingEngine::table_options_enable_append_mode(
1108 &HashMap::from([(APPEND_MODE_KEY.to_string(), "false".to_string())])
1109 ));
1110 assert!(BatchingEngine::table_options_enable_append_mode(
1111 &HashMap::from([(APPEND_MODE_KEY.to_string(), "TRUE".to_string())])
1112 ));
1113 }
1114
1115 #[test]
1116 fn test_sql_flow_requires_time_window_or_eval_interval() {
1117 BatchingEngine::ensure_sql_flow_has_twe_or_eval_interval(None, true)
1118 .expect("SQL flow with a time-window expression should be accepted");
1119 BatchingEngine::ensure_sql_flow_has_twe_or_eval_interval(Some(10), false).expect(
1120 "SQL flow with EVAL INTERVAL should be accepted as an explicit full-query flow",
1121 );
1122
1123 let err = BatchingEngine::ensure_sql_flow_has_twe_or_eval_interval(None, false)
1124 .expect_err("SQL flow without a time-window expression or EVAL INTERVAL should fail");
1125 assert!(matches!(err, Error::InvalidQuery { .. }), "{err}");
1126 assert!(
1127 err.to_string().contains("must specify EVAL INTERVAL"),
1128 "{err}"
1129 );
1130 }
1131
1132 #[tokio::test]
1133 async fn test_complex_sql_without_eval_interval_is_rejected_as_no_twe() {
1134 let query_engine = create_test_query_engine();
1135 let ctx = QueryContext::arc();
1136 let plan = sql_to_df_plan(
1137 ctx.clone(),
1138 query_engine.clone(),
1139 r#"
1140SELECT
1141 l.number,
1142 date_bin('5 minutes', l.ts) AS time_window
1143FROM numbers_with_ts l
1144JOIN numbers_with_ts r ON l.number = r.number
1145GROUP BY l.number, time_window
1146"#,
1147 true,
1148 )
1149 .await
1150 .unwrap();
1151
1152 let (_, time_window_expr, _, _) = find_time_window_expr(
1153 &plan,
1154 query_engine.engine_state().catalog_manager().clone(),
1155 ctx,
1156 )
1157 .await
1158 .unwrap();
1159 assert!(
1160 time_window_expr.is_none(),
1161 "complex SQL should be classified as having no safe TWE"
1162 );
1163
1164 BatchingEngine::ensure_sql_flow_has_twe_or_eval_interval(Some(10), false)
1165 .expect("complex SQL can run as an explicit full-query flow when EVAL INTERVAL is set");
1166 let err = BatchingEngine::ensure_sql_flow_has_twe_or_eval_interval(None, false)
1167 .expect_err("complex SQL without EVAL INTERVAL should fail creation");
1168 assert!(matches!(err, Error::InvalidQuery { .. }), "{err}");
1169 }
1170
1171 #[test]
1172 fn test_incremental_source_append_only_enforcement() {
1173 let table_name = [
1174 "greptime".to_string(),
1175 "public".to_string(),
1176 "numbers".to_string(),
1177 ];
1178 let disabled_opts = BatchingModeOptions::default();
1179 let enabled_opts = BatchingModeOptions {
1180 experimental_enable_incremental_read: true,
1181 ..Default::default()
1182 };
1183 let non_append_options = HashMap::new();
1184 let append_options = HashMap::from([(APPEND_MODE_KEY.to_string(), "true".to_string())]);
1185
1186 BatchingEngine::ensure_incremental_source_append_only(
1187 &disabled_opts,
1188 &table_name,
1189 &non_append_options,
1190 )
1191 .expect("disabled incremental read should not require append-only source");
1192 BatchingEngine::ensure_incremental_source_append_only(
1193 &enabled_opts,
1194 &table_name,
1195 &append_options,
1196 )
1197 .expect("append-only source should be accepted when incremental read is enabled");
1198
1199 let err = BatchingEngine::ensure_incremental_source_append_only(
1200 &enabled_opts,
1201 &table_name,
1202 &non_append_options,
1203 )
1204 .expect_err("non-append source should be rejected when incremental read is enabled");
1205 assert!(
1206 err.to_string()
1207 .contains("Flow incremental read requires append-only source table"),
1208 "{err}"
1209 );
1210 }
1211
1212 async fn new_test_task(flow_id: FlowId) -> (BatchingTask, oneshot::Sender<()>) {
1213 new_test_task_for_source(flow_id, "numbers_with_ts", None).await
1214 }
1215
1216 async fn new_test_task_with_time_window_expr(
1217 flow_id: FlowId,
1218 time_window_expr: Option<TimeWindowExpr>,
1219 ) -> (BatchingTask, oneshot::Sender<()>) {
1220 new_test_task_for_source(flow_id, "numbers_with_ts", time_window_expr).await
1221 }
1222
1223 fn test_table_info_with_ts_unit(
1224 table_id: TableId,
1225 table_name: &str,
1226 unit: TimeUnit,
1227 ) -> table::metadata::TableInfo {
1228 use datatypes::schema::{ColumnSchema, SchemaBuilder};
1229 use table::metadata::{TableInfoBuilder, TableMetaBuilder};
1230
1231 let ts_type = match unit {
1232 TimeUnit::Second => ConcreteDataType::timestamp_second_datatype(),
1233 TimeUnit::Millisecond => ConcreteDataType::timestamp_millisecond_datatype(),
1234 TimeUnit::Microsecond => ConcreteDataType::timestamp_microsecond_datatype(),
1235 TimeUnit::Nanosecond => ConcreteDataType::timestamp_nanosecond_datatype(),
1236 };
1237 let column_schemas = vec![
1238 ColumnSchema::new("col1", ConcreteDataType::int32_datatype(), true),
1239 ColumnSchema::new("ts", ts_type, false).with_time_index(true),
1240 ];
1241 let schema = SchemaBuilder::try_from(column_schemas)
1242 .unwrap()
1243 .build()
1244 .unwrap();
1245 let meta = TableMetaBuilder::empty()
1246 .schema(Arc::new(schema))
1247 .primary_key_indices(vec![0])
1248 .engine("engine")
1249 .next_column_id(3)
1250 .build()
1251 .unwrap();
1252 TableInfoBuilder::default()
1253 .table_id(table_id)
1254 .table_version(0)
1255 .name(table_name)
1256 .catalog_name("greptime")
1257 .schema_name("public")
1258 .meta(meta)
1259 .build()
1260 .unwrap()
1261 }
1262
1263 async fn test_time_window_expr() -> TimeWindowExpr {
1265 let query_engine = create_test_query_engine();
1266 let ctx = QueryContext::arc();
1267 let plan = sql_to_df_plan(
1268 ctx.clone(),
1269 query_engine.clone(),
1270 "SELECT date_bin(INTERVAL '5 second', ts) AS time_window FROM numbers_with_ts GROUP BY time_window",
1271 true,
1272 )
1273 .await
1274 .unwrap();
1275 let (column_name, time_window_expr, _, df_schema) = find_time_window_expr(
1276 &plan,
1277 query_engine.engine_state().catalog_manager().clone(),
1278 ctx,
1279 )
1280 .await
1281 .unwrap();
1282 TimeWindowExpr::from_expr(
1283 &time_window_expr.unwrap(),
1284 &column_name,
1285 &df_schema,
1286 &query_engine.engine_state().session_state(),
1287 )
1288 .unwrap()
1289 }
1290
1291 async fn new_test_task_for_source(
1292 flow_id: FlowId,
1293 source_table_name: &str,
1294 time_window_expr: Option<TimeWindowExpr>,
1295 ) -> (BatchingTask, oneshot::Sender<()>) {
1296 let query_engine = create_test_query_engine();
1297 let ctx = QueryContext::arc();
1298 let plan = sql_to_df_plan(
1299 ctx.clone(),
1300 query_engine.clone(),
1301 "SELECT number, ts FROM numbers_with_ts",
1302 true,
1303 )
1304 .await
1305 .unwrap();
1306 let (tx, rx) = oneshot::channel();
1307
1308 let task = BatchingTask::try_new(TaskArgs {
1309 flow_id,
1310 query: "SELECT number, ts FROM numbers_with_ts",
1311 plan,
1312 time_window_expr,
1313 expire_after: None,
1314 sink_table_name: [
1315 "greptime".to_string(),
1316 "public".to_string(),
1317 "sink".to_string(),
1318 ],
1319 source_table_names: vec![[
1320 "greptime".to_string(),
1321 "public".to_string(),
1322 source_table_name.to_string(),
1323 ]],
1324 query_ctx: ctx,
1325 catalog_manager: query_engine.engine_state().catalog_manager().clone(),
1326 shutdown_rx: rx,
1327 batch_opts: Arc::new(BatchingModeOptions::default()),
1328 flow_eval_interval: None,
1329 eval_schedule: None,
1330 })
1331 .unwrap();
1332
1333 (task, tx)
1334 }
1335
1336 #[tokio::test]
1337 async fn test_handle_mark_dirty_time_window_with_time_ranges() {
1338 let engine = new_test_engine().await;
1339
1340 let mut table_info = new_test_table_info_with_name(1, "numbers_with_ts");
1343 table_info.catalog_name = "greptime".to_string();
1344 table_info.schema_name = "public".to_string();
1345 engine
1346 .table_meta
1347 .create_table_metadata(
1348 table_info,
1349 TableRouteValue::physical(vec![]),
1350 HashMap::new(),
1351 )
1352 .await
1353 .unwrap();
1354
1355 let (task, shutdown_tx) =
1357 new_test_task_with_time_window_expr(1, Some(test_time_window_expr().await)).await;
1358 let task_identity = task.clone();
1359 engine.runtime.write().await.insert(1, task, shutdown_tx);
1360
1361 engine
1362 .handle_mark_dirty_time_window(DirtyWindowRequests {
1363 requests: vec![DirtyWindowRequest {
1364 table_id: 1,
1365 timestamps: vec![],
1366 time_ranges: vec![
1367 TimeRange {
1369 start_inclusive: 3_000,
1370 end_exclusive: 11_000,
1371 },
1372 TimeRange {
1374 start_inclusive: 5_000,
1375 end_exclusive: 5_000,
1376 },
1377 TimeRange {
1378 start_inclusive: 9_000,
1379 end_exclusive: 4_000,
1380 },
1381 ],
1382 }],
1383 })
1384 .await
1385 .unwrap();
1386
1387 let state = task_identity.state.read().unwrap();
1388 assert_eq!(1, state.dirty_time_windows.len());
1389 assert_eq!(
1390 Duration::from_secs(15),
1391 state.dirty_time_windows.window_size()
1392 );
1393 }
1394
1395 #[tokio::test]
1400 async fn test_handle_mark_dirty_time_window_time_index_units() {
1401 let engine = new_test_engine().await;
1402
1403 let cases = [
1404 (TimeUnit::Second, 1u32, "t_sec", 3i64, 11i64),
1405 (TimeUnit::Millisecond, 2, "t_ms", 3_000, 11_000),
1406 (TimeUnit::Microsecond, 3, "t_us", 3_000_000, 11_000_000),
1407 (
1408 TimeUnit::Nanosecond,
1409 4,
1410 "t_ns",
1411 3_000_000_000,
1412 11_000_000_000,
1413 ),
1414 ];
1415
1416 let mut task_identities = vec![];
1417 let mut requests = vec![];
1418 for (unit, table_id, table_name, start_inclusive, end_exclusive) in cases {
1419 engine
1420 .table_meta
1421 .create_table_metadata(
1422 test_table_info_with_ts_unit(table_id, table_name, unit),
1423 TableRouteValue::physical(vec![]),
1424 HashMap::new(),
1425 )
1426 .await
1427 .unwrap();
1428
1429 let (task, shutdown_tx) = new_test_task_for_source(
1430 table_id as FlowId,
1431 table_name,
1432 Some(test_time_window_expr().await),
1433 )
1434 .await;
1435 task_identities.push((table_id, task.clone()));
1436 engine
1437 .runtime
1438 .write()
1439 .await
1440 .insert(table_id as FlowId, task, shutdown_tx);
1441
1442 requests.push(DirtyWindowRequest {
1443 table_id,
1444 timestamps: vec![],
1445 time_ranges: vec![TimeRange {
1446 start_inclusive,
1447 end_exclusive,
1448 }],
1449 });
1450 }
1451
1452 engine
1453 .handle_mark_dirty_time_window(DirtyWindowRequests { requests })
1454 .await
1455 .unwrap();
1456
1457 for (table_id, task) in task_identities {
1458 let state = task.state.read().unwrap();
1459 assert_eq!(1, state.dirty_time_windows.len(), "table id = {table_id}");
1460 assert_eq!(
1461 Duration::from_secs(15),
1462 state.dirty_time_windows.window_size(),
1463 "table id = {table_id}"
1464 );
1465 }
1466 }
1467
1468 #[tokio::test]
1469 async fn test_handle_mark_dirty_time_window_returns_error_on_alignment_failure() {
1470 let engine = new_test_engine().await;
1471 let table_id = 10;
1472 let table_name = "t_bad_timestamp";
1473
1474 engine
1475 .table_meta
1476 .create_table_metadata(
1477 test_table_info_with_ts_unit(table_id, table_name, TimeUnit::Second),
1478 TableRouteValue::physical(vec![]),
1479 HashMap::new(),
1480 )
1481 .await
1482 .unwrap();
1483
1484 let (task, shutdown_tx) = new_test_task_for_source(
1485 table_id as FlowId,
1486 table_name,
1487 Some(test_time_window_expr().await),
1488 )
1489 .await;
1490 engine
1491 .runtime
1492 .write()
1493 .await
1494 .insert(table_id as FlowId, task, shutdown_tx);
1495
1496 let result = engine
1497 .handle_mark_dirty_time_window(DirtyWindowRequests {
1498 requests: vec![DirtyWindowRequest {
1499 table_id,
1500 timestamps: vec![i64::MAX],
1501 time_ranges: vec![],
1502 }],
1503 })
1504 .await;
1505
1506 assert!(
1507 result.is_err(),
1508 "invalid timestamp alignment should be returned to the caller"
1509 );
1510 }
1511
1512 async fn install_abort_observed_handle(task: &BatchingTask) -> oneshot::Receiver<()> {
1513 let (drop_tx, drop_rx) = oneshot::channel();
1514 let (entered_tx, entered_rx) = oneshot::channel();
1515 let handle = tokio::spawn(async move {
1516 let _guard = DropNotify(Some(drop_tx));
1517 let _ = entered_tx.send(());
1518 std::future::pending::<()>().await;
1519 });
1520 task.state.write().unwrap().task_handle = Some(handle);
1521 tokio::time::timeout(Duration::from_secs(1), entered_rx)
1522 .await
1523 .expect("test task handle should start")
1524 .expect("test task handle should report start");
1525 drop_rx
1526 }
1527
1528 #[tokio::test]
1529 async fn test_notify_flow_shutdown_sends_signal() {
1530 let (tx, rx) = oneshot::channel();
1531
1532 assert!(notify_flow_shutdown(42, Some(tx), "test"));
1533
1534 rx.await.expect("replaced flow should receive shutdown");
1535 }
1536
1537 #[test]
1538 fn test_notify_flow_shutdown_accepts_missing_sender() {
1539 assert!(!notify_flow_shutdown(42, None, "test"));
1540 }
1541
1542 #[tokio::test]
1543 async fn test_abort_flow_task_aborts_handle() {
1544 let (task, _shutdown_tx) = new_test_task(42).await;
1545 let drop_rx = install_abort_observed_handle(&task).await;
1546
1547 assert!(abort_flow_task(42, Some(task), "test"));
1548
1549 tokio::time::timeout(Duration::from_secs(1), drop_rx)
1550 .await
1551 .expect("aborted task should be dropped")
1552 .expect("drop notifier should fire");
1553 }
1554
1555 #[tokio::test]
1556 async fn test_remove_flow_inner_aborts_registered_task() {
1557 let engine = new_test_engine().await;
1558 let (task, shutdown_tx) = new_test_task(42).await;
1559 let drop_rx = install_abort_observed_handle(&task).await;
1560
1561 engine.runtime.write().await.insert(42, task, shutdown_tx);
1562
1563 engine.remove_flow_inner(42).await.unwrap();
1564
1565 tokio::time::timeout(Duration::from_secs(1), drop_rx)
1566 .await
1567 .expect("removed task should be dropped")
1568 .expect("drop notifier should fire");
1569 assert!(!engine.flow_exist_inner(42).await);
1570 assert!(!engine.runtime.read().await.shutdown_txs.contains_key(&42));
1571 }
1572
1573 #[tokio::test]
1574 async fn test_or_replace_flow_runtime_replaces_old_handles_and_keeps_new_task() {
1575 let engine = new_test_engine().await;
1576 let (old_task, old_shutdown_tx) = new_test_task(42).await;
1577 let old_task_identity = old_task.clone();
1578 let old_drop_rx = install_abort_observed_handle(&old_task).await;
1579 let (new_task, new_shutdown_tx) = new_test_task(42).await;
1580 let new_task_identity = new_task.clone();
1581
1582 engine
1583 .runtime
1584 .write()
1585 .await
1586 .insert(42, old_task, old_shutdown_tx);
1587 let (replaced_old_task, replaced_old_shutdown_tx) =
1588 engine
1589 .runtime
1590 .write()
1591 .await
1592 .insert(42, new_task, new_shutdown_tx);
1593
1594 let replaced_old_task = replaced_old_task.expect("old task should be returned");
1595 assert!(Arc::ptr_eq(
1596 &replaced_old_task.state,
1597 &old_task_identity.state
1598 ));
1599 assert!(notify_flow_shutdown(
1600 42,
1601 replaced_old_shutdown_tx,
1602 "replaced"
1603 ));
1604 old_task_identity
1605 .state
1606 .write()
1607 .unwrap()
1608 .shutdown_rx
1609 .try_recv()
1610 .expect("old shutdown receiver should receive signal");
1611 assert!(abort_flow_task(42, Some(replaced_old_task), "replaced"));
1612
1613 tokio::time::timeout(Duration::from_secs(1), old_drop_rx)
1614 .await
1615 .expect("replaced task should be dropped")
1616 .expect("drop notifier should fire");
1617
1618 let runtime = engine.runtime.read().await;
1619 assert_eq!(1, runtime.tasks.len());
1620 assert_eq!(1, runtime.shutdown_txs.len());
1621 let registered_task = runtime.tasks.get(&42).expect("new task should remain");
1622 assert!(Arc::ptr_eq(
1623 ®istered_task.state,
1624 &new_task_identity.state
1625 ));
1626 assert!(runtime.shutdown_txs.contains_key(&42));
1627 assert!(matches!(
1628 new_task_identity
1629 .state
1630 .write()
1631 .unwrap()
1632 .shutdown_rx
1633 .try_recv(),
1634 Err(oneshot::error::TryRecvError::Empty)
1635 ));
1636 }
1637
1638 #[tokio::test]
1639 async fn test_rollback_flow_runtime_if_current_removes_matching_task_only() {
1640 let engine = new_test_engine().await;
1641 let (old_task, _old_shutdown_tx) = new_test_task(42).await;
1642 let (current_task, current_shutdown_tx) = new_test_task(42).await;
1643 let current_task_identity = current_task.clone();
1644
1645 engine
1646 .runtime
1647 .write()
1648 .await
1649 .insert(42, current_task, current_shutdown_tx);
1650
1651 engine.rollback_flow_runtime_if_current(42, &old_task).await;
1652
1653 let registered_task = engine.runtime.read().await.tasks.get(&42).cloned().unwrap();
1654 assert!(Arc::ptr_eq(
1655 ®istered_task.state,
1656 ¤t_task_identity.state
1657 ));
1658 assert!(engine.runtime.read().await.shutdown_txs.contains_key(&42));
1659
1660 engine
1661 .rollback_flow_runtime_if_current(42, ¤t_task_identity)
1662 .await;
1663 assert!(!engine.flow_exist_inner(42).await);
1664 assert!(!engine.runtime.read().await.shutdown_txs.contains_key(&42));
1665 }
1666}