Skip to main content

flow/batching_mode/
engine.rs

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