1use std::collections::{BTreeMap, BTreeSet, HashMap};
19use std::time::Duration;
20
21use common_telemetry::debug;
22use common_time::Timestamp;
23use datatypes::value::Value;
24use session::context::QueryContextRef;
25use snafu::{OptionExt, ResultExt, ensure};
26use tokio::sync::oneshot;
27use tokio::time::Instant;
28
29use crate::batching_mode::task::BatchingTask;
30use crate::batching_mode::time_window::TimeWindowExpr;
31use crate::error::{DatatypesSnafu, InternalSnafu, TimeSnafu, UnexpectedSnafu};
32use crate::metrics::{
33 METRIC_FLOW_BATCHING_ENGINE_QUERY_WINDOW_CNT, METRIC_FLOW_BATCHING_ENGINE_QUERY_WINDOW_SIZE,
34 METRIC_FLOW_BATCHING_ENGINE_STALLED_WINDOW_SIZE,
35};
36use crate::{Error, FlowId};
37
38#[derive(Debug)]
40pub struct TaskState {
41 pub(crate) query_ctx: QueryContextRef,
43 last_update_time: Instant,
45 last_query_duration: Duration,
47 last_exec_time_millis: Option<i64>,
49 start_time_millis: Option<i64>,
51 pub(crate) dirty_time_windows: DirtyTimeWindows,
54 checkpoint_mode: CheckpointMode,
55 pending_fenced_repair: Option<FencedRepair>,
56 checkpoints: BTreeMap<u64, u64>,
59 incremental_disabled: bool,
63 exec_state: ExecState,
64 pub(crate) shutdown_rx: oneshot::Receiver<()>,
66 pub(crate) task_handle: Option<tokio::task::JoinHandle<()>>,
68}
69impl TaskState {
70 pub fn new(query_ctx: QueryContextRef, shutdown_rx: oneshot::Receiver<()>) -> Self {
71 Self::with_dirty_time_windows(query_ctx, shutdown_rx, DirtyTimeWindows::default())
72 }
73
74 pub fn with_dirty_time_windows(
75 query_ctx: QueryContextRef,
76 shutdown_rx: oneshot::Receiver<()>,
77 dirty_time_windows: DirtyTimeWindows,
78 ) -> Self {
79 Self {
80 query_ctx,
81 last_update_time: Instant::now(),
82 last_query_duration: Duration::from_secs(0),
83 last_exec_time_millis: None,
84 start_time_millis: None,
85 dirty_time_windows,
86 checkpoint_mode: CheckpointMode::FullSnapshot,
87 pending_fenced_repair: None,
88 checkpoints: Default::default(),
89 incremental_disabled: false,
90 exec_state: ExecState::Idle,
91 shutdown_rx,
92 task_handle: None,
93 }
94 }
95
96 pub fn record_start_time_if_first(&mut self) {
99 if self.start_time_millis.is_none() {
100 self.start_time_millis = Some(common_time::util::current_time_millis());
105 }
106 }
107
108 pub fn after_query_exec(&mut self, elapsed: Duration, is_succ: bool) {
109 self.exec_state = ExecState::Idle;
110 self.last_query_duration = elapsed;
111 self.last_update_time = Instant::now();
112 if is_succ {
113 self.last_exec_time_millis = Some(common_time::util::current_time_millis());
114 }
115 }
116
117 pub fn last_execution_time_millis(&self) -> Option<i64> {
118 self.last_exec_time_millis
119 }
120
121 pub fn start_time_millis(&self) -> Option<i64> {
123 self.start_time_millis
124 }
125
126 pub fn checkpoint_mode(&self) -> CheckpointMode {
127 self.checkpoint_mode
128 }
129
130 pub fn checkpoints(&self) -> &BTreeMap<u64, u64> {
131 &self.checkpoints
132 }
133
134 pub fn pending_fenced_repair(&self) -> Option<&FencedRepair> {
137 self.pending_fenced_repair.as_ref()
138 }
139
140 pub fn is_incremental_disabled(&self) -> bool {
141 self.incremental_disabled
142 }
143
144 pub fn disable_incremental(&mut self) {
147 self.incremental_disabled = true;
148 self.mark_full_snapshot();
149 }
150
151 pub fn mark_full_snapshot(&mut self) {
155 self.abandon_fenced_repair();
156 }
157
158 pub fn advance_checkpoints(&mut self, watermark_map: HashMap<u64, u64>) {
161 self.checkpoints = watermark_map.into_iter().collect();
162 self.pending_fenced_repair = None;
163 if !self.incremental_disabled {
164 self.checkpoint_mode = CheckpointMode::Incremental;
165 }
166 }
167
168 pub fn advance_incremental_checkpoints_with_participation(
171 &mut self,
172 participating_regions: &BTreeSet<u64>,
173 watermark_map: HashMap<u64, u64>,
174 ) {
175 for region_id in participating_regions {
176 if let Some(seq) = watermark_map.get(region_id) {
177 self.checkpoints.insert(*region_id, *seq);
178 }
179 }
180 if !self.incremental_disabled {
181 self.checkpoint_mode = CheckpointMode::Incremental;
182 }
183 self.pending_fenced_repair = None;
184 }
185
186 pub fn start_fenced_repair(&mut self, high: BTreeMap<u64, u64>) -> Option<&FencedRepair> {
191 if self.dirty_time_windows.is_empty() {
192 self.pending_fenced_repair = None;
193 return None;
194 }
195
196 let pending_windows = self.dirty_time_windows.clone();
197 self.dirty_time_windows.clean();
198 self.pending_fenced_repair = Some(FencedRepair {
199 high,
200 pending_windows,
201 });
202 self.checkpoint_mode = CheckpointMode::FullSnapshot;
203 self.pending_fenced_repair.as_ref()
204 }
205
206 pub fn finish_fenced_repair(&mut self) -> Option<BTreeMap<u64, u64>> {
209 let repair = self.pending_fenced_repair.take()?;
210 self.checkpoints = repair.high;
211 if !self.incremental_disabled {
212 self.checkpoint_mode = CheckpointMode::Incremental;
213 }
214 Some(self.checkpoints.clone())
215 }
216
217 pub fn abandon_fenced_repair(&mut self) -> bool {
220 self.checkpoint_mode = CheckpointMode::FullSnapshot;
221 let Some(repair) = self.pending_fenced_repair.take() else {
222 return false;
223 };
224
225 self.dirty_time_windows
226 .add_dirty_windows(&repair.pending_windows);
227 true
228 }
229
230 pub fn restore_scoped_windows(&mut self, filter: &FilterExprInfo) {
234 if let Some(repair) = self.pending_fenced_repair.as_mut() {
235 repair
236 .pending_windows
237 .add_windows(filter.time_ranges.clone());
238 return;
239 }
240
241 self.dirty_time_windows
242 .add_windows(filter.time_ranges.clone());
243 }
244
245 pub fn gen_scoped_filter_exprs(
248 &mut self,
249 col_name: &str,
250 expire_lower_bound: Option<Timestamp>,
251 window_size: chrono::Duration,
252 window_cnt: usize,
253 flow_id: FlowId,
254 task_ctx: Option<&BatchingTask>,
255 ) -> Result<Option<FilterExprInfo>, Error> {
256 if let Some(repair) = self.pending_fenced_repair.as_mut() {
257 let expr = repair.pending_windows.gen_filter_exprs(
258 col_name,
259 expire_lower_bound,
260 window_size,
261 window_cnt,
262 flow_id,
263 task_ctx,
264 )?;
265 if expr.is_some() || !repair.pending_windows.is_empty() {
266 return Ok(expr);
267 }
268
269 self.pending_fenced_repair = None;
273 }
274
275 self.dirty_time_windows.gen_filter_exprs(
276 col_name,
277 expire_lower_bound,
278 window_size,
279 window_cnt,
280 flow_id,
281 task_ctx,
282 )
283 }
284
285 pub fn fenced_repair_watermarks_match_high(
288 &self,
289 participating_regions: &BTreeSet<u64>,
290 watermark_map: &HashMap<u64, u64>,
291 ) -> bool {
292 let Some(repair) = self.pending_fenced_repair.as_ref() else {
293 return false;
294 };
295
296 !participating_regions.is_empty()
297 && participating_regions.len() == repair.high.len()
298 && watermark_map.len() == repair.high.len()
299 && participating_regions.iter().all(|region_id| {
300 repair
301 .high
302 .get(region_id)
303 .zip(watermark_map.get(region_id))
304 .is_some_and(|(high, watermark)| high == watermark)
305 })
306 }
307
308 pub fn fenced_repair_pending_is_empty(&self) -> bool {
310 self.pending_fenced_repair
311 .as_ref()
312 .is_some_and(|repair| repair.pending_windows.is_empty())
313 }
314
315 pub fn can_advance_full_snapshot_checkpoints(
318 &self,
319 participating_regions: &BTreeSet<u64>,
320 watermark_map: &HashMap<u64, u64>,
321 ) -> bool {
322 !participating_regions.is_empty()
323 && participating_regions.len() == watermark_map.len()
324 && participating_regions
325 .iter()
326 .all(|region_id| watermark_map.contains_key(region_id))
327 }
328
329 pub fn can_advance_incremental_checkpoints_with_participation(
332 &self,
333 participating_regions: &BTreeSet<u64>,
334 watermark_map: &HashMap<u64, u64>,
335 ) -> bool {
336 !self.incremental_disabled
337 && !self.checkpoints.is_empty()
338 && !participating_regions.is_empty()
339 && participating_regions.len() == watermark_map.len()
340 && participating_regions
341 .iter()
342 .all(|region_id| self.checkpoints.contains_key(region_id))
343 && participating_regions.iter().all(|region_id| {
344 let checkpoint = self.checkpoints.get(region_id);
345 watermark_map
346 .get(region_id)
347 .zip(checkpoint)
348 .is_some_and(|(seq, checkpoint)| seq >= checkpoint)
349 })
350 }
351
352 pub fn get_next_start_query_time(
370 &self,
371 flow_id: FlowId,
372 time_window_size: &Option<Duration>,
373 min_refresh_duration: Duration,
374 max_timeout: Option<Duration>,
375 max_filter_num_per_query: usize,
376 prefer_short_incremental_cadence: bool,
377 ) -> Instant {
378 let lower = time_window_size.unwrap_or(min_refresh_duration);
380 let next_duration = self.last_query_duration.max(lower);
381 let next_duration = if let Some(max_timeout) = max_timeout {
382 next_duration.min(max_timeout)
383 } else {
384 next_duration
385 };
386
387 if self
388 .pending_fenced_repair
389 .as_ref()
390 .is_some_and(|repair| !repair.pending_windows().is_empty())
391 {
392 debug!(
393 "Flow id = {}, active fenced repair still has pending windows, execute immediately",
394 flow_id,
395 );
396 return Instant::now();
397 }
398
399 let cur_dirty_window_size = self.dirty_time_windows.window_size();
400 let max_query_update_range = (*time_window_size)
402 .unwrap_or_default()
403 .mul_f64(max_filter_num_per_query as f64);
404 if cur_dirty_window_size < max_query_update_range {
407 if prefer_short_incremental_cadence {
408 let next_duration = self.last_query_duration.max(min_refresh_duration);
412 let next_duration = if let Some(max_timeout) = max_timeout {
413 next_duration.min(max_timeout)
414 } else {
415 next_duration
416 };
417 self.last_update_time + next_duration
418 } else {
419 self.last_update_time + next_duration
420 }
421 } else {
422 debug!(
425 "Flow id = {}, still have too many {} dirty time window({:?}), execute immediately",
426 flow_id,
427 self.dirty_time_windows.windows.len(),
428 self.dirty_time_windows.windows
429 );
430 Instant::now()
431 }
432 }
433}
434
435#[derive(Debug, Clone)]
438pub struct DirtyTimeWindows {
439 windows: BTreeMap<Timestamp, Option<Timestamp>>,
442 max_filter_num_per_query: usize,
444 time_window_merge_threshold: usize,
447}
448
449impl DirtyTimeWindows {
450 pub fn new(max_filter_num_per_query: usize, time_window_merge_threshold: usize) -> Self {
451 Self {
452 windows: BTreeMap::new(),
453 max_filter_num_per_query,
454 time_window_merge_threshold,
455 }
456 }
457
458 #[cfg(test)]
459 pub(crate) fn max_filter_num_per_query(&self) -> usize {
460 self.max_filter_num_per_query
461 }
462
463 #[cfg(test)]
464 pub(crate) fn time_window_merge_threshold(&self) -> usize {
465 self.time_window_merge_threshold
466 }
467}
468
469impl Default for DirtyTimeWindows {
470 fn default() -> Self {
471 Self {
472 windows: BTreeMap::new(),
473 max_filter_num_per_query: 20,
474 time_window_merge_threshold: 3,
475 }
476 }
477}
478
479impl DirtyTimeWindows {
480 pub const MERGE_DIST: i32 = 3;
484
485 pub fn add_lower_bounds(&mut self, lower_bounds: impl Iterator<Item = Timestamp>) {
491 for lower_bound in lower_bounds {
492 let entry = self.windows.entry(lower_bound);
493 entry.or_insert(None);
494 }
495 }
496
497 pub fn window_size(&self) -> Duration {
498 let mut ret = Duration::from_secs(0);
499 for (start, end) in &self.windows {
500 if let Some(end) = end
501 && let Some(duration) = end.sub(start)
502 {
503 ret += duration.to_std().unwrap_or_default();
504 }
505 }
506 ret
507 }
508
509 pub fn add_window(&mut self, start: Timestamp, end: Option<Timestamp>) {
510 self.add_or_merge_window(start, end);
511 }
512
513 pub fn add_windows(&mut self, time_ranges: Vec<(Timestamp, Timestamp)>) {
514 for (start, end) in time_ranges {
515 self.add_or_merge_window(start, Some(end));
516 }
517 }
518
519 pub fn add_dirty_windows(&mut self, dirty_windows: &DirtyTimeWindows) {
521 for (start, end) in &dirty_windows.windows {
522 self.add_or_merge_window(*start, *end);
523 }
524 }
525
526 fn add_or_merge_window(&mut self, start: Timestamp, end: Option<Timestamp>) {
527 self.windows
528 .entry(start)
529 .and_modify(|current_end| {
530 *current_end = Self::union_window_end(*current_end, end);
531 })
532 .or_insert(end);
533 }
534
535 fn union_window_end(
536 current_end: Option<Timestamp>,
537 incoming_end: Option<Timestamp>,
538 ) -> Option<Timestamp> {
539 match (current_end, incoming_end) {
540 (Some(current), Some(incoming)) => Some(current.max(incoming)),
541 (Some(end), None) | (None, Some(end)) => Some(end),
545 (None, None) => None,
546 }
547 }
548
549 pub fn clean(&mut self) {
551 self.windows.clear();
552 }
553
554 pub fn set_dirty(&mut self) {
557 self.add_or_merge_window(Timestamp::new_second(0), None);
558 }
559
560 pub fn len(&self) -> usize {
562 self.windows.len()
563 }
564
565 pub fn is_empty(&self) -> bool {
566 self.windows.is_empty()
567 }
568
569 pub fn effective_count(&self, window_size: &Duration) -> usize {
572 if self.windows.is_empty() {
573 return 0;
574 }
575 let window_size =
576 chrono::Duration::from_std(*window_size).unwrap_or(chrono::Duration::zero());
577 let total_window_time_range =
578 self.windows
579 .iter()
580 .fold(chrono::Duration::zero(), |acc, (start, end)| {
581 if let Some(end) = end {
582 acc + end.sub(start).unwrap_or(chrono::Duration::zero())
583 } else {
584 acc + window_size
585 }
586 });
587
588 if window_size.num_seconds() == 0 {
590 0
591 } else {
592 (total_window_time_range.num_seconds() / window_size.num_seconds()) as usize
593 }
594 }
595
596 pub fn gen_filter_exprs(
602 &mut self,
603 col_name: &str,
604 expire_lower_bound: Option<Timestamp>,
605 window_size: chrono::Duration,
606 window_cnt: usize,
607 flow_id: FlowId,
608 task_ctx: Option<&BatchingTask>,
609 ) -> Result<Option<FilterExprInfo>, Error> {
610 ensure!(
611 window_size.num_seconds() > 0,
612 UnexpectedSnafu {
613 reason: "window_size is zero, can't generate filter exprs",
614 }
615 );
616
617 debug!(
618 "expire_lower_bound: {:?}, window_size: {:?}",
619 expire_lower_bound.map(|t| t.to_iso8601_string()),
620 window_size
621 );
622 self.merge_dirty_time_windows(window_size, expire_lower_bound)?;
623
624 if self.windows.len() > window_cnt {
625 let first_time_window = self.windows.first_key_value();
626 let last_time_window = self.windows.last_key_value();
627
628 if let Some(task_ctx) = task_ctx {
629 debug!(
630 "Flow id = {:?}, too many time windows: {}, only the first {} are taken for this query, the group by expression might be wrong. Time window expr={:?}, expire_after={:?}, first_time_window={:?}, last_time_window={:?}, the original query: {:?}",
631 task_ctx.config.flow_id,
632 self.windows.len(),
633 window_cnt,
634 task_ctx.config.time_window_expr,
635 task_ctx.config.expire_after,
636 first_time_window,
637 last_time_window,
638 task_ctx.config.query
639 );
640 } else {
641 debug!(
642 "Flow id = {:?}, too many time windows: {}, only the first {} are taken for this query, the group by expression might be wrong. first_time_window={:?}, last_time_window={:?}",
643 flow_id,
644 self.windows.len(),
645 window_cnt,
646 first_time_window,
647 last_time_window
648 )
649 }
650 }
651
652 let max_time_range = window_size * window_cnt as i32;
654
655 let mut to_be_query = BTreeMap::new();
656 let mut new_windows = self.windows.clone();
657 let mut cur_time_range = chrono::Duration::zero();
658 for (idx, (start, end)) in self.windows.iter().enumerate() {
659 let first_end = start
660 .add_duration(window_size.to_std().unwrap())
661 .context(TimeSnafu)?;
662 let end = end.unwrap_or(first_end);
663
664 if cur_time_range >= max_time_range {
666 break;
667 }
668
669 if idx >= window_cnt {
671 break;
672 }
673
674 let Some(x) = end.sub(start) else {
675 continue;
676 };
677 if cur_time_range + x <= max_time_range {
678 to_be_query.insert(*start, Some(end));
679 new_windows.remove(start);
680 cur_time_range += x;
681 } else {
682 let surplus = max_time_range - cur_time_range;
685 if surplus.num_seconds() <= window_size.num_seconds() {
686 break;
688 }
689 let times = surplus.num_seconds() / window_size.num_seconds();
690
691 let split_offset = window_size * times as i32;
692 let split_at = start
693 .add_duration(split_offset.to_std().unwrap())
694 .context(TimeSnafu)?;
695 to_be_query.insert(*start, Some(split_at));
696
697 new_windows.remove(start);
699 new_windows.insert(split_at, Some(end));
700 cur_time_range += split_offset;
701 break;
702 }
703 }
704
705 self.windows = new_windows;
706
707 METRIC_FLOW_BATCHING_ENGINE_QUERY_WINDOW_CNT
708 .with_label_values(&[flow_id.to_string().as_str()])
709 .observe(to_be_query.len() as f64);
710
711 let full_time_range = to_be_query
712 .iter()
713 .fold(chrono::Duration::zero(), |acc, (start, end)| {
714 if let Some(end) = end {
715 acc + end.sub(start).unwrap_or(chrono::Duration::zero())
716 } else {
717 acc + window_size
718 }
719 })
720 .num_seconds() as f64;
721 METRIC_FLOW_BATCHING_ENGINE_QUERY_WINDOW_SIZE
722 .with_label_values(&[flow_id.to_string().as_str()])
723 .observe(full_time_range);
724
725 let stalled_time_range =
726 self.windows
727 .iter()
728 .fold(chrono::Duration::zero(), |acc, (start, end)| {
729 if let Some(end) = end {
730 acc + end.sub(start).unwrap_or(chrono::Duration::zero())
731 } else {
732 acc + window_size
733 }
734 });
735
736 METRIC_FLOW_BATCHING_ENGINE_STALLED_WINDOW_SIZE
737 .with_label_values(&[flow_id.to_string().as_str()])
738 .observe(stalled_time_range.num_seconds() as f64);
739
740 let std_window_size = window_size.to_std().map_err(|e| {
741 InternalSnafu {
742 reason: e.to_string(),
743 }
744 .build()
745 })?;
746
747 let mut expr_lst = vec![];
748 let mut time_ranges = vec![];
749 for (start, end) in to_be_query.into_iter() {
750 let (start, end) = if let Some(ctx) = task_ctx {
752 let Some(time_window_expr) = &ctx.config.time_window_expr else {
753 UnexpectedSnafu {
754 reason: "time_window_expr is not set",
755 }
756 .fail()?
757 };
758 Self::align_time_window(start, end, time_window_expr)?
759 } else {
760 (start, end)
761 };
762 let end = end.unwrap_or(start.add_duration(std_window_size).context(TimeSnafu)?);
763 time_ranges.push((start, end));
764
765 debug!(
766 "Time window start: {:?}, end: {:?}",
767 start.to_iso8601_string(),
768 end.to_iso8601_string()
769 );
770
771 use datafusion_expr::{col, lit};
772 let lower = to_df_literal(start)?;
773 let upper = to_df_literal(end)?;
774 let expr = col(col_name)
775 .gt_eq(lit(lower))
776 .and(col(col_name).lt(lit(upper)));
777 expr_lst.push(expr);
778 }
779 let expr = expr_lst.into_iter().reduce(|a, b| a.or(b));
780 let ret = expr.map(|expr| FilterExprInfo {
781 expr,
782 col_name: col_name.to_string(),
783 time_ranges,
784 window_size,
785 });
786 Ok(ret)
787 }
788
789 pub(crate) fn align_time_window(
792 start: Timestamp,
793 end: Option<Timestamp>,
794 time_window_expr: &TimeWindowExpr,
795 ) -> Result<(Timestamp, Option<Timestamp>), Error> {
796 let align_start = time_window_expr.eval(start)?.0.context(UnexpectedSnafu {
797 reason: format!(
798 "Failed to align start time {:?} with time window expr {:?}",
799 start, time_window_expr
800 ),
801 })?;
802 let align_end = end
803 .and_then(|end| {
804 time_window_expr
805 .eval(end)
806 .map(|r| if r.0 == Some(end) { r.0 } else { r.1 })
808 .transpose()
809 })
810 .transpose()?;
811 Ok((align_start, align_end))
812 }
813
814 pub fn merge_dirty_time_windows(
818 &mut self,
819 window_size: chrono::Duration,
820 expire_lower_bound: Option<Timestamp>,
821 ) -> Result<(), Error> {
822 if self.windows.is_empty() {
823 return Ok(());
824 }
825
826 let mut new_windows = BTreeMap::new();
827
828 let std_window_size = window_size.to_std().map_err(|e| {
829 InternalSnafu {
830 reason: e.to_string(),
831 }
832 .build()
833 })?;
834
835 let mut prev_tw = None;
837 for (mut lower_bound, upper_bound) in std::mem::take(&mut self.windows) {
838 if let Some(expire_lower_bound) = expire_lower_bound {
840 match upper_bound {
841 Some(upper_bound) if upper_bound <= expire_lower_bound => continue,
844 Some(_) if lower_bound < expire_lower_bound => {
849 lower_bound = expire_lower_bound;
850 }
851 None if lower_bound < expire_lower_bound => continue,
853 _ => {}
854 }
855 }
856
857 let Some(prev_tw) = &mut prev_tw else {
858 prev_tw = Some((lower_bound, upper_bound));
859 continue;
860 };
861
862 let prev_upper = prev_tw
865 .1
866 .unwrap_or(prev_tw.0.add_duration(std_window_size).context(TimeSnafu)?);
867 prev_tw.1 = Some(prev_upper);
868
869 let cur_upper = upper_bound.unwrap_or(
870 lower_bound
871 .add_duration(std_window_size)
872 .context(TimeSnafu)?,
873 );
874
875 if lower_bound
876 .sub(&prev_upper)
877 .map(|dist| dist <= window_size * self.time_window_merge_threshold as i32)
878 .unwrap_or(false)
879 {
880 prev_tw.1 = Some(prev_upper.max(cur_upper));
883 } else {
884 new_windows.insert(prev_tw.0, prev_tw.1);
885 *prev_tw = (lower_bound, Some(cur_upper));
886 }
887 }
888
889 if let Some(prev_tw) = prev_tw {
890 new_windows.insert(prev_tw.0, prev_tw.1);
891 }
892
893 self.windows = new_windows;
894
895 Ok(())
896 }
897}
898
899pub(crate) fn to_df_literal(value: Timestamp) -> Result<datafusion_common::ScalarValue, Error> {
900 let value = Value::from(value);
901 let value = value
902 .try_to_scalar_value(&value.data_type())
903 .with_context(|_| DatatypesSnafu {
904 extra: format!("Failed to convert to scalar value: {}", value),
905 })?;
906 Ok(value)
907}
908
909#[derive(Debug, Clone)]
910enum ExecState {
911 Idle,
912 Executing,
913}
914
915#[derive(Debug, Clone, Copy, PartialEq, Eq)]
916pub enum CheckpointMode {
917 FullSnapshot,
918 Incremental,
919}
920
921#[derive(Debug, Clone)]
924pub struct FencedRepair {
925 high: BTreeMap<u64, u64>,
926 pending_windows: DirtyTimeWindows,
927}
928
929impl FencedRepair {
930 pub fn high(&self) -> &BTreeMap<u64, u64> {
932 &self.high
933 }
934
935 pub fn pending_windows(&self) -> &DirtyTimeWindows {
937 &self.pending_windows
938 }
939}
940
941#[derive(Debug, Clone)]
943pub struct FilterExprInfo {
944 pub expr: datafusion_expr::Expr,
945 pub col_name: String,
946 pub time_ranges: Vec<(Timestamp, Timestamp)>,
947 pub window_size: chrono::Duration,
948}
949
950impl FilterExprInfo {
951 pub fn total_window_length(&self) -> chrono::Duration {
952 self.time_ranges
953 .iter()
954 .fold(chrono::Duration::zero(), |acc, (start, end)| {
955 acc + end.sub(start).unwrap_or(chrono::Duration::zero())
956 })
957 }
958
959 pub fn predicate_for_col(
960 &self,
961 col_name: &str,
962 ) -> Result<Option<datafusion_expr::Expr>, Error> {
963 use datafusion_common::Column;
964 use datafusion_expr::{Expr, lit};
965
966 let mut expr_lst = Vec::with_capacity(self.time_ranges.len());
967 for (start, end) in &self.time_ranges {
968 let lower = to_df_literal(*start)?;
969 let upper = to_df_literal(*end)?;
970 let filter_col = || Expr::Column(Column::new_unqualified(col_name));
971 expr_lst.push(
972 filter_col()
973 .gt_eq(lit(lower))
974 .and(filter_col().lt(lit(upper))),
975 );
976 }
977
978 Ok(expr_lst.into_iter().reduce(|a, b| a.or(b)))
979 }
980}
981
982#[cfg(test)]
983mod test {
984 use pretty_assertions::assert_eq;
985 use session::context::QueryContext;
986
987 use super::*;
988 use crate::batching_mode::time_window::find_time_window_expr;
989 use crate::batching_mode::utils::sql_to_df_plan;
990 use crate::test_utils::create_test_query_engine;
991
992 #[test]
993 fn test_task_state_records_last_execution_time() {
994 let query_ctx = QueryContext::arc();
995 let (_tx, rx) = tokio::sync::oneshot::channel();
996 let mut state = TaskState::new(query_ctx, rx);
997
998 assert_eq!(None, state.last_execution_time_millis());
999 state.after_query_exec(std::time::Duration::from_millis(1), false);
1000 assert_eq!(None, state.last_execution_time_millis());
1001
1002 state.after_query_exec(std::time::Duration::from_millis(1), true);
1003 assert!(state.last_execution_time_millis().is_some());
1004 }
1005
1006 #[test]
1007 fn test_merge_dirty_time_windows() {
1008 let merge_dist = DirtyTimeWindows::default().time_window_merge_threshold;
1009 let testcases = vec![
1010 (
1012 vec![
1013 Timestamp::new_second(0),
1014 Timestamp::new_second((1 + merge_dist as i64) * 5 * 60),
1015 ],
1016 (chrono::Duration::seconds(5 * 60), None),
1017 BTreeMap::from([(
1018 Timestamp::new_second(0),
1019 Some(Timestamp::new_second((2 + merge_dist as i64) * 5 * 60)),
1020 )]),
1021 Some(
1022 "((ts >= CAST('1970-01-01 00:00:00' AS TIMESTAMP)) AND (ts < CAST('1970-01-01 00:25:00' AS TIMESTAMP)))",
1023 ),
1024 ),
1025 (
1027 vec![
1028 Timestamp::new_second(0),
1029 Timestamp::new_second((2 + merge_dist as i64) * 5 * 60),
1030 ],
1031 (chrono::Duration::seconds(5 * 60), None),
1032 BTreeMap::from([
1033 (
1034 Timestamp::new_second(0),
1035 Some(Timestamp::new_second(5 * 60)),
1036 ),
1037 (
1038 Timestamp::new_second((2 + merge_dist as i64) * 5 * 60),
1039 Some(Timestamp::new_second((3 + merge_dist as i64) * 5 * 60)),
1040 ),
1041 ]),
1042 Some(
1043 "(((ts >= CAST('1970-01-01 00:00:00' AS TIMESTAMP)) AND (ts < CAST('1970-01-01 00:05:00' AS TIMESTAMP))) OR ((ts >= CAST('1970-01-01 00:25:00' AS TIMESTAMP)) AND (ts < CAST('1970-01-01 00:30:00' AS TIMESTAMP))))",
1044 ),
1045 ),
1046 (
1048 vec![
1049 Timestamp::new_second(0),
1050 Timestamp::new_second((merge_dist as i64) * 5 * 60),
1051 ],
1052 (chrono::Duration::seconds(5 * 60), None),
1053 BTreeMap::from([(
1054 Timestamp::new_second(0),
1055 Some(Timestamp::new_second((1 + merge_dist as i64) * 5 * 60)),
1056 )]),
1057 Some(
1058 "((ts >= CAST('1970-01-01 00:00:00' AS TIMESTAMP)) AND (ts < CAST('1970-01-01 00:20:00' AS TIMESTAMP)))",
1059 ),
1060 ),
1061 (
1063 vec![
1064 Timestamp::new_second(0),
1065 Timestamp::new_second((merge_dist as i64) * 3),
1066 Timestamp::new_second((merge_dist as i64) * 3 * 2),
1067 ],
1068 (chrono::Duration::seconds(3), None),
1069 BTreeMap::from([(
1070 Timestamp::new_second(0),
1071 Some(Timestamp::new_second((merge_dist as i64) * 7)),
1072 )]),
1073 Some(
1074 "((ts >= CAST('1970-01-01 00:00:00' AS TIMESTAMP)) AND (ts < CAST('1970-01-01 00:00:21' AS TIMESTAMP)))",
1075 ),
1076 ),
1077 (
1079 Vec::from_iter((0..20).map(|i| Timestamp::new_second(i * 3)).chain(
1080 std::iter::once(Timestamp::new_second(
1081 60 + 3 * (DirtyTimeWindows::MERGE_DIST as i64 + 1),
1082 )),
1083 )),
1084 (chrono::Duration::seconds(3), None),
1085 BTreeMap::from([
1086 (Timestamp::new_second(0), Some(Timestamp::new_second(60))),
1087 (
1088 Timestamp::new_second(60 + 3 * (DirtyTimeWindows::MERGE_DIST as i64 + 1)),
1089 Some(Timestamp::new_second(
1090 60 + 3 * (DirtyTimeWindows::MERGE_DIST as i64 + 1) + 3,
1091 )),
1092 ),
1093 ]),
1094 Some(
1095 "((ts >= CAST('1970-01-01 00:00:00' AS TIMESTAMP)) AND (ts < CAST('1970-01-01 00:01:00' AS TIMESTAMP)))",
1096 ),
1097 ),
1098 (
1100 Vec::from_iter((0..40).map(|i| Timestamp::new_second(i * 3))),
1101 (chrono::Duration::seconds(3), None),
1102 BTreeMap::from([(
1103 Timestamp::new_second(0),
1104 Some(Timestamp::new_second(40 * 3)),
1105 )]),
1106 Some(
1107 "((ts >= CAST('1970-01-01 00:00:00' AS TIMESTAMP)) AND (ts < CAST('1970-01-01 00:01:00' AS TIMESTAMP)))",
1108 ),
1109 ),
1110 (
1112 Vec::from_iter(
1113 std::iter::once(Timestamp::new_second(0))
1114 .chain((0..40).map(|i| Timestamp::new_second(20 + i * 3))),
1115 ),
1116 (chrono::Duration::seconds(3), None),
1117 BTreeMap::from([
1118 (Timestamp::new_second(0), Some(Timestamp::new_second(3))),
1119 (Timestamp::new_second(20), Some(Timestamp::new_second(140))),
1120 ]),
1121 Some(
1122 "(((ts >= CAST('1970-01-01 00:00:00' AS TIMESTAMP)) AND (ts < CAST('1970-01-01 00:00:03' AS TIMESTAMP))) OR ((ts >= CAST('1970-01-01 00:00:20' AS TIMESTAMP)) AND (ts < CAST('1970-01-01 00:01:17' AS TIMESTAMP))))",
1123 ),
1124 ),
1125 (
1127 vec![
1128 Timestamp::new_second(0),
1129 Timestamp::new_second((merge_dist as i64) * 5 * 60),
1130 ],
1131 (
1132 chrono::Duration::seconds(5 * 60),
1133 Some(Timestamp::new_second((merge_dist as i64) * 6 * 60)),
1134 ),
1135 BTreeMap::from([]),
1136 None,
1137 ),
1138 ];
1139 for (lower_bounds, (window_size, expire_lower_bound), expected, expected_filter_expr) in
1142 testcases
1143 {
1144 let mut dirty = DirtyTimeWindows::default();
1145 dirty.add_lower_bounds(lower_bounds.into_iter());
1146 dirty
1147 .merge_dirty_time_windows(window_size, expire_lower_bound)
1148 .unwrap();
1149 assert_eq!(expected, dirty.windows);
1150 let filter_expr = dirty
1151 .gen_filter_exprs(
1152 "ts",
1153 expire_lower_bound,
1154 window_size,
1155 dirty.max_filter_num_per_query,
1156 0,
1157 None,
1158 )
1159 .unwrap()
1160 .map(|e| e.expr);
1161
1162 let unparser = datafusion::sql::unparser::Unparser::default();
1163 let to_sql = filter_expr
1164 .as_ref()
1165 .map(|e| unparser.expr_to_sql(e).unwrap().to_string());
1166 assert_eq!(expected_filter_expr, to_sql.as_deref());
1167 }
1168 }
1169
1170 #[test]
1171 fn test_merge_dirty_time_windows_with_bounded_ranges() {
1172 let window_size = chrono::Duration::seconds(5);
1173 let testcases = vec![
1174 (
1177 vec![
1178 (Timestamp::new_second(0), Some(Timestamp::new_second(15))),
1179 (Timestamp::new_second(5), Some(Timestamp::new_second(10))),
1180 ],
1181 BTreeMap::from([(Timestamp::new_second(0), Some(Timestamp::new_second(15)))]),
1182 ),
1183 (
1187 vec![
1188 (Timestamp::new_second(0), Some(Timestamp::new_second(15))),
1189 (Timestamp::new_second(3), None),
1190 ],
1191 BTreeMap::from([(Timestamp::new_second(0), Some(Timestamp::new_second(15)))]),
1192 ),
1193 (
1195 vec![
1196 (Timestamp::new_second(0), Some(Timestamp::new_second(5))),
1197 (Timestamp::new_second(100), Some(Timestamp::new_second(110))),
1198 ],
1199 BTreeMap::from([
1200 (Timestamp::new_second(0), Some(Timestamp::new_second(5))),
1201 (Timestamp::new_second(100), Some(Timestamp::new_second(110))),
1202 ]),
1203 ),
1204 (
1207 vec![
1208 (Timestamp::new_second(0), Some(Timestamp::new_second(10))),
1209 (Timestamp::new_second(5), Some(Timestamp::new_second(20))),
1210 ],
1211 BTreeMap::from([(Timestamp::new_second(0), Some(Timestamp::new_second(20)))]),
1212 ),
1213 ];
1214
1215 for (windows, expected) in testcases {
1216 let mut dirty = DirtyTimeWindows::default();
1217 for (start, end) in windows {
1218 dirty.add_window(start, end);
1219 }
1220 dirty.merge_dirty_time_windows(window_size, None).unwrap();
1221 assert_eq!(expected, dirty.windows);
1222 }
1223
1224 let expire_testcases = vec![
1226 (
1228 vec![(Timestamp::new_second(0), Some(Timestamp::new_second(10)))],
1229 BTreeMap::from([]),
1230 ),
1231 (
1233 vec![(Timestamp::new_second(0), Some(Timestamp::new_second(5)))],
1234 BTreeMap::from([]),
1235 ),
1236 (
1239 vec![(Timestamp::new_second(0), Some(Timestamp::new_second(15)))],
1240 BTreeMap::from([(Timestamp::new_second(10), Some(Timestamp::new_second(15)))]),
1241 ),
1242 (
1244 vec![(Timestamp::new_second(10), Some(Timestamp::new_second(15)))],
1245 BTreeMap::from([(Timestamp::new_second(10), Some(Timestamp::new_second(15)))]),
1246 ),
1247 (vec![(Timestamp::new_second(5), None)], BTreeMap::from([])),
1250 (
1252 vec![(Timestamp::new_second(10), None)],
1253 BTreeMap::from([(Timestamp::new_second(10), None)]),
1254 ),
1255 ];
1256
1257 for (windows, expected) in expire_testcases {
1258 let mut dirty = DirtyTimeWindows::default();
1259 for (start, end) in windows {
1260 dirty.add_window(start, end);
1261 }
1262 dirty
1263 .merge_dirty_time_windows(window_size, Some(Timestamp::new_second(10)))
1264 .unwrap();
1265 assert_eq!(expected, dirty.windows);
1266 }
1267 }
1268
1269 #[tokio::test]
1270 async fn test_align_time_window() {
1271 type TimeWindow = (Timestamp, Option<Timestamp>);
1272 struct TestCase {
1273 sql: String,
1274 aligns: Vec<(TimeWindow, TimeWindow)>,
1275 }
1276 let testcases: Vec<TestCase> = vec![TestCase{
1277 sql: "SELECT date_bin(INTERVAL '5 second', ts) AS time_window FROM numbers_with_ts GROUP BY time_window;".to_string(),
1278 aligns: vec![
1279 ((Timestamp::new_second(3), None), (Timestamp::new_second(0), None)),
1280 ((Timestamp::new_second(8), None), (Timestamp::new_second(5), None)),
1281 ((Timestamp::new_second(8), Some(Timestamp::new_second(10))), (Timestamp::new_second(5), Some(Timestamp::new_second(10)))),
1282 ((Timestamp::new_second(8), Some(Timestamp::new_second(9))), (Timestamp::new_second(5), Some(Timestamp::new_second(10)))),
1283 ],
1284 }];
1285
1286 let query_engine = create_test_query_engine();
1287 let ctx = QueryContext::arc();
1288 for TestCase { sql, aligns } in testcases {
1289 let plan = sql_to_df_plan(ctx.clone(), query_engine.clone(), &sql, true)
1290 .await
1291 .unwrap();
1292
1293 let (column_name, time_window_expr, _, df_schema) = find_time_window_expr(
1294 &plan,
1295 query_engine.engine_state().catalog_manager().clone(),
1296 ctx.clone(),
1297 )
1298 .await
1299 .unwrap();
1300
1301 let time_window_expr = time_window_expr
1302 .map(|expr| {
1303 TimeWindowExpr::from_expr(
1304 &expr,
1305 &column_name,
1306 &df_schema,
1307 &query_engine.engine_state().session_state(),
1308 )
1309 })
1310 .transpose()
1311 .unwrap()
1312 .unwrap();
1313
1314 for (before_align, expected_after_align) in aligns {
1315 let after_align = DirtyTimeWindows::align_time_window(
1316 before_align.0,
1317 before_align.1,
1318 &time_window_expr,
1319 )
1320 .unwrap();
1321 assert_eq!(expected_after_align, after_align);
1322 }
1323 }
1324 }
1325
1326 #[test]
1327 fn test_task_state_checkpoint_mode_and_advancement() {
1328 let query_ctx = QueryContext::arc();
1329 let (_tx, rx) = tokio::sync::oneshot::channel();
1330 let mut state = TaskState::new(query_ctx, rx);
1331
1332 assert_eq!(state.checkpoint_mode(), CheckpointMode::FullSnapshot);
1333 assert!(state.checkpoints().is_empty());
1334
1335 state.advance_checkpoints(HashMap::from([(1_u64, 10_u64), (2_u64, 20_u64)]));
1336 assert_eq!(state.checkpoint_mode(), CheckpointMode::Incremental);
1337 assert_eq!(
1338 state.checkpoints(),
1339 &BTreeMap::from([(1_u64, 10_u64), (2_u64, 20_u64)])
1340 );
1341
1342 state.mark_full_snapshot();
1343 assert_eq!(state.checkpoint_mode(), CheckpointMode::FullSnapshot);
1344 assert_eq!(
1345 state.checkpoints(),
1346 &BTreeMap::from([(1_u64, 10_u64), (2_u64, 20_u64)])
1347 );
1348 }
1349
1350 #[test]
1351 fn test_mark_full_snapshot_restores_pending_fenced_repair_windows() {
1352 let query_ctx = QueryContext::arc();
1353 let (_tx, rx) = tokio::sync::oneshot::channel();
1354 let mut state = TaskState::new(query_ctx, rx);
1355 state
1356 .dirty_time_windows
1357 .add_window(Timestamp::new_second(10), Some(Timestamp::new_second(15)));
1358 state
1359 .dirty_time_windows
1360 .add_window(Timestamp::new_second(100), Some(Timestamp::new_second(105)));
1361
1362 state
1363 .start_fenced_repair(BTreeMap::from([(1_u64, 10_u64)]))
1364 .unwrap();
1365 assert!(state.dirty_time_windows.is_empty());
1366 assert_eq!(
1367 state
1368 .pending_fenced_repair()
1369 .unwrap()
1370 .pending_windows()
1371 .len(),
1372 2
1373 );
1374
1375 state.mark_full_snapshot();
1376
1377 assert_eq!(state.checkpoint_mode(), CheckpointMode::FullSnapshot);
1378 assert!(state.pending_fenced_repair().is_none());
1379 assert_eq!(state.dirty_time_windows.len(), 2);
1380 }
1381
1382 #[test]
1383 fn test_disable_incremental_persists_full_snapshot_mode() {
1384 let query_ctx = QueryContext::arc();
1385 let (_tx, rx) = tokio::sync::oneshot::channel();
1386 let mut state = TaskState::new(query_ctx, rx);
1387
1388 assert!(!state.is_incremental_disabled());
1389
1390 state.disable_incremental();
1392 assert!(state.is_incremental_disabled());
1393 assert_eq!(state.checkpoint_mode(), CheckpointMode::FullSnapshot);
1394
1395 state.advance_checkpoints(HashMap::from([(1_u64, 10_u64), (2_u64, 20_u64)]));
1397 assert_eq!(state.checkpoint_mode(), CheckpointMode::FullSnapshot);
1398 assert_eq!(
1399 state.checkpoints(),
1400 &BTreeMap::from([(1_u64, 10_u64), (2_u64, 20_u64)])
1401 );
1402
1403 state.mark_full_snapshot();
1405 assert!(state.is_incremental_disabled());
1406 assert_eq!(state.checkpoint_mode(), CheckpointMode::FullSnapshot);
1407 }
1408
1409 #[test]
1410 fn test_full_snapshot_checkpoint_advancement_requires_participating_regions() {
1411 let query_ctx = QueryContext::arc();
1412 let (_tx, rx) = tokio::sync::oneshot::channel();
1413 let state = TaskState::new(query_ctx, rx);
1414
1415 assert!(!state.can_advance_full_snapshot_checkpoints(&BTreeSet::new(), &HashMap::new()));
1416 assert!(!state.can_advance_full_snapshot_checkpoints(
1417 &BTreeSet::from([1_u64, 2_u64]),
1418 &HashMap::from([(1_u64, 10_u64)]),
1419 ));
1420 assert!(state.can_advance_full_snapshot_checkpoints(
1421 &BTreeSet::from([1_u64, 2_u64]),
1422 &HashMap::from([(1_u64, 10_u64), (2_u64, 20_u64)]),
1423 ));
1424 }
1425
1426 #[test]
1427 fn test_incremental_checkpoint_advancement_requires_participation_alignment() {
1428 let query_ctx = QueryContext::arc();
1429 let (_tx, rx) = tokio::sync::oneshot::channel();
1430 let mut state = TaskState::new(query_ctx, rx);
1431 state.advance_checkpoints(HashMap::from([(1_u64, 10_u64), (2_u64, 20_u64)]));
1432
1433 assert!(
1434 state.can_advance_incremental_checkpoints_with_participation(
1435 &BTreeSet::from([1_u64]),
1436 &HashMap::from([(1_u64, 11_u64)]),
1437 )
1438 );
1439 assert!(
1440 !state.can_advance_incremental_checkpoints_with_participation(
1441 &BTreeSet::from([1_u64, 2_u64]),
1442 &HashMap::from([(1_u64, 11_u64)]),
1443 )
1444 );
1445 assert!(
1446 !state.can_advance_incremental_checkpoints_with_participation(
1447 &BTreeSet::from([3_u64]),
1448 &HashMap::from([(3_u64, 11_u64)]),
1449 )
1450 );
1451 assert!(
1452 !state.can_advance_incremental_checkpoints_with_participation(
1453 &BTreeSet::from([1_u64]),
1454 &HashMap::from([(1_u64, 9_u64)]),
1455 )
1456 );
1457 assert!(
1458 state.can_advance_incremental_checkpoints_with_participation(
1459 &BTreeSet::from([1_u64, 2_u64]),
1460 &HashMap::from([(1_u64, 11_u64), (2_u64, 21_u64)]),
1461 )
1462 );
1463
1464 state.disable_incremental();
1465 assert!(
1466 !state.can_advance_incremental_checkpoints_with_participation(
1467 &BTreeSet::from([1_u64, 2_u64]),
1468 &HashMap::from([(1_u64, 12_u64), (2_u64, 22_u64)]),
1469 )
1470 );
1471 }
1472
1473 #[test]
1474 fn test_incremental_checkpoint_advancement_merges_participating_subset() {
1475 let query_ctx = QueryContext::arc();
1476 let (_tx, rx) = tokio::sync::oneshot::channel();
1477 let mut state = TaskState::new(query_ctx, rx);
1478 state.advance_checkpoints(HashMap::from([
1479 (1_u64, 10_u64),
1480 (2_u64, 20_u64),
1481 (3_u64, 30_u64),
1482 ]));
1483
1484 state.advance_incremental_checkpoints_with_participation(
1485 &BTreeSet::from([1_u64, 3_u64]),
1486 HashMap::from([(1_u64, 12_u64), (3_u64, 35_u64)]),
1487 );
1488
1489 assert_eq!(state.checkpoint_mode(), CheckpointMode::Incremental);
1490 assert_eq!(
1491 state.checkpoints(),
1492 &BTreeMap::from([(1_u64, 12_u64), (2_u64, 20_u64), (3_u64, 35_u64)])
1493 );
1494 }
1495
1496 #[test]
1497 fn test_filter_expr_info_predicate_for_col_empty_ranges() {
1498 let filter = FilterExprInfo {
1499 expr: datafusion_expr::col("ts"),
1500 col_name: "ts".to_string(),
1501 time_ranges: vec![],
1502 window_size: chrono::Duration::seconds(1),
1503 };
1504
1505 assert!(filter.predicate_for_col("time_window").unwrap().is_none());
1506 }
1507
1508 #[test]
1509 fn test_filter_expr_info_predicate_for_col_single_range() {
1510 let filter = FilterExprInfo {
1511 expr: datafusion_expr::col("ts"),
1512 col_name: "ts".to_string(),
1513 time_ranges: vec![(Timestamp::new_second(0), Timestamp::new_second(1))],
1514 window_size: chrono::Duration::seconds(1),
1515 };
1516
1517 let predicate = filter.predicate_for_col("time_window").unwrap().unwrap();
1518 let unparser = datafusion::sql::unparser::Unparser::default();
1519 assert_eq!(
1520 "((time_window >= CAST('1970-01-01 00:00:00' AS TIMESTAMP)) AND (time_window < CAST('1970-01-01 00:00:01' AS TIMESTAMP)))",
1521 unparser.expr_to_sql(&predicate).unwrap().to_string()
1522 );
1523 }
1524
1525 #[test]
1526 fn test_filter_expr_info_predicate_for_col_multiple_ranges() {
1527 let filter = FilterExprInfo {
1528 expr: datafusion_expr::col("ts"),
1529 col_name: "ts".to_string(),
1530 time_ranges: vec![
1531 (Timestamp::new_second(0), Timestamp::new_second(1)),
1532 (Timestamp::new_second(10), Timestamp::new_second(11)),
1533 ],
1534 window_size: chrono::Duration::seconds(1),
1535 };
1536
1537 let predicate = filter.predicate_for_col("time_window").unwrap().unwrap();
1538 let unparser = datafusion::sql::unparser::Unparser::default();
1539 assert_eq!(
1540 "(((time_window >= CAST('1970-01-01 00:00:00' AS TIMESTAMP)) AND (time_window < CAST('1970-01-01 00:00:01' AS TIMESTAMP))) OR ((time_window >= CAST('1970-01-01 00:00:10' AS TIMESTAMP)) AND (time_window < CAST('1970-01-01 00:00:11' AS TIMESTAMP))))",
1541 unparser.expr_to_sql(&predicate).unwrap().to_string()
1542 );
1543 }
1544
1545 fn state_with_past_update(age: Duration) -> TaskState {
1547 let query_ctx = QueryContext::arc();
1548 let (_tx, rx) = tokio::sync::oneshot::channel();
1549 let mut state = TaskState::new(query_ctx, rx);
1550 state.last_update_time = Instant::now() - age;
1551 state
1552 }
1553
1554 #[test]
1555 fn test_short_incremental_cadence_uses_min_refresh() {
1556 let state = state_with_past_update(Duration::from_secs(10));
1560
1561 let time_window_size = Some(Duration::from_secs(60)); let min_refresh = Duration::from_secs(5);
1563 let flow_id = 1;
1564
1565 let result = state.get_next_start_query_time(
1566 flow_id,
1567 &time_window_size,
1568 min_refresh,
1569 None,
1570 20,
1571 true, );
1573
1574 let expected = state.last_update_time + min_refresh;
1576 assert_eq!(result, expected);
1577 }
1578
1579 #[test]
1580 fn test_short_incremental_cadence_respects_last_query_duration() {
1581 let mut state = state_with_past_update(Duration::from_secs(10));
1582 state.last_query_duration = Duration::from_secs(20);
1583
1584 let time_window_size = Some(Duration::from_secs(60));
1585 let min_refresh = Duration::from_secs(5);
1586 let flow_id = 1;
1587
1588 let result = state.get_next_start_query_time(
1589 flow_id,
1590 &time_window_size,
1591 min_refresh,
1592 None,
1593 20,
1594 true,
1595 );
1596
1597 assert_eq!(result, state.last_update_time + state.last_query_duration);
1598 }
1599
1600 #[test]
1601 fn test_short_incremental_cadence_respects_max_timeout() {
1602 let mut state = state_with_past_update(Duration::from_secs(10));
1603 state.last_query_duration = Duration::from_secs(20);
1604
1605 let time_window_size = Some(Duration::from_secs(60));
1606 let min_refresh = Duration::from_secs(30);
1607 let max_timeout = Duration::from_secs(5);
1608 let flow_id = 1;
1609
1610 let result = state.get_next_start_query_time(
1611 flow_id,
1612 &time_window_size,
1613 min_refresh,
1614 Some(max_timeout),
1615 20,
1616 true,
1617 );
1618
1619 assert_eq!(result, state.last_update_time + max_timeout);
1620 }
1621
1622 #[test]
1623 fn test_full_snapshot_ignores_short_cadence() {
1624 let mut state = state_with_past_update(Duration::from_secs(10));
1627 state.last_query_duration = Duration::from_secs(1);
1629
1630 let time_window_size = Some(Duration::from_secs(60)); let min_refresh = Duration::from_secs(5);
1632 let flow_id = 1;
1633
1634 let result = state.get_next_start_query_time(
1635 flow_id,
1636 &time_window_size,
1637 min_refresh,
1638 None,
1639 20,
1640 false, );
1642
1643 let expected = state.last_update_time + Duration::from_secs(60);
1646 assert_eq!(result, expected);
1647 }
1648
1649 #[test]
1650 fn test_dirty_window_overflow_schedules_immediately_even_with_short_cadence() {
1651 let mut state = state_with_past_update(Duration::from_secs(10));
1654 state
1656 .dirty_time_windows
1657 .add_window(Timestamp::new_second(0), Some(Timestamp::new_second(3600)));
1658
1659 let time_window_size = Some(Duration::from_secs(1)); let min_refresh = Duration::from_secs(5);
1661 let flow_id = 1;
1662
1663 let result = state.get_next_start_query_time(
1665 flow_id,
1666 &time_window_size,
1667 min_refresh,
1668 None,
1669 1, true,
1671 );
1672 assert!(
1673 result <= Instant::now(),
1674 "dirty overflow should schedule immediately"
1675 );
1676
1677 let result2 = state.get_next_start_query_time(
1679 flow_id,
1680 &time_window_size,
1681 min_refresh,
1682 None,
1683 1,
1684 false,
1685 );
1686 assert!(
1687 result2 <= Instant::now(),
1688 "dirty overflow should schedule immediately"
1689 );
1690 }
1691
1692 #[test]
1693 fn test_pending_fenced_repair_schedules_immediately() {
1694 let mut state = state_with_past_update(Duration::from_secs(10));
1695 state
1696 .dirty_time_windows
1697 .add_window(Timestamp::new_second(0), Some(Timestamp::new_second(5)));
1698 state
1699 .start_fenced_repair(BTreeMap::from([(1_u64, 10_u64)]))
1700 .unwrap();
1701 assert!(state.dirty_time_windows.is_empty());
1702 assert!(!state.fenced_repair_pending_is_empty());
1703
1704 let result = state.get_next_start_query_time(
1705 1,
1706 &Some(Duration::from_secs(60)),
1707 Duration::from_secs(5),
1708 None,
1709 20,
1710 false,
1711 );
1712
1713 assert!(
1714 result <= Instant::now(),
1715 "pending fenced repair backlog should schedule immediately"
1716 );
1717 }
1718
1719 #[test]
1720 fn test_incremental_disabled_ignores_short_cadence() {
1721 let mut state = state_with_past_update(Duration::from_secs(10));
1731 state.last_query_duration = Duration::from_secs(1);
1732
1733 let time_window_size = Some(Duration::from_secs(60));
1734 let min_refresh = Duration::from_secs(5);
1735 let flow_id = 1;
1736
1737 let result = state.get_next_start_query_time(
1738 flow_id,
1739 &time_window_size,
1740 min_refresh,
1741 None,
1742 20,
1743 false, );
1745
1746 let expected = state.last_update_time + Duration::from_secs(60);
1748 assert_eq!(result, expected);
1749 }
1750}