1use std::collections::{BTreeSet, HashMap, HashSet};
16use std::sync::{Arc, RwLock};
17use std::time::{Duration, SystemTime, UNIX_EPOCH};
18
19use api::v1::{CreateTableExpr, TableName};
20use catalog::CatalogManagerRef;
21use common_error::ext::BoxedError;
22use common_query::logical_plan::breakup_insert_plan;
23use common_telemetry::tracing::warn;
24use common_telemetry::{debug, info};
25use common_time::Timestamp;
26use datafusion::datasource::DefaultTableSource;
27use datafusion::sql::unparser::expr_to_sql;
28use datafusion_common::tree_node::{Transformed, TreeNode};
29use datafusion_common::utils::quote_identifier;
30use datafusion_common::{DFSchemaRef, TableReference};
31use datafusion_expr::{DmlStatement, LogicalPlan, WriteOp, col, lit};
32use datatypes::schema::Schema;
33use query::QueryEngineRef;
34use query::options::FLOW_INCREMENTAL_MODE;
35use query::query_engine::DefaultSerializer;
36use session::context::QueryContextRef;
37use snafu::{OptionExt, ResultExt};
38use sql::parsers::utils::is_tql;
39use store_api::mito_engine_options::MERGE_MODE_KEY;
40use substrait::{DFLogicalSubstraitConvertor, SubstraitPlan};
41use table::table::adapter::DfTableProviderAdapter;
42use tokio::sync::oneshot::error::TryRecvError;
43use tokio::sync::{Mutex, oneshot};
44use tokio::time::Instant;
45
46use crate::batching_mode::BatchingModeOptions;
47use crate::batching_mode::checkpoint::checkpoint_mode_label;
48use crate::batching_mode::frontend_client::{FrontendClient, PeerDesc};
49use crate::batching_mode::state::{
50 CheckpointMode, DirtyTimeWindows, FilterExprInfo, TaskState, to_df_literal,
51};
52use crate::batching_mode::table_creator::{QueryType, create_table_with_expr};
53use crate::batching_mode::time_window::TimeWindowExpr;
54use crate::batching_mode::utils::{
55 AddFilterRewriter, ColumnMatcherRewriter, df_plan_to_sql, gen_plan_with_matching_schema,
56 get_table_info_df_schema, sql_to_df_plan,
57};
58use crate::df_optimizer::apply_df_optimizer;
59use crate::error::{
60 DatafusionSnafu, ExternalSnafu, InvalidQuerySnafu, SubstraitEncodeLogicalPlanSnafu,
61 UnexpectedSnafu,
62};
63use crate::metrics::{
64 METRIC_FLOW_BATCHING_ENGINE_ERROR_CNT, METRIC_FLOW_BATCHING_ENGINE_QUERY_TIME,
65 METRIC_FLOW_BATCHING_ENGINE_SLOW_QUERY, METRIC_FLOW_BATCHING_ENGINE_START_QUERY_CNT,
66 METRIC_FLOW_ROWS,
67};
68use crate::{Error, FlowId};
69
70mod ckpt;
71mod inc;
72
73#[derive(Clone)]
75pub struct TaskConfig {
76 pub flow_id: FlowId,
77 pub query: String,
78 pub output_schema: DFSchemaRef,
80 pub time_window_expr: Option<TimeWindowExpr>,
81 pub expire_after: Option<i64>,
83 pub sink_table_name: [String; 3],
84 pub source_table_names: HashSet<[String; 3]>,
85 pub catalog_manager: CatalogManagerRef,
86 pub query_type: QueryType,
87 pub batch_opts: Arc<BatchingModeOptions>,
88 pub flow_eval_interval: Option<Duration>,
89}
90
91fn determine_query_type(query: &str, query_ctx: &QueryContextRef) -> Result<QueryType, Error> {
92 let is_tql = is_tql(query_ctx.sql_dialect(), query)
93 .map_err(BoxedError::new)
94 .context(ExternalSnafu)?;
95 Ok(if is_tql {
96 QueryType::Tql
97 } else {
98 QueryType::Sql
99 })
100}
101
102fn is_merge_mode_last_non_null(options: &HashMap<String, String>) -> bool {
103 options
104 .get(MERGE_MODE_KEY)
105 .map(|mode| mode.eq_ignore_ascii_case("last_non_null"))
106 .unwrap_or(false)
107}
108
109fn encode_insert_plan_request(
110 insert_to: TableName,
111 insert_input_plan: &LogicalPlan,
112) -> Result<api::v1::QueryRequest, Error> {
113 let message = DFLogicalSubstraitConvertor {}
114 .encode(insert_input_plan, DefaultSerializer)
115 .context(SubstraitEncodeLogicalPlanSnafu)?;
116 Ok(api::v1::QueryRequest {
117 query: Some(api::v1::query_request::Query::InsertIntoPlan(
118 api::v1::InsertIntoPlan {
119 table_name: Some(insert_to),
120 logical_plan: message.to_vec(),
121 },
122 )),
123 })
124}
125
126fn format_insert_target_columns(plan: &LogicalPlan) -> String {
127 plan.schema()
128 .fields()
129 .iter()
130 .map(|field| quote_identifier(field.name()).to_string())
131 .collect::<Vec<_>>()
132 .join(", ")
133}
134
135#[derive(Clone)]
136pub struct BatchingTask {
137 pub config: Arc<TaskConfig>,
138 pub state: Arc<RwLock<TaskState>>,
139 execution_lock: Arc<Mutex<()>>,
143}
144
145pub struct TaskArgs<'a> {
147 pub flow_id: FlowId,
148 pub query: &'a str,
149 pub plan: LogicalPlan,
150 pub time_window_expr: Option<TimeWindowExpr>,
151 pub expire_after: Option<i64>,
152 pub sink_table_name: [String; 3],
153 pub source_table_names: Vec<[String; 3]>,
154 pub query_ctx: QueryContextRef,
155 pub catalog_manager: CatalogManagerRef,
156 pub shutdown_rx: oneshot::Receiver<()>,
157 pub batch_opts: Arc<BatchingModeOptions>,
158 pub flow_eval_interval: Option<Duration>,
159}
160
161pub struct PlanInfo {
162 pub plan: LogicalPlan,
163 pub dirty_restore: DirtyRestore,
164 pub can_advance_checkpoints: bool,
165}
166
167pub enum DirtyRestore {
168 Scoped(FilterExprInfo),
171 Unscoped(DirtyTimeWindows),
178}
179
180struct ExecuteOnceOutcome {
181 new_query: Option<PlanInfo>,
182 result: Result<Option<(usize, Duration)>, Error>,
188}
189
190impl BatchingTask {
191 #[allow(clippy::too_many_arguments)]
192 pub fn try_new(
193 TaskArgs {
194 flow_id,
195 query,
196 plan,
197 time_window_expr,
198 expire_after,
199 sink_table_name,
200 source_table_names,
201 query_ctx,
202 catalog_manager,
203 shutdown_rx,
204 batch_opts,
205 flow_eval_interval,
206 }: TaskArgs<'_>,
207 ) -> Result<Self, Error> {
208 let mut state = TaskState::with_dirty_time_windows(
209 query_ctx.clone(),
210 shutdown_rx,
211 DirtyTimeWindows::new(
212 batch_opts.experimental_max_filter_num_per_query,
213 batch_opts.experimental_time_window_merge_threshold,
214 ),
215 );
216 if !batch_opts.experimental_enable_incremental_read {
217 state.disable_incremental();
218 }
219
220 Ok(Self {
221 config: Arc::new(TaskConfig {
222 flow_id,
223 query: query.to_string(),
224 time_window_expr,
225 expire_after,
226 sink_table_name,
227 source_table_names: source_table_names.into_iter().collect(),
228 catalog_manager,
229 output_schema: plan.schema().clone(),
230 query_type: determine_query_type(query, &query_ctx)?,
231 batch_opts,
232 flow_eval_interval,
233 }),
234 state: Arc::new(RwLock::new(state)),
235 execution_lock: Arc::new(Mutex::new(())),
236 })
237 }
238
239 pub fn last_execution_time_millis(&self) -> Option<i64> {
240 self.state.read().unwrap().last_execution_time_millis()
241 }
242
243 pub fn mark_all_windows_as_dirty(&self) -> Result<(), Error> {
247 let now = SystemTime::now();
248 let now = Timestamp::new_second(
249 now.duration_since(UNIX_EPOCH)
250 .expect("Time went backwards")
251 .as_secs() as _,
252 );
253 let lower_bound = self
254 .config
255 .expire_after
256 .map(|e| now.sub_duration(Duration::from_secs(e as _)))
257 .transpose()
258 .map_err(BoxedError::new)
259 .context(ExternalSnafu)?
260 .unwrap_or(Timestamp::new_second(0));
261 debug!(
262 "Flow {} mark range ({:?}, {:?}) as dirty",
263 self.config.flow_id, lower_bound, now
264 );
265 self.state
266 .write()
267 .unwrap()
268 .dirty_time_windows
269 .add_window(lower_bound, Some(now));
270 Ok(())
271 }
272
273 pub async fn check_or_create_sink_table(
275 &self,
276 engine: &QueryEngineRef,
277 frontend_client: &Arc<FrontendClient>,
278 ) -> Result<Option<(usize, Duration)>, Error> {
279 if !self.is_table_exist(&self.config.sink_table_name).await? {
280 let create_table = self.gen_create_table_expr(engine.clone()).await?;
281 info!(
282 "Try creating sink table(if not exists) with expr: {:?}",
283 create_table
284 );
285 self.create_table(frontend_client, create_table).await?;
286 info!(
287 "Sink table {}(if not exists) created",
288 self.config.sink_table_name.join(".")
289 );
290 }
291
292 Ok(None)
293 }
294
295 pub async fn validate_sink_table_schema(&self, engine: &QueryEngineRef) -> Result<(), Error> {
301 let (table, _) = get_table_info_df_schema(
302 self.config.catalog_manager.clone(),
303 self.config.sink_table_name.clone(),
304 )
305 .await?;
306
307 let table_meta = &table.table_info().meta;
308 let merge_mode_last_non_null =
309 is_merge_mode_last_non_null(&table_meta.options.extra_options);
310 let primary_key_indices = table_meta.primary_key_indices.clone();
311 let query_ctx = self.state.read().unwrap().query_ctx.clone();
312
313 gen_plan_with_matching_schema(
314 &self.config.query,
315 query_ctx,
316 engine.clone(),
317 table_meta.schema.clone(),
318 &primary_key_indices,
319 merge_mode_last_non_null,
320 )
321 .await
322 .map(|_| ())
323 }
324
325 async fn is_table_exist(&self, table_name: &[String; 3]) -> Result<bool, Error> {
326 self.config
327 .catalog_manager
328 .table_exists(&table_name[0], &table_name[1], &table_name[2], None)
329 .await
330 .map_err(BoxedError::new)
331 .context(ExternalSnafu)
332 }
333
334 pub(crate) async fn execute_once_serialized(
335 &self,
336 engine: &QueryEngineRef,
337 frontend_client: &Arc<FrontendClient>,
338 max_window_cnt: Option<usize>,
339 ) -> Result<Option<(usize, Duration)>, Error> {
340 let outcome = self
341 .execute_once_serialized_with_outcome(engine, frontend_client, max_window_cnt)
342 .await;
343 outcome.result
344 }
345
346 async fn execute_once_serialized_with_outcome(
349 &self,
350 engine: &QueryEngineRef,
351 frontend_client: &Arc<FrontendClient>,
352 max_window_cnt: Option<usize>,
353 ) -> ExecuteOnceOutcome {
354 let _execution_guard = self.execution_lock.lock().await;
355 self.execute_once_unlocked(engine, frontend_client, max_window_cnt)
356 .await
357 }
358
359 async fn execute_once_unlocked(
361 &self,
362 engine: &QueryEngineRef,
363 frontend_client: &Arc<FrontendClient>,
364 max_window_cnt: Option<usize>,
365 ) -> ExecuteOnceOutcome {
366 let new_query = match self.gen_insert_plan_unlocked(engine, max_window_cnt).await {
367 Ok(new_query) => new_query,
368 Err(err) => {
369 return ExecuteOnceOutcome {
370 new_query: None,
371 result: Err(err),
372 };
373 }
374 };
375
376 if let Some(new_query) = new_query {
377 debug!("Generate new query: {}", new_query.plan);
378 let res = self
379 .execute_logical_plan_unlocked(
380 frontend_client,
381 &new_query.plan,
382 new_query.can_advance_checkpoints,
383 )
384 .await;
385 if res.is_err() {
386 self.handle_executed_query_failure(Some(&new_query));
387 }
388 ExecuteOnceOutcome {
389 new_query: Some(new_query),
390 result: res,
391 }
392 } else {
393 debug!("Generate no query");
394 ExecuteOnceOutcome {
395 new_query: None,
396 result: Ok(None),
397 }
398 }
399 }
400
401 async fn gen_insert_plan_unlocked(
403 &self,
404 engine: &QueryEngineRef,
405 max_window_cnt: Option<usize>,
406 ) -> Result<Option<PlanInfo>, Error> {
407 let (table, df_schema) = get_table_info_df_schema(
408 self.config.catalog_manager.clone(),
409 self.config.sink_table_name.clone(),
410 )
411 .await?;
412
413 let table_meta = &table.table_info().meta;
414 let merge_mode_last_non_null =
415 is_merge_mode_last_non_null(&table_meta.options.extra_options);
416 let primary_key_indices = table_meta.primary_key_indices.clone();
417
418 let new_query = self
419 .gen_query_with_time_window(
420 engine.clone(),
421 &table.table_info().meta.schema,
422 &primary_key_indices,
423 merge_mode_last_non_null,
424 max_window_cnt,
425 )
426 .await?;
427
428 let Some(new_query) = new_query else {
429 return Ok(None);
430 };
431
432 let table_columns = df_schema
435 .columns()
436 .into_iter()
437 .map(|c| c.name)
438 .collect::<BTreeSet<_>>();
439 for column in new_query.plan.schema().columns() {
440 if !table_columns.contains(column.name()) {
441 self.restore_dirty_windows_after_failure(&new_query);
442 return InvalidQuerySnafu {
443 reason: format!(
444 "Column {} not found in sink table with columns {:?}",
445 column, table_columns
446 ),
447 }
448 .fail();
449 }
450 }
451
452 let table_provider = Arc::new(DfTableProviderAdapter::new(table));
453 let table_source = Arc::new(DefaultTableSource::new(table_provider));
454
455 let plan = LogicalPlan::Dml(DmlStatement::new(
457 datafusion_common::TableReference::Full {
458 catalog: self.config.sink_table_name[0].clone().into(),
459 schema: self.config.sink_table_name[1].clone().into(),
460 table: self.config.sink_table_name[2].clone().into(),
461 },
462 table_source,
463 WriteOp::Insert(datafusion_expr::dml::InsertOp::Append),
464 Arc::new(new_query.plan.clone()),
465 ));
466 let insert_into_info = PlanInfo {
467 plan,
468 dirty_restore: new_query.dirty_restore,
469 can_advance_checkpoints: new_query.can_advance_checkpoints,
470 };
471 let insert_into =
472 match insert_into_info
473 .plan
474 .clone()
475 .recompute_schema()
476 .context(DatafusionSnafu {
477 context: "Failed to recompute schema",
478 }) {
479 Ok(insert_into) => insert_into,
480 Err(err) => {
481 self.restore_dirty_windows_after_failure(&insert_into_info);
482 return Err(err);
483 }
484 };
485
486 Ok(Some(PlanInfo {
487 plan: insert_into,
488 dirty_restore: insert_into_info.dirty_restore,
489 can_advance_checkpoints: insert_into_info.can_advance_checkpoints,
490 }))
491 }
492
493 pub async fn create_table(
494 &self,
495 frontend_client: &Arc<FrontendClient>,
496 expr: CreateTableExpr,
497 ) -> Result<(), Error> {
498 let catalog = &self.config.sink_table_name[0];
499 let schema = &self.config.sink_table_name[1];
500 frontend_client
501 .create(expr.clone(), catalog, schema)
502 .await?;
503 Ok(())
504 }
505
506 async fn execute_logical_plan_unlocked(
508 &self,
509 frontend_client: &Arc<FrontendClient>,
510 plan: &LogicalPlan,
511 can_advance_checkpoints: bool,
512 ) -> Result<Option<(usize, Duration)>, Error> {
513 let instant = Instant::now();
514 let flow_id = self.config.flow_id;
515
516 debug!(
517 "Executing flow {flow_id}(expire_after={:?} secs) with query {}",
518 self.config.expire_after, &plan
519 );
520
521 let catalog = &self.config.sink_table_name[0];
522 let schema = &self.config.sink_table_name[1];
523
524 let plan = plan
526 .clone()
527 .transform_down_with_subqueries(|p| {
528 if let LogicalPlan::TableScan(mut table_scan) = p {
529 let resolved = table_scan.table_name.resolve(catalog, schema);
530 table_scan.table_name = resolved.into();
531 Ok(Transformed::yes(LogicalPlan::TableScan(table_scan)))
532 } else {
533 Ok(Transformed::no(p))
534 }
535 })
536 .with_context(|_| DatafusionSnafu {
537 context: format!("Failed to fix table ref in logical plan, plan={:?}", plan),
538 })?
539 .data;
540
541 let incremental_plan = if can_advance_checkpoints {
544 self.prepare_plan_for_incremental(&plan).await?
545 } else {
546 None
547 };
548 let incremental_safe = incremental_plan.is_some();
549 let plan = incremental_plan.unwrap_or_else(|| plan.clone());
550
551 let extensions = self
552 .build_flow_query_extensions(incremental_safe, can_advance_checkpoints)
553 .await?;
554 let extension_refs = extensions
555 .iter()
556 .map(|(key, value)| (*key, value.as_str()))
557 .collect::<Vec<_>>();
558 let query_mode = if extensions
559 .iter()
560 .any(|(key, _)| *key == FLOW_INCREMENTAL_MODE)
561 {
562 CheckpointMode::Incremental
563 } else {
564 CheckpointMode::FullSnapshot
565 };
566 Self::record_query_mode(flow_id, query_mode);
567 debug!(
568 "Flow {flow_id} executing batching query with checkpoint_mode={}, extension_count={}",
569 checkpoint_mode_label(query_mode),
570 extensions.len()
571 );
572
573 let mut peer_desc = None;
574 let res = {
575 let _timer = METRIC_FLOW_BATCHING_ENGINE_QUERY_TIME
576 .with_label_values(&[flow_id.to_string().as_str()])
577 .start_timer();
578
579 let req = if let Some((insert_to, insert_input_plan)) =
580 breakup_insert_plan(&plan, catalog, schema)
581 {
582 if query_mode == CheckpointMode::FullSnapshot
583 && matches!(self.config.query_type, QueryType::Sql)
584 && self.config.flow_eval_interval.is_some()
585 && self.config.time_window_expr.is_none()
586 {
587 match df_plan_to_sql(&insert_input_plan) {
597 Ok(select_sql) => {
598 let target_columns = format_insert_target_columns(&insert_input_plan);
599 let sql = format!(
600 "INSERT INTO {} ({}) {}",
601 TableReference::full(
602 insert_to.catalog_name.as_str(),
603 insert_to.schema_name.as_str(),
604 insert_to.table_name.as_str(),
605 )
606 .to_quoted_string(),
607 target_columns,
608 select_sql
609 );
610 api::v1::QueryRequest {
611 query: Some(api::v1::query_request::Query::Sql(sql)),
612 }
613 }
614 Err(err) => {
615 warn!(
616 "Failed to unparse full-snapshot SQL flow {} plan; \
617 falling back to InsertIntoPlan: {:?}",
618 flow_id, err
619 );
620 encode_insert_plan_request(insert_to, &insert_input_plan)?
621 }
622 }
623 } else {
624 encode_insert_plan_request(insert_to, &insert_input_plan)?
625 }
626 } else {
627 let message = DFLogicalSubstraitConvertor {}
628 .encode(&plan, DefaultSerializer)
629 .context(SubstraitEncodeLogicalPlanSnafu)?;
630
631 api::v1::QueryRequest {
632 query: Some(api::v1::query_request::Query::LogicalPlan(message.to_vec())),
633 }
634 };
635
636 frontend_client
637 .query_with_terminal_metrics(catalog, schema, req, &extension_refs, &mut peer_desc)
638 .await
639 };
640
641 let elapsed = instant.elapsed();
642 let peer_label = peer_desc
643 .as_ref()
644 .map(ToString::to_string)
645 .unwrap_or_else(|| PeerDesc::default().to_string());
646 if let Err(err) = &res {
647 warn!(
648 "Failed to execute Flow {flow_id} on frontend {peer_label}, result: {err:?}, elapsed: {:?} with query: {}",
649 elapsed, &plan
650 );
651 let decision = {
652 let mut state = self.state.write().unwrap();
653 let reason = Self::query_failure_reason(err);
654 Self::apply_query_failure_to_state(&mut state, elapsed, reason)
655 };
656 if let Some(decision) = decision {
657 Self::record_checkpoint_decision(flow_id, decision);
658 }
659 }
660
661 if elapsed >= self.config.batch_opts.slow_query_threshold {
663 warn!(
664 "Flow {flow_id} on frontend {peer_label} executed for {:?} before complete, query: {}",
665 elapsed, &plan
666 );
667 let flow_id = flow_id.to_string();
668 METRIC_FLOW_BATCHING_ENGINE_SLOW_QUERY
669 .with_label_values(&[flow_id.as_str(), peer_label.as_str()])
670 .observe(elapsed.as_secs_f64());
671 }
672
673 let res = res?;
674 let (affected_rows, _) = res.output.extract_rows_and_cost();
675 debug!(
676 "Flow {flow_id} executed, affected_rows: {affected_rows:?}, elapsed: {:?}, watermark: {:?}",
677 elapsed,
678 res.region_watermark_map()
679 );
680 METRIC_FLOW_ROWS
681 .with_label_values(&[format!("{}-out-batching", flow_id).as_str()])
682 .inc_by(affected_rows as _);
683 {
684 let mut state = self.state.write().unwrap();
685 let decision = Self::apply_query_result_to_state(
686 &mut state,
687 &res,
688 elapsed,
689 can_advance_checkpoints,
690 );
691 Self::record_checkpoint_decision(flow_id, decision);
692 }
693
694 Ok(Some((affected_rows, elapsed)))
695 }
696
697 fn restore_dirty_windows_after_failure(&self, query: &PlanInfo) {
701 match &query.dirty_restore {
702 DirtyRestore::Scoped(filter) => self.restore_scoped_dirty_windows(filter),
703 DirtyRestore::Unscoped(dirty_windows) => self
704 .state
705 .write()
706 .unwrap()
707 .dirty_time_windows
708 .add_dirty_windows(dirty_windows),
709 }
710 }
711
712 fn restore_scoped_dirty_windows(&self, filter: &FilterExprInfo) {
713 self.state
714 .write()
715 .unwrap()
716 .dirty_time_windows
717 .add_windows(filter.time_ranges.clone());
718 }
719
720 fn restore_scoped_dirty_windows_on_err<T>(
721 &self,
722 filter: &FilterExprInfo,
723 result: Result<T, Error>,
724 ) -> Result<T, Error> {
725 result.inspect_err(|_| {
726 self.restore_scoped_dirty_windows(filter);
727 })
728 }
729
730 fn restore_unscoped_dirty_windows(&self, dirty_windows: &DirtyTimeWindows) {
731 self.state
732 .write()
733 .unwrap()
734 .dirty_time_windows
735 .add_dirty_windows(dirty_windows);
736 }
737
738 fn restore_unscoped_dirty_windows_on_err<T>(
739 &self,
740 dirty_windows: &DirtyTimeWindows,
741 result: Result<T, Error>,
742 ) -> Result<T, Error> {
743 result.inspect_err(|_| {
744 self.restore_unscoped_dirty_windows(dirty_windows);
745 })
746 }
747
748 fn drain_dirty_windows_signal(&self) -> (bool, DirtyTimeWindows) {
749 let mut state = self.state.write().unwrap();
750 let dirty_windows_to_restore = state.dirty_time_windows.clone();
751 let is_dirty = !dirty_windows_to_restore.is_empty();
752 state.dirty_time_windows.clean();
753 (is_dirty, dirty_windows_to_restore)
754 }
755
756 #[allow(clippy::too_many_arguments)]
757 async fn gen_unfiltered_plan_info(
758 &self,
759 engine: QueryEngineRef,
760 query_ctx: QueryContextRef,
761 sink_table_schema: Arc<Schema>,
762 primary_key_indices: &[usize],
763 allow_partial: bool,
764 dirty_windows_to_restore: DirtyTimeWindows,
765 retention_filter: Option<(&str, Timestamp, &'static str)>,
766 ) -> Result<PlanInfo, Error> {
767 let mut plan = self.restore_unscoped_dirty_windows_on_err(
768 &dirty_windows_to_restore,
769 gen_plan_with_matching_schema(
770 &self.config.query,
771 query_ctx,
772 engine,
773 sink_table_schema,
774 primary_key_indices,
775 allow_partial,
776 )
777 .await,
778 )?;
779
780 if let Some((col_name, lower_bound, context)) = retention_filter {
781 let lower = self.restore_unscoped_dirty_windows_on_err(
782 &dirty_windows_to_restore,
783 to_df_literal(lower_bound),
784 )?;
785 let retention_filter = col(col_name).gt_eq(lit(lower));
786 let mut add_filter = AddFilterRewriter::new(retention_filter);
787 plan = self.restore_unscoped_dirty_windows_on_err(
788 &dirty_windows_to_restore,
789 plan.clone()
790 .rewrite(&mut add_filter)
791 .with_context(|_| DatafusionSnafu {
792 context: format!(
793 "Failed to apply {context} expire_after filter to plan:\n {}\n",
794 plan
795 ),
796 })
797 .map(|rewrite| rewrite.data),
798 )?;
799 }
800
801 Ok(PlanInfo {
802 plan,
803 dirty_restore: DirtyRestore::Unscoped(dirty_windows_to_restore),
804 can_advance_checkpoints: true,
805 })
806 }
807
808 async fn gen_unfiltered_plan_info_if_dirty(
809 &self,
810 engine: QueryEngineRef,
811 query_ctx: QueryContextRef,
812 sink_table_schema: Arc<Schema>,
813 primary_key_indices: &[usize],
814 allow_partial: bool,
815 retention_filter: Option<(&str, Timestamp, &'static str)>,
816 ) -> Result<Option<PlanInfo>, Error> {
817 let (is_dirty, dirty_windows_to_restore) = self.drain_dirty_windows_signal();
818 if !is_dirty {
819 debug!("Flow id={:?}, no new data, not update", self.config.flow_id);
820 return Ok(None);
821 }
822
823 self.gen_unfiltered_plan_info(
824 engine,
825 query_ctx,
826 sink_table_schema,
827 primary_key_indices,
828 allow_partial,
829 dirty_windows_to_restore,
830 retention_filter,
831 )
832 .await
833 .map(Some)
834 }
835
836 fn handle_executed_query_failure(&self, query: Option<&PlanInfo>) {
837 if let Some(query) = query {
838 self.restore_dirty_windows_after_failure(query);
839 }
840 }
841
842 pub async fn start_executing_loop(
846 &self,
847 engine: QueryEngineRef,
848 frontend_client: Arc<FrontendClient>,
849 ) {
850 let flow_id_str = self.config.flow_id.to_string();
851 let mut max_window_cnt = None;
852 let mut interval = self
853 .config
854 .flow_eval_interval
855 .map(|d| tokio::time::interval(d));
856 if let Some(tick) = &mut interval {
857 tick.tick().await; }
859 loop {
860 {
863 let mut state = self.state.write().unwrap();
864 match state.shutdown_rx.try_recv() {
865 Ok(()) => break,
866 Err(TryRecvError::Closed) => {
867 warn!(
868 "Unexpected shutdown flow {}, shutdown anyway",
869 self.config.flow_id
870 );
871 break;
872 }
873 Err(TryRecvError::Empty) => (),
874 }
875 }
876 METRIC_FLOW_BATCHING_ENGINE_START_QUERY_CNT
877 .with_label_values(&[&flow_id_str])
878 .inc();
879
880 let min_refresh = self.config.batch_opts.experimental_min_refresh_duration;
881
882 let outcome = self
883 .execute_once_serialized_with_outcome(&engine, &frontend_client, max_window_cnt)
884 .await;
885
886 match outcome.result {
887 Ok(Some(_)) => {
889 max_window_cnt = max_window_cnt.map(|cnt| {
891 (cnt + 1).min(self.config.batch_opts.experimental_max_filter_num_per_query)
892 });
893
894 if let Some(eval_interval) = &mut interval {
896 eval_interval.tick().await;
897 } else {
898 let sleep_until = {
901 let state = self.state.write().unwrap();
902
903 let time_window_size = self
904 .config
905 .time_window_expr
906 .as_ref()
907 .and_then(|t| *t.time_window_size());
908
909 let prefer_short_incremental_cadence = state.checkpoint_mode()
910 == CheckpointMode::Incremental
911 && !state.is_incremental_disabled();
912
913 state.get_next_start_query_time(
914 self.config.flow_id,
915 &time_window_size,
916 min_refresh,
917 Some(self.config.batch_opts.query_timeout),
918 self.config.batch_opts.experimental_max_filter_num_per_query,
919 prefer_short_incremental_cadence,
920 )
921 };
922
923 tokio::time::sleep_until(sleep_until).await;
924 };
925 }
926 Ok(None) => {
928 debug!(
929 "Flow id = {:?} found no new data, sleep for {:?} then continue",
930 self.config.flow_id, min_refresh
931 );
932 tokio::time::sleep(min_refresh).await;
933 continue;
934 }
935 Err(err) => {
937 METRIC_FLOW_BATCHING_ENGINE_ERROR_CNT
938 .with_label_values(&[&flow_id_str])
939 .inc();
940 match outcome.new_query {
941 Some(query) => {
942 common_telemetry::error!(err; "Failed to execute query for flow={} with query: {}", self.config.flow_id, query.plan);
943 max_window_cnt = Some(1);
948 }
949 None => {
950 common_telemetry::error!(err; "Failed to generate query for flow={}", self.config.flow_id)
951 }
952 }
953 tokio::time::sleep(min_refresh).await;
955 }
956 }
957 }
958 }
959
960 async fn gen_create_table_expr(
967 &self,
968 engine: QueryEngineRef,
969 ) -> Result<CreateTableExpr, Error> {
970 let query_ctx = self.state.read().unwrap().query_ctx.clone();
971 let plan =
972 sql_to_df_plan(query_ctx.clone(), engine.clone(), &self.config.query, true).await?;
973 create_table_with_expr(&plan, &self.config.sink_table_name, &self.config.query_type)
974 }
975
976 fn should_use_unfiltered_incremental_delta(&self) -> bool {
977 let state = self.state.read().unwrap();
978 state.checkpoint_mode() == CheckpointMode::Incremental
979 && !state.is_incremental_disabled()
980 && matches!(self.config.query_type, QueryType::Sql)
981 }
982
983 fn should_use_unfiltered_full_snapshot_seeding(&self) -> bool {
984 let state = self.state.read().unwrap();
985 state.checkpoint_mode() == CheckpointMode::FullSnapshot
986 && !state.is_incremental_disabled()
987 && matches!(self.config.query_type, QueryType::Sql)
988 }
989
990 async fn gen_query_with_time_window(
992 &self,
993 engine: QueryEngineRef,
994 sink_table_schema: &Arc<Schema>,
995 primary_key_indices: &[usize],
996 allow_partial: bool,
997 max_window_cnt: Option<usize>,
998 ) -> Result<Option<PlanInfo>, Error> {
999 let query_ctx = self.state.read().unwrap().query_ctx.clone();
1000 let start = SystemTime::now();
1001 let since_the_epoch = start
1002 .duration_since(UNIX_EPOCH)
1003 .expect("Time went backwards");
1004 let low_bound = self
1005 .config
1006 .expire_after
1007 .map(|e| since_the_epoch.as_secs() - e as u64)
1008 .unwrap_or(u64::MIN);
1009
1010 let low_bound = Timestamp::new_second(low_bound as i64);
1011
1012 let expire_time_window_bound = self
1013 .config
1014 .time_window_expr
1015 .as_ref()
1016 .map(|expr| expr.eval(low_bound))
1017 .transpose()?;
1018
1019 let (expire_lower_bound, expire_upper_bound) =
1020 match (expire_time_window_bound, &self.config.query_type) {
1021 (Some((Some(l), Some(u))), QueryType::Sql) => (l, u),
1022 (None, QueryType::Sql) if self.config.flow_eval_interval.is_none() => {
1023 debug!(
1026 "Flow id = {:?}, no time window, using the same query",
1027 self.config.flow_id
1028 );
1029 return self
1031 .gen_unfiltered_plan_info_if_dirty(
1032 engine,
1033 query_ctx,
1034 sink_table_schema.clone(),
1035 primary_key_indices,
1036 allow_partial,
1037 None,
1038 )
1039 .await;
1040 }
1041 _ => {
1042 let (_, dirty_windows_to_restore) = self.drain_dirty_windows_signal();
1046
1047 let plan_info = self
1048 .gen_unfiltered_plan_info(
1049 engine,
1050 query_ctx,
1051 sink_table_schema.clone(),
1052 primary_key_indices,
1053 allow_partial,
1054 dirty_windows_to_restore,
1055 None,
1056 )
1057 .await?;
1058
1059 return Ok(Some(plan_info));
1060 }
1061 };
1062
1063 debug!(
1064 "Flow id = {:?}, found time window: precise_lower_bound={:?}, precise_upper_bound={:?} with dirty time windows: {:?}",
1065 self.config.flow_id,
1066 expire_lower_bound,
1067 expire_upper_bound,
1068 self.state.read().unwrap().dirty_time_windows
1069 );
1070 let window_size = expire_upper_bound
1071 .sub(&expire_lower_bound)
1072 .with_context(|| UnexpectedSnafu {
1073 reason: format!(
1074 "Can't get window size from {expire_upper_bound:?} - {expire_lower_bound:?}"
1075 ),
1076 })?;
1077 let col_name = self
1078 .config
1079 .time_window_expr
1080 .as_ref()
1081 .map(|expr| expr.column_name.clone())
1082 .with_context(|| UnexpectedSnafu {
1083 reason: format!(
1084 "Flow id={:?}, Failed to get column name from time window expr",
1085 self.config.flow_id
1086 ),
1087 })?;
1088
1089 if self.should_use_unfiltered_full_snapshot_seeding() {
1090 let retention_filter = self
1099 .config
1100 .expire_after
1101 .map(|_| (col_name.as_str(), expire_lower_bound, "full-snapshot"));
1102 return self
1103 .gen_unfiltered_plan_info_if_dirty(
1104 engine,
1105 query_ctx,
1106 sink_table_schema.clone(),
1107 primary_key_indices,
1108 allow_partial,
1109 retention_filter,
1110 )
1111 .await;
1112 }
1113
1114 if self.should_use_unfiltered_incremental_delta() {
1115 let retention_filter = self
1125 .config
1126 .expire_after
1127 .map(|_| (col_name.as_str(), expire_lower_bound, "incremental"));
1128 return self
1129 .gen_unfiltered_plan_info_if_dirty(
1130 engine,
1131 query_ctx,
1132 sink_table_schema.clone(),
1133 primary_key_indices,
1134 allow_partial,
1135 retention_filter,
1136 )
1137 .await;
1138 }
1139
1140 let (expr, can_advance_checkpoints) = {
1141 let mut state = self.state.write().unwrap();
1142 let window_cnt = max_window_cnt
1143 .unwrap_or(self.config.batch_opts.experimental_max_filter_num_per_query);
1144 let expr = state.dirty_time_windows.gen_filter_exprs(
1145 &col_name,
1146 Some(expire_lower_bound),
1147 window_size,
1148 window_cnt,
1149 self.config.flow_id,
1150 Some(self),
1151 )?;
1152 let can_advance_checkpoints = state.dirty_time_windows.is_empty();
1153 (expr, can_advance_checkpoints)
1154 };
1155
1156 let Some(expr) = expr else {
1157 debug!("Flow id={:?}, no new data, not update", self.config.flow_id);
1159 return Ok(None);
1160 };
1161
1162 let filter_sql = expr_to_sql(&expr.expr)
1163 .map(|sql| sql.to_string())
1164 .unwrap_or_else(|err| format!("<failed to format filter expr: {err}>"));
1165
1166 debug!(
1167 "Flow id={:?}, Generated filter expr: {:?}",
1168 self.config.flow_id, filter_sql
1169 );
1170
1171 let mut add_filter = AddFilterRewriter::new(expr.expr.clone());
1172 let mut add_auto_column = ColumnMatcherRewriter::new(
1173 sink_table_schema.clone(),
1174 primary_key_indices.to_vec(),
1175 allow_partial,
1176 );
1177
1178 let plan = self.restore_scoped_dirty_windows_on_err(
1179 &expr,
1180 sql_to_df_plan(query_ctx.clone(), engine.clone(), &self.config.query, false).await,
1181 )?;
1182 let rewrite = self.restore_scoped_dirty_windows_on_err(
1183 &expr,
1184 plan.clone()
1185 .rewrite(&mut add_filter)
1186 .and_then(|p| p.data.rewrite(&mut add_auto_column))
1187 .with_context(|_| DatafusionSnafu {
1188 context: format!("Failed to rewrite plan:\n {}\n", plan),
1189 })
1190 .map(|rewrite| rewrite.data),
1191 )?;
1192 let new_plan = self.restore_scoped_dirty_windows_on_err(
1194 &expr,
1195 apply_df_optimizer(rewrite, &query_ctx).await,
1196 )?;
1197
1198 let info = PlanInfo {
1199 plan: new_plan.clone(),
1200 dirty_restore: DirtyRestore::Scoped(expr),
1201 can_advance_checkpoints,
1202 };
1203
1204 Ok(Some(info))
1205 }
1206}
1207
1208#[cfg(test)]
1209mod test;