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 FlowStat {
418 state_size: BTreeMap::new(),
419 last_exec_time_map: self
420 .get_last_exec_time_map()
421 .await
422 .into_iter()
423 .map(|(flow_id, timestamp)| (flow_id as u32, timestamp))
424 .collect(),
425 }
426 }
427}
428
429async fn get_table_name(
430 table_info: &TableInfoManager,
431 table_id: &TableId,
432) -> Result<TableName, Error> {
433 get_table_info(table_info, table_id).await.map(|info| {
434 let name = info.table_name();
435 [name.catalog_name, name.schema_name, name.table_name]
436 })
437}
438
439async fn get_table_info(
440 table_info: &TableInfoManager,
441 table_id: &TableId,
442) -> Result<TableInfoValue, Error> {
443 table_info
444 .get(*table_id)
445 .await
446 .map_err(BoxedError::new)
447 .context(ExternalSnafu)?
448 .with_context(|| UnexpectedSnafu {
449 reason: format!("Table id = {:?}, couldn't found table name", table_id),
450 })
451 .map(|info| info.into_inner())
452}
453
454impl BatchingEngine {
455 fn batch_opts_for_flow_options(
456 &self,
457 flow_options: &HashMap<String, String>,
458 ) -> Result<Arc<BatchingModeOptions>, Error> {
459 let mut batch_opts = (*self.batch_opts).clone();
460 if let Some(enable_incremental_read) =
461 flow_options.get(FLOW_EXPERIMENTAL_ENABLE_INCREMENTAL_READ_KEY)
462 {
463 batch_opts.experimental_enable_incremental_read = enable_incremental_read
464 .parse::<bool>()
465 .map_err(|_| {
466 InvalidQuerySnafu {
467 reason: format!(
468 "Invalid flow option {FLOW_EXPERIMENTAL_ENABLE_INCREMENTAL_READ_KEY}: {enable_incremental_read}"
469 ),
470 }
471 .build()
472 })?;
473 }
474
475 Ok(Arc::new(batch_opts))
476 }
477
478 fn table_options_enable_append_mode(extra_options: &HashMap<String, String>) -> bool {
479 extra_options
480 .get(APPEND_MODE_KEY)
481 .is_some_and(|value| value.eq_ignore_ascii_case("true"))
482 }
483
484 fn ensure_sql_flow_has_twe_or_eval_interval(
487 eval_interval: Option<i64>,
488 has_time_window_expr: bool,
489 ) -> Result<(), Error> {
490 ensure!(
491 eval_interval.is_some() || has_time_window_expr,
492 InvalidQuerySnafu {
493 reason: "SQL batching flow without a time-window expression must specify EVAL INTERVAL to run as an explicit full-query flow"
494 .to_string(),
495 }
496 );
497 Ok(())
498 }
499
500 fn ensure_incremental_source_append_only(
501 batch_opts: &BatchingModeOptions,
502 table_name: &[String; 3],
503 extra_options: &HashMap<String, String>,
504 ) -> Result<(), Error> {
505 if batch_opts.experimental_enable_incremental_read {
506 ensure!(
507 Self::table_options_enable_append_mode(extra_options),
508 UnsupportedSnafu {
509 reason: format!(
510 "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",
511 table_name.join(".")
512 ),
513 }
514 );
515 }
516
517 Ok(())
518 }
519
520 pub async fn create_flow_inner(&self, args: CreateFlowArgs) -> Result<Option<FlowId>, Error> {
521 let CreateFlowArgs {
522 flow_id,
523 sink_table_name,
524 source_table_ids,
525 create_if_not_exists,
526 or_replace,
527 expire_after,
528 eval_interval,
529 comment: _,
530 sql,
531 flow_options,
532 query_ctx,
533 eval_schedule: eval_schedule_config,
534 } = args;
535
536 {
538 let is_exist = self.runtime.read().await.tasks.contains_key(&flow_id);
539 match (create_if_not_exists, or_replace, is_exist) {
540 (_, true, true) => {
542 info!("Replacing flow with id={}", flow_id);
543 }
544 (false, false, true) => FlowAlreadyExistSnafu { id: flow_id }.fail()?,
545 (true, false, true) => {
547 info!("Flow with id={} already exists, do nothing", flow_id);
548 return Ok(None);
549 }
550
551 (_, _, false) => (),
553 }
554 }
555
556 let query_ctx = query_ctx.context({
557 UnexpectedSnafu {
558 reason: "Query context is None".to_string(),
559 }
560 })?;
561 let query_ctx = Arc::new(query_ctx);
562 let is_tql = is_tql(query_ctx.sql_dialect(), &sql)
563 .map_err(BoxedError::new)
564 .context(CreateFlowSnafu { sql: &sql })?;
565
566 if eval_interval.is_none() && is_tql {
568 InvalidQuerySnafu {
569 reason: "TQL query requires EVAL INTERVAL to be set".to_string(),
570 }
571 .fail()?;
572 }
573
574 let flow_type = flow_options.get(FlowType::FLOW_TYPE_KEY);
575
576 ensure!(
577 match flow_type {
578 None => true,
579 Some(ty) if ty == FlowType::BATCHING => true,
580 _ => false,
581 },
582 UnexpectedSnafu {
583 reason: format!("Flow type is not batching nor None, got {flow_type:?}")
584 }
585 );
586
587 let batch_opts = self.batch_opts_for_flow_options(&flow_options)?;
588
589 let mut source_table_names = Vec::with_capacity(2);
590 for src_id in source_table_ids {
591 let table_name = get_table_name(self.table_meta.table_info_manager(), &src_id).await?;
593 let table_info = get_table_info(self.table_meta.table_info_manager(), &src_id).await?;
594 ensure!(
595 table_info.table_info.meta.options.ttl != Some(TimeToLive::Instant),
596 UnsupportedSnafu {
597 reason: format!(
598 "Source table `{}`(id={}) has instant TTL, Instant TTL is not supported under batching mode. Consider using a TTL longer than flush interval",
599 table_name.join("."),
600 src_id
601 ),
602 }
603 );
604 Self::ensure_incremental_source_append_only(
605 &batch_opts,
606 &table_name,
607 &table_info.table_info.meta.options.extra_options,
608 )?;
609
610 source_table_names.push(table_name);
611 }
612
613 let (tx, rx) = oneshot::channel();
614
615 let plan = sql_to_df_plan(query_ctx.clone(), self.query_engine.clone(), &sql, true).await?;
616
617 if is_tql {
618 self.check_is_tql_table(&plan, &query_ctx).await?;
619 }
620
621 let phy_expr = if !is_tql {
622 let (column_name, time_window_expr, _, df_schema) = find_time_window_expr(
623 &plan,
624 self.query_engine.engine_state().catalog_manager().clone(),
625 query_ctx.clone(),
626 )
627 .await?;
628 time_window_expr
629 .map(|expr| {
630 TimeWindowExpr::from_expr(
631 &expr,
632 &column_name,
633 &df_schema,
634 &self.query_engine.engine_state().session_state(),
635 )
636 })
637 .transpose()?
638 } else {
639 None
641 };
642
643 debug!(
644 "Flow id={}, found time window expr={}",
645 flow_id,
646 phy_expr
647 .as_ref()
648 .map(|phy_expr| phy_expr.to_string())
649 .unwrap_or("None".to_string())
650 );
651
652 if !is_tql {
653 Self::ensure_sql_flow_has_twe_or_eval_interval(eval_interval, phy_expr.is_some())?;
654 }
655
656 let eval_schedule = {
658 let interval = eval_interval;
659 let config = eval_schedule_config.as_ref();
660 match EvalSchedule::from_config(interval, config) {
661 Ok(s) => s,
662 Err(e) => {
663 return UnexpectedSnafu {
664 reason: format!(
665 "Failed to build eval schedule for flow {}: {}",
666 flow_id, e
667 ),
668 }
669 .fail();
670 }
671 }
672 };
673
674 let task_args = TaskArgs {
675 flow_id,
676 query: &sql,
677 plan,
678 time_window_expr: phy_expr,
679 expire_after,
680 sink_table_name,
681 source_table_names,
682 query_ctx,
683 catalog_manager: self.catalog_manager.clone(),
684 shutdown_rx: rx,
685 batch_opts,
686 flow_eval_interval: eval_interval.map(|secs| Duration::from_secs(secs as u64)),
687 eval_schedule,
688 };
689
690 let task = BatchingTask::try_new(task_args)?;
691
692 let task_inner = task.clone();
693 let engine = self.query_engine.clone();
694 let frontend = self.frontend_client.clone();
695
696 task.check_or_create_sink_table(&engine, &frontend).await?;
700 task.validate_sink_table_schema(&engine).await?;
701
702 let (start_tx, start_rx) = oneshot::channel();
703
704 let handle = common_runtime::spawn_global(async move {
706 if start_rx.await.is_ok() {
707 task_inner.start_executing_loop(engine, frontend).await;
708 }
709 });
710 task.state.write().unwrap().task_handle = Some(handle);
711 let task_for_rollback = task.clone();
712
713 let (replaced_old_task_opt, replaced_old_shutdown_tx) = {
718 let mut runtime = self.runtime.write().await;
719
720 let is_exist = runtime.tasks.contains_key(&flow_id);
721 match (create_if_not_exists, or_replace, is_exist) {
722 (_, true, true) => {
723 info!(
724 "Replacing flow with id={} after final registry check",
725 flow_id
726 );
727 }
728 (false, false, true) => {
729 abort_flow_task(flow_id, Some(task), "unregistered");
730 return FlowAlreadyExistSnafu { id: flow_id }.fail();
731 }
732 (true, false, true) => {
733 info!(
734 "Flow with id={} already exists at final registry check, do nothing",
735 flow_id
736 );
737 abort_flow_task(flow_id, Some(task), "unregistered");
738 return Ok(None);
739 }
740 (_, _, false) => (),
741 }
742
743 runtime.insert(flow_id, task, tx)
744 };
745
746 notify_flow_shutdown(flow_id, replaced_old_shutdown_tx, "replaced");
747 abort_flow_task(flow_id, replaced_old_task_opt, "replaced");
748 if start_tx.send(()).is_err() {
749 self.rollback_flow_runtime_if_current(flow_id, &task_for_rollback)
750 .await;
751 UnexpectedSnafu {
752 reason: format!("Failed to start flow {flow_id} due to task already dropped"),
753 }
754 .fail()?;
755 }
756
757 Ok(Some(flow_id))
758 }
759
760 async fn check_is_tql_table(
761 &self,
762 query: &LogicalPlan,
763 query_ctx: &QueryContext,
764 ) -> Result<(), Error> {
765 struct CollectTableRef {
766 table_refs: HashSet<datafusion_common::TableReference>,
767 }
768
769 impl TreeNodeVisitor<'_> for CollectTableRef {
770 type Node = LogicalPlan;
771 fn f_down(
772 &mut self,
773 node: &Self::Node,
774 ) -> datafusion_common::Result<TreeNodeRecursion> {
775 if let LogicalPlan::TableScan(scan) = node {
776 self.table_refs.insert(scan.table_name.clone());
777 }
778 Ok(TreeNodeRecursion::Continue)
779 }
780 }
781 let mut table_refs = CollectTableRef {
782 table_refs: HashSet::new(),
783 };
784 query
785 .visit_with_subqueries(&mut table_refs)
786 .context(DatafusionSnafu {
787 context: "Checking if all source tables are TQL tables",
788 })?;
789
790 let default_catalog = query_ctx.current_catalog();
791 let default_schema = query_ctx.current_schema();
792 let default_schema = &default_schema;
793
794 for table_ref in table_refs.table_refs {
795 let table_ref = match &table_ref {
796 datafusion_common::TableReference::Bare { table } => {
797 TableReference::full(default_catalog, default_schema, table)
798 }
799 datafusion_common::TableReference::Partial { schema, table } => {
800 TableReference::full(default_catalog, schema, table)
801 }
802 datafusion_common::TableReference::Full {
803 catalog,
804 schema,
805 table,
806 } => TableReference::full(catalog, schema, table),
807 };
808
809 let table_id = self
810 .table_meta
811 .table_name_manager()
812 .get(table_ref.into())
813 .await
814 .map_err(BoxedError::new)
815 .context(ExternalSnafu)?
816 .with_context(|| UnexpectedSnafu {
817 reason: format!("Failed to get table id for table: {}", table_ref),
818 })?
819 .table_id();
820 let table_info =
821 get_table_info(self.table_meta.table_info_manager(), &table_id).await?;
822 let value_cols = table_info
824 .table_info
825 .meta
826 .schema
827 .column_schemas()
828 .iter()
829 .filter(|col| col.data_type == ConcreteDataType::float64_datatype())
830 .collect::<Vec<_>>();
831 ensure!(
832 value_cols.len() == 1,
833 InvalidQuerySnafu {
834 reason: format!(
835 "TQL query only supports one f64 value column, table `{}`(id={}) has {} f64 value columns, columns are: {:?}",
836 table_ref,
837 table_id,
838 value_cols.len(),
839 value_cols
840 ),
841 }
842 );
843 let pk_idxs = table_info
845 .table_info
846 .meta
847 .primary_key_indices
848 .iter()
849 .collect::<HashSet<_>>();
850
851 for (idx, col) in table_info
852 .table_info
853 .meta
854 .schema
855 .column_schemas()
856 .iter()
857 .enumerate()
858 {
859 if is_metric_engine_internal_column(&col.name) {
860 continue;
861 }
862 let is_pk: bool = pk_idxs.contains(&&idx);
868
869 ensure!(
870 col.data_type == ConcreteDataType::float64_datatype()
871 || col.data_type.is_timestamp()
872 || (col.data_type == ConcreteDataType::string_datatype() && is_pk),
873 InvalidQuerySnafu {
874 reason: format!(
875 "TQL query only supports f64 value column, timestamp column and string tag columns, table `{}`(id={}) has column `{}` with type {:?} which is not supported",
876 table_ref, table_id, col.name, col.data_type
877 ),
878 }
879 );
880 }
881 }
882 Ok(())
883 }
884
885 pub async fn remove_flow_inner(&self, flow_id: FlowId) -> Result<(), Error> {
886 let (task, shutdown_tx) = {
887 let mut runtime = self.runtime.write().await;
888 let Some((task, shutdown_tx)) = runtime.remove(flow_id) else {
889 warn!("Flow {flow_id} not found in tasks");
890 FlowNotFoundSnafu { id: flow_id }.fail()?
891 };
892 (task, shutdown_tx)
893 };
894
895 let had_shutdown_tx = notify_flow_shutdown(flow_id, shutdown_tx, "removed");
896 abort_flow_task(flow_id, Some(task), "removed");
897
898 if !had_shutdown_tx {
899 UnexpectedSnafu {
900 reason: format!("Can't found shutdown tx for flow {flow_id}"),
901 }
902 .fail()?
903 }
904
905 Ok(())
906 }
907
908 pub async fn flush_flow_inner(&self, flow_id: FlowId) -> Result<usize, Error> {
911 debug!("Try flush flow {flow_id}");
912 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
916 let task = self.runtime.read().await.tasks.get(&flow_id).cloned();
917 let task = task.with_context(|| FlowNotFoundSnafu { id: flow_id })?;
918
919 let time_window_size = task
920 .config
921 .time_window_expr
922 .as_ref()
923 .and_then(|expr| *expr.time_window_size());
924
925 let cur_dirty_window_cnt = time_window_size.map(|time_window_size| {
926 task.state
927 .read()
928 .unwrap()
929 .dirty_time_windows
930 .effective_count(&time_window_size)
931 });
932
933 let res = task
934 .execute_once_serialized(
935 &self.query_engine,
936 &self.frontend_client,
937 cur_dirty_window_cnt,
938 )
939 .await?;
940
941 let affected_rows = res.map(|(r, _)| r).unwrap_or_default();
942 debug!(
943 "Successfully flush flow {flow_id}, affected rows={}",
944 affected_rows
945 );
946 Ok(affected_rows)
947 }
948
949 pub async fn flow_exist_inner(&self, flow_id: FlowId) -> bool {
951 self.runtime.read().await.tasks.contains_key(&flow_id)
952 }
953
954 async fn rollback_flow_runtime_if_current(&self, flow_id: FlowId, task: &BatchingTask) {
955 let (removed_task, removed_shutdown_tx) = {
956 let mut runtime = self.runtime.write().await;
957 runtime.remove_if_current(flow_id, task)
958 };
959
960 notify_flow_shutdown(flow_id, removed_shutdown_tx, "rolled back");
961 abort_flow_task(flow_id, removed_task, "rolled back");
962 }
963}
964
965fn notify_flow_shutdown(flow_id: FlowId, tx: Option<oneshot::Sender<()>>, action: &str) -> bool {
966 let Some(tx) = tx else {
967 return false;
968 };
969
970 if tx.send(()).is_err() {
971 warn!(
972 "Fail to shutdown {action} flow {flow_id} due to receiver already dropped, maybe flow {flow_id} is already dropped?"
973 );
974 }
975
976 true
977}
978
979fn abort_flow_task(flow_id: FlowId, task: Option<BatchingTask>, action: &str) -> bool {
980 let Some(task) = task else {
981 return false;
982 };
983
984 if let Some(handle) = task.state.write().unwrap().task_handle.take() {
985 handle.abort();
986 debug!("Aborted {action} flow task {flow_id}");
987 return true;
988 }
989
990 false
991}
992
993impl FlowEngine for BatchingEngine {
994 async fn create_flow(&self, args: CreateFlowArgs) -> Result<Option<FlowId>, Error> {
995 self.create_flow_inner(args).await
996 }
997 async fn remove_flow(&self, flow_id: FlowId) -> Result<(), Error> {
998 self.remove_flow_inner(flow_id).await
999 }
1000 async fn flush_flow(&self, flow_id: FlowId) -> Result<usize, Error> {
1001 self.flush_flow_inner(flow_id).await
1002 }
1003 async fn flow_exist(&self, flow_id: FlowId) -> Result<bool, Error> {
1004 Ok(self.flow_exist_inner(flow_id).await)
1005 }
1006 async fn list_flows(&self) -> Result<impl IntoIterator<Item = FlowId>, Error> {
1007 Ok(self
1008 .runtime
1009 .read()
1010 .await
1011 .tasks
1012 .keys()
1013 .cloned()
1014 .collect::<Vec<_>>())
1015 }
1016 async fn handle_flow_inserts(
1017 &self,
1018 request: api::v1::region::InsertRequests,
1019 ) -> Result<(), Error> {
1020 self.handle_inserts_inner(request).await
1021 }
1022 async fn handle_mark_window_dirty(
1023 &self,
1024 req: api::v1::flow::DirtyWindowRequests,
1025 ) -> Result<(), Error> {
1026 self.handle_mark_dirty_time_window(req).await
1027 }
1028}
1029
1030#[cfg(test)]
1031mod tests {
1032 use api::v1::flow::{DirtyWindowRequest, TimeRange};
1033 use catalog::memory::new_memory_catalog_manager;
1034 use common_meta::key::TableMetadataManager;
1035 use common_meta::key::flow::FlowMetadataManager;
1036 use common_meta::key::table_route::TableRouteValue;
1037 use common_meta::key::test_utils::new_test_table_info_with_name;
1038 use common_meta::kv_backend::memory::MemoryKvBackend;
1039 use common_time::timestamp::TimeUnit;
1040 use query::options::QueryOptions;
1041 use session::context::QueryContext;
1042
1043 use super::*;
1044 use crate::test_utils::create_test_query_engine;
1045
1046 struct DropNotify(Option<oneshot::Sender<()>>);
1047
1048 impl Drop for DropNotify {
1049 fn drop(&mut self) {
1050 if let Some(tx) = self.0.take() {
1051 let _ = tx.send(());
1052 }
1053 }
1054 }
1055
1056 async fn new_test_engine() -> BatchingEngine {
1057 let kv_backend = Arc::new(MemoryKvBackend::new());
1058 let table_meta = Arc::new(TableMetadataManager::new(kv_backend.clone()));
1059 table_meta.init().await.unwrap();
1060 let flow_meta = Arc::new(FlowMetadataManager::new(kv_backend));
1061 let catalog_manager = new_memory_catalog_manager().unwrap();
1062 let query_engine = create_test_query_engine();
1063 let (frontend_client, _handler) =
1064 FrontendClient::from_empty_grpc_handler(QueryOptions::default());
1065
1066 BatchingEngine::new(
1067 Arc::new(frontend_client),
1068 query_engine,
1069 flow_meta,
1070 table_meta,
1071 catalog_manager,
1072 BatchingModeOptions::default(),
1073 )
1074 }
1075
1076 #[tokio::test]
1077 async fn test_flow_option_overrides_incremental_read_switch() {
1078 let engine = new_test_engine().await;
1079
1080 let default_opts = engine.batch_opts_for_flow_options(&HashMap::new()).unwrap();
1081 assert!(!default_opts.experimental_enable_incremental_read);
1082
1083 let enabled_opts = engine
1084 .batch_opts_for_flow_options(&HashMap::from([(
1085 FLOW_EXPERIMENTAL_ENABLE_INCREMENTAL_READ_KEY.to_string(),
1086 "true".to_string(),
1087 )]))
1088 .unwrap();
1089 assert!(enabled_opts.experimental_enable_incremental_read);
1090 }
1091
1092 #[test]
1093 fn test_table_options_enable_append_mode() {
1094 assert!(!BatchingEngine::table_options_enable_append_mode(
1095 &HashMap::new()
1096 ));
1097 assert!(!BatchingEngine::table_options_enable_append_mode(
1098 &HashMap::from([(APPEND_MODE_KEY.to_string(), "false".to_string())])
1099 ));
1100 assert!(BatchingEngine::table_options_enable_append_mode(
1101 &HashMap::from([(APPEND_MODE_KEY.to_string(), "TRUE".to_string())])
1102 ));
1103 }
1104
1105 #[test]
1106 fn test_sql_flow_requires_time_window_or_eval_interval() {
1107 BatchingEngine::ensure_sql_flow_has_twe_or_eval_interval(None, true)
1108 .expect("SQL flow with a time-window expression should be accepted");
1109 BatchingEngine::ensure_sql_flow_has_twe_or_eval_interval(Some(10), false).expect(
1110 "SQL flow with EVAL INTERVAL should be accepted as an explicit full-query flow",
1111 );
1112
1113 let err = BatchingEngine::ensure_sql_flow_has_twe_or_eval_interval(None, false)
1114 .expect_err("SQL flow without a time-window expression or EVAL INTERVAL should fail");
1115 assert!(matches!(err, Error::InvalidQuery { .. }), "{err}");
1116 assert!(
1117 err.to_string().contains("must specify EVAL INTERVAL"),
1118 "{err}"
1119 );
1120 }
1121
1122 #[tokio::test]
1123 async fn test_complex_sql_without_eval_interval_is_rejected_as_no_twe() {
1124 let query_engine = create_test_query_engine();
1125 let ctx = QueryContext::arc();
1126 let plan = sql_to_df_plan(
1127 ctx.clone(),
1128 query_engine.clone(),
1129 r#"
1130SELECT
1131 l.number,
1132 date_bin('5 minutes', l.ts) AS time_window
1133FROM numbers_with_ts l
1134JOIN numbers_with_ts r ON l.number = r.number
1135GROUP BY l.number, time_window
1136"#,
1137 true,
1138 )
1139 .await
1140 .unwrap();
1141
1142 let (_, time_window_expr, _, _) = find_time_window_expr(
1143 &plan,
1144 query_engine.engine_state().catalog_manager().clone(),
1145 ctx,
1146 )
1147 .await
1148 .unwrap();
1149 assert!(
1150 time_window_expr.is_none(),
1151 "complex SQL should be classified as having no safe TWE"
1152 );
1153
1154 BatchingEngine::ensure_sql_flow_has_twe_or_eval_interval(Some(10), false)
1155 .expect("complex SQL can run as an explicit full-query flow when EVAL INTERVAL is set");
1156 let err = BatchingEngine::ensure_sql_flow_has_twe_or_eval_interval(None, false)
1157 .expect_err("complex SQL without EVAL INTERVAL should fail creation");
1158 assert!(matches!(err, Error::InvalidQuery { .. }), "{err}");
1159 }
1160
1161 #[test]
1162 fn test_incremental_source_append_only_enforcement() {
1163 let table_name = [
1164 "greptime".to_string(),
1165 "public".to_string(),
1166 "numbers".to_string(),
1167 ];
1168 let disabled_opts = BatchingModeOptions::default();
1169 let enabled_opts = BatchingModeOptions {
1170 experimental_enable_incremental_read: true,
1171 ..Default::default()
1172 };
1173 let non_append_options = HashMap::new();
1174 let append_options = HashMap::from([(APPEND_MODE_KEY.to_string(), "true".to_string())]);
1175
1176 BatchingEngine::ensure_incremental_source_append_only(
1177 &disabled_opts,
1178 &table_name,
1179 &non_append_options,
1180 )
1181 .expect("disabled incremental read should not require append-only source");
1182 BatchingEngine::ensure_incremental_source_append_only(
1183 &enabled_opts,
1184 &table_name,
1185 &append_options,
1186 )
1187 .expect("append-only source should be accepted when incremental read is enabled");
1188
1189 let err = BatchingEngine::ensure_incremental_source_append_only(
1190 &enabled_opts,
1191 &table_name,
1192 &non_append_options,
1193 )
1194 .expect_err("non-append source should be rejected when incremental read is enabled");
1195 assert!(
1196 err.to_string()
1197 .contains("Flow incremental read requires append-only source table"),
1198 "{err}"
1199 );
1200 }
1201
1202 async fn new_test_task(flow_id: FlowId) -> (BatchingTask, oneshot::Sender<()>) {
1203 new_test_task_for_source(flow_id, "numbers_with_ts", None).await
1204 }
1205
1206 async fn new_test_task_with_time_window_expr(
1207 flow_id: FlowId,
1208 time_window_expr: Option<TimeWindowExpr>,
1209 ) -> (BatchingTask, oneshot::Sender<()>) {
1210 new_test_task_for_source(flow_id, "numbers_with_ts", time_window_expr).await
1211 }
1212
1213 fn test_table_info_with_ts_unit(
1214 table_id: TableId,
1215 table_name: &str,
1216 unit: TimeUnit,
1217 ) -> table::metadata::TableInfo {
1218 use datatypes::schema::{ColumnSchema, SchemaBuilder};
1219 use table::metadata::{TableInfoBuilder, TableMetaBuilder};
1220
1221 let ts_type = match unit {
1222 TimeUnit::Second => ConcreteDataType::timestamp_second_datatype(),
1223 TimeUnit::Millisecond => ConcreteDataType::timestamp_millisecond_datatype(),
1224 TimeUnit::Microsecond => ConcreteDataType::timestamp_microsecond_datatype(),
1225 TimeUnit::Nanosecond => ConcreteDataType::timestamp_nanosecond_datatype(),
1226 };
1227 let column_schemas = vec![
1228 ColumnSchema::new("col1", ConcreteDataType::int32_datatype(), true),
1229 ColumnSchema::new("ts", ts_type, false).with_time_index(true),
1230 ];
1231 let schema = SchemaBuilder::try_from(column_schemas)
1232 .unwrap()
1233 .build()
1234 .unwrap();
1235 let meta = TableMetaBuilder::empty()
1236 .schema(Arc::new(schema))
1237 .primary_key_indices(vec![0])
1238 .engine("engine")
1239 .next_column_id(3)
1240 .build()
1241 .unwrap();
1242 TableInfoBuilder::default()
1243 .table_id(table_id)
1244 .table_version(0)
1245 .name(table_name)
1246 .catalog_name("greptime")
1247 .schema_name("public")
1248 .meta(meta)
1249 .build()
1250 .unwrap()
1251 }
1252
1253 async fn test_time_window_expr() -> TimeWindowExpr {
1255 let query_engine = create_test_query_engine();
1256 let ctx = QueryContext::arc();
1257 let plan = sql_to_df_plan(
1258 ctx.clone(),
1259 query_engine.clone(),
1260 "SELECT date_bin(INTERVAL '5 second', ts) AS time_window FROM numbers_with_ts GROUP BY time_window",
1261 true,
1262 )
1263 .await
1264 .unwrap();
1265 let (column_name, time_window_expr, _, df_schema) = find_time_window_expr(
1266 &plan,
1267 query_engine.engine_state().catalog_manager().clone(),
1268 ctx,
1269 )
1270 .await
1271 .unwrap();
1272 TimeWindowExpr::from_expr(
1273 &time_window_expr.unwrap(),
1274 &column_name,
1275 &df_schema,
1276 &query_engine.engine_state().session_state(),
1277 )
1278 .unwrap()
1279 }
1280
1281 async fn new_test_task_for_source(
1282 flow_id: FlowId,
1283 source_table_name: &str,
1284 time_window_expr: Option<TimeWindowExpr>,
1285 ) -> (BatchingTask, oneshot::Sender<()>) {
1286 let query_engine = create_test_query_engine();
1287 let ctx = QueryContext::arc();
1288 let plan = sql_to_df_plan(
1289 ctx.clone(),
1290 query_engine.clone(),
1291 "SELECT number, ts FROM numbers_with_ts",
1292 true,
1293 )
1294 .await
1295 .unwrap();
1296 let (tx, rx) = oneshot::channel();
1297
1298 let task = BatchingTask::try_new(TaskArgs {
1299 flow_id,
1300 query: "SELECT number, ts FROM numbers_with_ts",
1301 plan,
1302 time_window_expr,
1303 expire_after: None,
1304 sink_table_name: [
1305 "greptime".to_string(),
1306 "public".to_string(),
1307 "sink".to_string(),
1308 ],
1309 source_table_names: vec![[
1310 "greptime".to_string(),
1311 "public".to_string(),
1312 source_table_name.to_string(),
1313 ]],
1314 query_ctx: ctx,
1315 catalog_manager: query_engine.engine_state().catalog_manager().clone(),
1316 shutdown_rx: rx,
1317 batch_opts: Arc::new(BatchingModeOptions::default()),
1318 flow_eval_interval: None,
1319 eval_schedule: None,
1320 })
1321 .unwrap();
1322
1323 (task, tx)
1324 }
1325
1326 #[tokio::test]
1327 async fn test_handle_mark_dirty_time_window_with_time_ranges() {
1328 let engine = new_test_engine().await;
1329
1330 let mut table_info = new_test_table_info_with_name(1, "numbers_with_ts");
1333 table_info.catalog_name = "greptime".to_string();
1334 table_info.schema_name = "public".to_string();
1335 engine
1336 .table_meta
1337 .create_table_metadata(
1338 table_info,
1339 TableRouteValue::physical(vec![]),
1340 HashMap::new(),
1341 )
1342 .await
1343 .unwrap();
1344
1345 let (task, shutdown_tx) =
1347 new_test_task_with_time_window_expr(1, Some(test_time_window_expr().await)).await;
1348 let task_identity = task.clone();
1349 engine.runtime.write().await.insert(1, task, shutdown_tx);
1350
1351 engine
1352 .handle_mark_dirty_time_window(DirtyWindowRequests {
1353 requests: vec![DirtyWindowRequest {
1354 table_id: 1,
1355 timestamps: vec![],
1356 time_ranges: vec![
1357 TimeRange {
1359 start_inclusive: 3_000,
1360 end_exclusive: 11_000,
1361 },
1362 TimeRange {
1364 start_inclusive: 5_000,
1365 end_exclusive: 5_000,
1366 },
1367 TimeRange {
1368 start_inclusive: 9_000,
1369 end_exclusive: 4_000,
1370 },
1371 ],
1372 }],
1373 })
1374 .await
1375 .unwrap();
1376
1377 let state = task_identity.state.read().unwrap();
1378 assert_eq!(1, state.dirty_time_windows.len());
1379 assert_eq!(
1380 Duration::from_secs(15),
1381 state.dirty_time_windows.window_size()
1382 );
1383 }
1384
1385 #[tokio::test]
1390 async fn test_handle_mark_dirty_time_window_time_index_units() {
1391 let engine = new_test_engine().await;
1392
1393 let cases = [
1394 (TimeUnit::Second, 1u32, "t_sec", 3i64, 11i64),
1395 (TimeUnit::Millisecond, 2, "t_ms", 3_000, 11_000),
1396 (TimeUnit::Microsecond, 3, "t_us", 3_000_000, 11_000_000),
1397 (
1398 TimeUnit::Nanosecond,
1399 4,
1400 "t_ns",
1401 3_000_000_000,
1402 11_000_000_000,
1403 ),
1404 ];
1405
1406 let mut task_identities = vec![];
1407 let mut requests = vec![];
1408 for (unit, table_id, table_name, start_inclusive, end_exclusive) in cases {
1409 engine
1410 .table_meta
1411 .create_table_metadata(
1412 test_table_info_with_ts_unit(table_id, table_name, unit),
1413 TableRouteValue::physical(vec![]),
1414 HashMap::new(),
1415 )
1416 .await
1417 .unwrap();
1418
1419 let (task, shutdown_tx) = new_test_task_for_source(
1420 table_id as FlowId,
1421 table_name,
1422 Some(test_time_window_expr().await),
1423 )
1424 .await;
1425 task_identities.push((table_id, task.clone()));
1426 engine
1427 .runtime
1428 .write()
1429 .await
1430 .insert(table_id as FlowId, task, shutdown_tx);
1431
1432 requests.push(DirtyWindowRequest {
1433 table_id,
1434 timestamps: vec![],
1435 time_ranges: vec![TimeRange {
1436 start_inclusive,
1437 end_exclusive,
1438 }],
1439 });
1440 }
1441
1442 engine
1443 .handle_mark_dirty_time_window(DirtyWindowRequests { requests })
1444 .await
1445 .unwrap();
1446
1447 for (table_id, task) in task_identities {
1448 let state = task.state.read().unwrap();
1449 assert_eq!(1, state.dirty_time_windows.len(), "table id = {table_id}");
1450 assert_eq!(
1451 Duration::from_secs(15),
1452 state.dirty_time_windows.window_size(),
1453 "table id = {table_id}"
1454 );
1455 }
1456 }
1457
1458 #[tokio::test]
1459 async fn test_handle_mark_dirty_time_window_returns_error_on_alignment_failure() {
1460 let engine = new_test_engine().await;
1461 let table_id = 10;
1462 let table_name = "t_bad_timestamp";
1463
1464 engine
1465 .table_meta
1466 .create_table_metadata(
1467 test_table_info_with_ts_unit(table_id, table_name, TimeUnit::Second),
1468 TableRouteValue::physical(vec![]),
1469 HashMap::new(),
1470 )
1471 .await
1472 .unwrap();
1473
1474 let (task, shutdown_tx) = new_test_task_for_source(
1475 table_id as FlowId,
1476 table_name,
1477 Some(test_time_window_expr().await),
1478 )
1479 .await;
1480 engine
1481 .runtime
1482 .write()
1483 .await
1484 .insert(table_id as FlowId, task, shutdown_tx);
1485
1486 let result = engine
1487 .handle_mark_dirty_time_window(DirtyWindowRequests {
1488 requests: vec![DirtyWindowRequest {
1489 table_id,
1490 timestamps: vec![i64::MAX],
1491 time_ranges: vec![],
1492 }],
1493 })
1494 .await;
1495
1496 assert!(
1497 result.is_err(),
1498 "invalid timestamp alignment should be returned to the caller"
1499 );
1500 }
1501
1502 async fn install_abort_observed_handle(task: &BatchingTask) -> oneshot::Receiver<()> {
1503 let (drop_tx, drop_rx) = oneshot::channel();
1504 let (entered_tx, entered_rx) = oneshot::channel();
1505 let handle = tokio::spawn(async move {
1506 let _guard = DropNotify(Some(drop_tx));
1507 let _ = entered_tx.send(());
1508 std::future::pending::<()>().await;
1509 });
1510 task.state.write().unwrap().task_handle = Some(handle);
1511 tokio::time::timeout(Duration::from_secs(1), entered_rx)
1512 .await
1513 .expect("test task handle should start")
1514 .expect("test task handle should report start");
1515 drop_rx
1516 }
1517
1518 #[tokio::test]
1519 async fn test_notify_flow_shutdown_sends_signal() {
1520 let (tx, rx) = oneshot::channel();
1521
1522 assert!(notify_flow_shutdown(42, Some(tx), "test"));
1523
1524 rx.await.expect("replaced flow should receive shutdown");
1525 }
1526
1527 #[test]
1528 fn test_notify_flow_shutdown_accepts_missing_sender() {
1529 assert!(!notify_flow_shutdown(42, None, "test"));
1530 }
1531
1532 #[tokio::test]
1533 async fn test_abort_flow_task_aborts_handle() {
1534 let (task, _shutdown_tx) = new_test_task(42).await;
1535 let drop_rx = install_abort_observed_handle(&task).await;
1536
1537 assert!(abort_flow_task(42, Some(task), "test"));
1538
1539 tokio::time::timeout(Duration::from_secs(1), drop_rx)
1540 .await
1541 .expect("aborted task should be dropped")
1542 .expect("drop notifier should fire");
1543 }
1544
1545 #[tokio::test]
1546 async fn test_remove_flow_inner_aborts_registered_task() {
1547 let engine = new_test_engine().await;
1548 let (task, shutdown_tx) = new_test_task(42).await;
1549 let drop_rx = install_abort_observed_handle(&task).await;
1550
1551 engine.runtime.write().await.insert(42, task, shutdown_tx);
1552
1553 engine.remove_flow_inner(42).await.unwrap();
1554
1555 tokio::time::timeout(Duration::from_secs(1), drop_rx)
1556 .await
1557 .expect("removed task should be dropped")
1558 .expect("drop notifier should fire");
1559 assert!(!engine.flow_exist_inner(42).await);
1560 assert!(!engine.runtime.read().await.shutdown_txs.contains_key(&42));
1561 }
1562
1563 #[tokio::test]
1564 async fn test_or_replace_flow_runtime_replaces_old_handles_and_keeps_new_task() {
1565 let engine = new_test_engine().await;
1566 let (old_task, old_shutdown_tx) = new_test_task(42).await;
1567 let old_task_identity = old_task.clone();
1568 let old_drop_rx = install_abort_observed_handle(&old_task).await;
1569 let (new_task, new_shutdown_tx) = new_test_task(42).await;
1570 let new_task_identity = new_task.clone();
1571
1572 engine
1573 .runtime
1574 .write()
1575 .await
1576 .insert(42, old_task, old_shutdown_tx);
1577 let (replaced_old_task, replaced_old_shutdown_tx) =
1578 engine
1579 .runtime
1580 .write()
1581 .await
1582 .insert(42, new_task, new_shutdown_tx);
1583
1584 let replaced_old_task = replaced_old_task.expect("old task should be returned");
1585 assert!(Arc::ptr_eq(
1586 &replaced_old_task.state,
1587 &old_task_identity.state
1588 ));
1589 assert!(notify_flow_shutdown(
1590 42,
1591 replaced_old_shutdown_tx,
1592 "replaced"
1593 ));
1594 old_task_identity
1595 .state
1596 .write()
1597 .unwrap()
1598 .shutdown_rx
1599 .try_recv()
1600 .expect("old shutdown receiver should receive signal");
1601 assert!(abort_flow_task(42, Some(replaced_old_task), "replaced"));
1602
1603 tokio::time::timeout(Duration::from_secs(1), old_drop_rx)
1604 .await
1605 .expect("replaced task should be dropped")
1606 .expect("drop notifier should fire");
1607
1608 let runtime = engine.runtime.read().await;
1609 assert_eq!(1, runtime.tasks.len());
1610 assert_eq!(1, runtime.shutdown_txs.len());
1611 let registered_task = runtime.tasks.get(&42).expect("new task should remain");
1612 assert!(Arc::ptr_eq(
1613 ®istered_task.state,
1614 &new_task_identity.state
1615 ));
1616 assert!(runtime.shutdown_txs.contains_key(&42));
1617 assert!(matches!(
1618 new_task_identity
1619 .state
1620 .write()
1621 .unwrap()
1622 .shutdown_rx
1623 .try_recv(),
1624 Err(oneshot::error::TryRecvError::Empty)
1625 ));
1626 }
1627
1628 #[tokio::test]
1629 async fn test_rollback_flow_runtime_if_current_removes_matching_task_only() {
1630 let engine = new_test_engine().await;
1631 let (old_task, _old_shutdown_tx) = new_test_task(42).await;
1632 let (current_task, current_shutdown_tx) = new_test_task(42).await;
1633 let current_task_identity = current_task.clone();
1634
1635 engine
1636 .runtime
1637 .write()
1638 .await
1639 .insert(42, current_task, current_shutdown_tx);
1640
1641 engine.rollback_flow_runtime_if_current(42, &old_task).await;
1642
1643 let registered_task = engine.runtime.read().await.tasks.get(&42).cloned().unwrap();
1644 assert!(Arc::ptr_eq(
1645 ®istered_task.state,
1646 ¤t_task_identity.state
1647 ));
1648 assert!(engine.runtime.read().await.shutdown_txs.contains_key(&42));
1649
1650 engine
1651 .rollback_flow_runtime_if_current(42, ¤t_task_identity)
1652 .await;
1653 assert!(!engine.flow_exist_inner(42).await);
1654 assert!(!engine.runtime.read().await.shutdown_txs.contains_key(&42));
1655 }
1656}