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