1mod units;
16
17use std::collections::hash_map::Entry;
18use std::collections::{BTreeMap, HashMap, HashSet};
19use std::fmt::Debug;
20use std::sync::{Arc, LazyLock};
21
22use common_base::readable_size::ReadableSize;
23use common_telemetry::{debug, info};
24use common_time::Timestamp;
25use common_time::range::TimestampRange;
26use common_time::timestamp::TimeUnit;
27use common_time::timestamp_millis::BucketAligned;
28use snafu::ResultExt;
29use store_api::storage::RegionId;
30
31use crate::compaction::CompactionOutput;
32use crate::compaction::buckets::infer_time_bucket;
33use crate::compaction::compactor::CompactionRegion;
34use crate::compaction::picker::{Picker, PickerOutput, get_expired_ssts};
35use crate::compaction::run::{
36 Ranged, SortedRun, files_overlap, files_overlap_inclusive, find_sorted_runs,
37 find_sorted_runs_by_time_range, merge_primary_key_ranges, primary_key_ranges_overlap,
38};
39use crate::compaction::twcs::units::{ClosedMergeOptions, PickLimits};
40use crate::error::{JoinSnafu, Result};
41use crate::region::options::MergeMode;
42use crate::sst::file::{FileHandle, Level, overlaps};
43use crate::sst::primary_key::PrimaryKeyRangeMapper;
44use crate::sst::version::LevelMeta;
45
46const LEVEL_COMPACTED: Level = 1;
47
48#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
51enum PickClass {
52 L0Merge,
54 FileReduction,
56 OverlapResolution,
58}
59
60#[derive(Debug)]
62struct WindowPick {
63 class: PickClass,
64 branch: &'static str,
66 inputs: Vec<FileHandle>,
67 found_runs: usize,
69}
70
71struct WindowOutputContext {
72 active_window: Option<i64>,
73 time_window_size: Option<i64>,
74 max_outputs: Option<usize>,
75 primary_key_mapper: Arc<PrimaryKeyRangeMapper>,
76}
77
78const MAX_L1_L0_ROW_RATIO: usize = 2;
80
81const DEFAULT_MAX_INPUT_FILES: usize = 16;
83
84const MAX_INPUT_FILES_ENV: &str = "GREPTIME_TWCS_MAX_INPUT_FILES";
85
86static MAX_INPUT_FILES: LazyLock<usize> = LazyLock::new(|| {
89 let env_value = std::env::var(MAX_INPUT_FILES_ENV).ok();
90 parse_max_input_files(env_value.as_deref())
91});
92
93const CLOSED_MERGE_INPUT_FACTOR: usize = 4;
97
98fn parse_max_input_files(env_value: Option<&str>) -> usize {
99 env_value
100 .and_then(|env_value| env_value.parse().ok())
101 .filter(|max_input_files| *max_input_files >= 2)
102 .unwrap_or(DEFAULT_MAX_INPUT_FILES)
103}
104
105#[derive(Clone, Debug)]
108pub struct TwcsPicker {
109 pub trigger_file_num: usize,
111 pub active_window_l1_merge_trigger: usize,
113 pub inactive_window_trigger_file_num: usize,
115 pub inactive_window_l1_merge_trigger: usize,
117 pub time_window_seconds: Option<i64>,
119 pub max_output_file_size: Option<u64>,
122 pub append_mode: bool,
124 pub(crate) merge_mode: MergeMode,
127 pub max_background_tasks: Option<usize>,
129 pub(crate) time_range: Option<TimestampRange>,
131 pub(crate) all_windows_inactive: bool,
135}
136
137impl TwcsPicker {
138 async fn build_output(
141 &self,
142 region_id: RegionId,
143 time_windows: BTreeMap<i64, Window>,
144 context: WindowOutputContext,
145 ) -> Result<Vec<CompactionOutput>> {
146 let WindowOutputContext {
147 active_window,
148 time_window_size,
149 max_outputs,
150 primary_key_mapper,
151 } = context;
152 let region_sequence = time_windows
153 .values()
154 .flat_map(|window| window.files())
155 .filter_map(|file| file.meta_ref().sequence)
156 .map(|sequence| sequence.get())
157 .max()
158 .unwrap_or(0);
159 let windows = time_windows
160 .values()
161 .rev()
162 .filter(|window| {
163 !window.files.is_empty()
164 && self.time_range.as_ref().is_none_or(|time_range| {
165 time_window_size.is_none_or(|time_window_size| {
166 time_window_intersects_range(
167 window.time_window,
168 time_window_size,
169 time_range,
170 )
171 })
172 })
173 })
174 .map(|window| window.time_window)
175 .collect::<Vec<_>>();
176 let time_windows = Arc::new(time_windows);
177 let max_outputs = max_outputs.unwrap_or(usize::MAX);
178 let chunk_size = self.max_background_tasks.unwrap_or(windows.len()).max(1);
181 let mut picks = Vec::new();
182 for chunk in windows.chunks(chunk_size) {
183 let mut handles = Vec::with_capacity(chunk.len());
184 for &window in chunk {
185 let picker = self.clone();
186 let time_windows = time_windows.clone();
187 let primary_key_mapper = primary_key_mapper.clone();
188 handles.push(common_runtime::spawn_blocking_compact(move || {
189 let pick = time_windows.get(&window).and_then(|files| {
190 picker.pick_window(
191 region_id,
192 files,
193 active_window,
194 time_window_size,
195 region_sequence,
196 &primary_key_mapper,
197 )
198 });
199 (window, pick)
200 }));
201 tokio::task::yield_now().await;
202 }
203 for result in futures::future::join_all(handles).await {
204 let (window, pick) = result.context(JoinSnafu)?;
205 picks.extend(pick.map(|pick| (window, pick)));
206 }
207 if picks
210 .iter()
211 .filter(|(_, pick)| pick.class == PickClass::L0Merge)
212 .count()
213 >= max_outputs
214 {
215 break;
216 }
217 }
218
219 picks.sort_by_key(|(_, pick)| pick.class);
221 if picks.len() > max_outputs {
222 debug!(
223 "Region ({:?}) compaction output limit ({}) reached, remaining candidates discarded",
224 region_id, max_outputs
225 );
226 picks.truncate(max_outputs);
227 }
228 let mut output = picks
229 .into_iter()
230 .map(|(window, pick)| {
231 let files = &time_windows[&window];
232 let filter_deleted = !self.append_mode
233 && !window_has_overlap(files, &time_windows)
234 && !selected_overlaps_unselected(&pick.inputs, files, &primary_key_mapper);
235 if pick.inputs.len() > 1 {
236 log_pick_result(
237 region_id,
238 window,
239 active_window,
240 &pick,
241 files.files.len(),
242 self.max_output_file_size,
243 filter_deleted,
244 );
245 }
246 CompactionOutput {
247 output_level: LEVEL_COMPACTED, inputs: pick.inputs,
249 filter_deleted,
250 output_time_range: None, }
252 })
253 .collect::<Vec<_>>();
254 output.reverse();
256 Ok(output)
257 }
258
259 fn pick_window(
261 &self,
262 region_id: RegionId,
263 window: &Window,
264 active_window: Option<i64>,
265 time_window_size: Option<i64>,
266 region_sequence: u64,
267 mapper: &PrimaryKeyRangeMapper,
268 ) -> Option<WindowPick> {
269 let mut files: Vec<_> = window.files().cloned().collect();
270 let quiet = is_quiet_window(&files, region_sequence);
271
272 if self.append_mode
274 && let Some(max_size) = self.max_output_file_size
275 {
276 let (kept_files, ignored_files) =
277 files.into_iter().partition(|file| file.size() <= max_size);
278 files = kept_files;
279 if !ignored_files.is_empty() {
280 info!(
281 "Skipped {} large files in append mode for region {}, window {}, max_size: {}",
282 ignored_files.len(),
283 region_id,
284 window.time_window,
285 max_size
286 );
287 }
288 }
289
290 let (l0_files, l1_files): (Vec<_>, Vec<_>) =
291 files.into_iter().partition(|file| file.level() == 0);
292 if !self.all_windows_inactive && active_window == Some(window.time_window) {
293 self.pick_active_window(l0_files, l1_files, mapper)
294 } else if window.files.len() < self.inactive_window_trigger_file_num
295 && l1_files.len() < self.inactive_window_l1_merge_trigger
296 {
297 None
298 } else {
299 let settling = !self.all_windows_inactive
300 && is_settling_window(window.time_window, active_window, time_window_size);
301 let num_l1_files = l1_files.len();
302 self.pick_inactive_window(l0_files, l1_files, mapper, settling, quiet)
303 .or_else(|| {
304 if self.merge_mode == MergeMode::LastNonNull
305 || num_l1_files < self.inactive_window_l1_merge_trigger
306 {
307 return None;
308 }
309 let pick = units::pick_small_l1(&window.files, mapper, self.pick_limits())?;
312 Some(WindowPick {
313 class: PickClass::FileReduction,
314 branch: "inactive:pick_small_l1",
315 inputs: pick.inputs,
316 found_runs: pick.units,
317 })
318 })
319 }
320 }
321
322 fn pick_active_window(
333 &self,
334 l0_files: Vec<FileHandle>,
335 l1_files: Vec<FileHandle>,
336 mapper: &PrimaryKeyRangeMapper,
337 ) -> Option<WindowPick> {
338 if l0_files.len() >= self.trigger_file_num {
339 let (inputs, found_runs) = pick_candidate_files(
340 l0_files,
341 self.max_output_file_size,
342 pick_count_first,
343 mapper,
344 );
345 if !inputs.is_empty() {
346 return Some(WindowPick {
347 class: PickClass::L0Merge,
348 branch: "active:pick_count_first",
349 inputs,
350 found_runs,
351 });
352 }
353 }
354 if l1_files.len() < self.active_window_l1_merge_trigger {
355 return None;
356 }
357 let pick = units::pick_active_l1(&l1_files, mapper, self.pick_limits())?;
358 Some(WindowPick {
359 class: PickClass::FileReduction,
360 branch: "active:pick_l1_file_reduction",
361 inputs: pick.inputs,
362 found_runs: pick.units,
363 })
364 }
365
366 fn pick_inactive_window(
390 &self,
391 l0_files: Vec<FileHandle>,
392 l1_files: Vec<FileHandle>,
393 mapper: &PrimaryKeyRangeMapper,
394 settling: bool,
395 quiet: bool,
396 ) -> Option<WindowPick> {
397 let num_l1_files = l1_files.len();
398 if !l0_files.is_empty()
399 && let Some((inputs, found_runs, branch)) = pick_inactive_l0(
400 l0_files.clone(),
401 (!settling).then_some(l1_files.as_slice()),
402 self.max_output_file_size,
403 mapper,
404 )
405 {
406 return Some(WindowPick {
407 class: PickClass::L0Merge,
408 branch,
409 inputs,
410 found_runs,
411 });
412 }
413
414 let mut files = l1_files;
415 files.extend(l0_files);
416 let pick = units::pick_closed_merge(
417 &files,
418 mapper,
419 ClosedMergeOptions {
420 limits: self.closed_merge_limits(),
421 allow_file_reduction: num_l1_files >= self.inactive_window_l1_merge_trigger,
422 allow_conflicts: !settling,
423 allow_unbalanced: (quiet || self.all_windows_inactive) && !settling,
424 },
425 )?;
426 let (class, branch) = if pick.dense_fallback {
427 (PickClass::FileReduction, "inactive:dense_file_reduction")
428 } else if pick.reduces_files {
429 (PickClass::FileReduction, "inactive:pick_l1_file_reduction")
430 } else {
431 (PickClass::OverlapResolution, "inactive:pick_l1_overlap")
432 };
433 Some(WindowPick {
434 class,
435 branch,
436 inputs: pick.inputs,
437 found_runs: pick.units,
438 })
439 }
440
441 fn pick_limits(&self) -> PickLimits {
442 PickLimits {
443 max_output_file_size: self.max_output_file_size,
444 max_input_files: *MAX_INPUT_FILES,
445 }
446 }
447
448 fn closed_merge_limits(&self) -> PickLimits {
449 PickLimits {
450 max_input_files: MAX_INPUT_FILES.saturating_mul(CLOSED_MERGE_INPUT_FACTOR),
451 ..self.pick_limits()
452 }
453 }
454}
455
456fn pick_inactive_l0(
467 l0_files: Vec<FileHandle>,
468 l1_files: Option<&[FileHandle]>,
469 max_output_file_size: Option<u64>,
470 mapper: &PrimaryKeyRangeMapper,
471) -> Option<(Vec<FileHandle>, usize, &'static str)> {
472 let (inputs, found_runs) = pick_candidate_files(
473 l0_files.clone(),
474 max_output_file_size,
475 pick_count_first,
476 mapper,
477 );
478 if !inputs.is_empty() {
479 return Some((inputs, found_runs, "inactive:pick_count_first"));
480 }
481 if let Some(l1_files) = l1_files {
482 let mut all_files = l0_files.clone();
483 all_files.extend_from_slice(l1_files);
484 let (inputs, found_runs) = pick_candidate_files(
485 all_files,
486 max_output_file_size,
487 pick_mixed_within_budget,
488 mapper,
489 );
490 if !inputs.is_empty() {
491 return Some((inputs, found_runs, "inactive:pick_mixed_within_budget"));
492 }
493 }
494 let (inputs, found_runs) = pick_candidate_files(
495 l0_files,
496 max_output_file_size,
497 pick_unbalanced_count_first,
498 mapper,
499 );
500 (!inputs.is_empty()).then_some((inputs, found_runs, "inactive:pick_unbalanced_count_first"))
501}
502
503type CandidatePicker =
504 fn(Vec<SortedRun<FileHandle>>, Option<u64>, &PrimaryKeyRangeMapper) -> Vec<FileHandle>;
505
506fn pick_candidate_files(
507 mut files: Vec<FileHandle>,
508 max_output_file_size: Option<u64>,
509 picker: CandidatePicker,
510 mapper: &PrimaryKeyRangeMapper,
511) -> (Vec<FileHandle>, usize) {
512 let sorted_runs = if files.len() < 1024 {
513 find_sorted_runs(&mut files, |lhs, rhs| files_overlap(lhs, rhs, mapper))
514 } else {
515 find_sorted_runs_by_time_range(&mut files)
516 };
517 let found_runs = sorted_runs.len();
518 (
519 picker(sorted_runs, max_output_file_size, mapper),
520 found_runs,
521 )
522}
523
524#[derive(Debug)]
525struct OrderedFile<'a> {
526 file: &'a FileHandle,
527 run_id: usize,
528 position_in_run: usize,
529 primary_key_range: Option<(bytes::Bytes, bytes::Bytes)>,
531}
532
533impl OrderedFile<'_> {
534 fn overlaps_inclusive(&self, other: &Self) -> bool {
536 self.file.overlap_inclusive(other.file)
537 && match (&self.primary_key_range, &other.primary_key_range) {
538 (Some(lhs), Some(rhs)) => primary_key_ranges_overlap(lhs, rhs),
539 _ => true,
540 }
541 }
542}
543
544#[derive(Debug, Default)]
546struct Candidate {
547 num_files: usize,
549 total_size: usize,
551 largest_file_size: usize,
553 overlap_participants: usize,
556 l0_rows: usize,
558 l1_rows: usize,
560 has_l0: bool,
561 has_l1: bool,
562 has_unknown_rows: bool,
564}
565
566impl Candidate {
567 fn absorb(
571 &mut self,
572 file: &OrderedFile,
573 preceding: &[OrderedFile],
574 participations: &mut Vec<bool>,
575 ) {
576 self.absorb_file(file.file);
577
578 let mut participates = false;
579 for (offset, other) in preceding.iter().enumerate() {
580 if file.run_id != other.run_id && file.overlaps_inclusive(other) {
581 if !participations[offset] {
582 participations[offset] = true;
583 self.overlap_participants += 1;
584 }
585 participates = true;
586 }
587 }
588 participations.push(participates);
589 if participates {
590 self.overlap_participants += 1;
591 }
592 }
593
594 fn absorb_file(&mut self, file: &FileHandle) {
596 self.num_files += 1;
597 let file_size = file.size() as usize;
598 self.total_size += file_size;
599 self.largest_file_size = self.largest_file_size.max(file_size);
600 self.absorb_level_rows(file);
601 }
602
603 fn absorb_level_rows(&mut self, file: &FileHandle) {
604 let num_rows = file.num_rows();
605 self.has_unknown_rows |= num_rows == 0;
606 if file.level() == 0 {
607 self.has_l0 = true;
608 self.l0_rows = self.l0_rows.saturating_add(num_rows);
609 } else {
610 self.has_l1 = true;
611 self.l1_rows = self.l1_rows.saturating_add(num_rows);
612 }
613 }
614
615 fn predicted_output_files(&self, max_output_file_size: Option<u64>) -> usize {
620 match max_output_file_size {
621 Some(max) if max > 0 => self.total_size.div_ceil(max as usize).max(1),
622 _ => 1,
623 }
624 }
625
626 fn file_reduction(&self, max_output_file_size: Option<u64>) -> usize {
630 self.num_files
631 .saturating_sub(self.predicted_output_files(max_output_file_size))
632 }
633
634 fn is_balanced(&self) -> bool {
637 self.largest_file_size <= self.total_size - self.largest_file_size
638 }
639
640 fn has_balanced_level_rows(&self) -> bool {
643 !self.has_l0
644 || !self.has_l1
645 || self.has_unknown_rows
646 || self.l1_rows <= self.l0_rows.saturating_mul(MAX_L1_L0_ROW_RATIO)
647 }
648
649 fn has_mixed_levels(&self) -> bool {
650 self.has_l0 && self.has_l1
651 }
652
653 fn within_rewrite_budget(&self, max_output_file_size: Option<u64>) -> bool {
658 match max_output_file_size {
659 Some(limit) if limit > 0 => self.total_size <= limit as usize,
660 _ => true,
661 }
662 }
663
664 fn makes_progress(&self, max_output_file_size: Option<u64>) -> bool {
669 self.file_reduction(max_output_file_size) > 0 || self.overlap_participants > 0
670 }
671
672 fn score(&self, max_output_file_size: Option<u64>) -> CandidateScore {
673 CandidateScore {
674 file_reduction: self.file_reduction(max_output_file_size),
675 overlap_participants: self.overlap_participants,
676 total_size: self.total_size,
677 }
678 }
679}
680
681#[derive(Debug)]
685struct CandidateScore {
686 file_reduction: usize,
687 overlap_participants: usize,
688 total_size: usize,
689}
690
691impl CandidateScore {
692 fn is_better_than(&self, other: &Self) -> bool {
693 self.file_reduction
694 .cmp(&other.file_reduction)
695 .then_with(|| self.overlap_participants.cmp(&other.overlap_participants))
696 .then_with(|| other.total_size.cmp(&self.total_size))
697 .is_gt()
698 }
699}
700
701fn pick_count_first(
717 sorted_runs: Vec<SortedRun<FileHandle>>,
718 max_output_file_size: Option<u64>,
719 mapper: &PrimaryKeyRangeMapper,
720) -> Vec<FileHandle> {
721 pick_count_first_where(
722 sorted_runs,
723 max_output_file_size,
724 mapper,
725 is_balanced_candidate,
726 )
727}
728
729#[cfg(test)]
730fn pick_mixed_count_first(
731 sorted_runs: Vec<SortedRun<FileHandle>>,
732 max_output_file_size: Option<u64>,
733 mapper: &PrimaryKeyRangeMapper,
734) -> Vec<FileHandle> {
735 pick_count_first_where(sorted_runs, max_output_file_size, mapper, |candidate| {
736 candidate.has_mixed_levels() && is_balanced_candidate(candidate)
737 })
738}
739
740fn pick_mixed_within_budget(
743 sorted_runs: Vec<SortedRun<FileHandle>>,
744 max_output_file_size: Option<u64>,
745 mapper: &PrimaryKeyRangeMapper,
746) -> Vec<FileHandle> {
747 pick_count_first_where(sorted_runs, max_output_file_size, mapper, |candidate| {
748 candidate.has_mixed_levels() && candidate.within_rewrite_budget(max_output_file_size)
749 })
750}
751
752fn pick_unbalanced_count_first(
756 sorted_runs: Vec<SortedRun<FileHandle>>,
757 max_output_file_size: Option<u64>,
758 mapper: &PrimaryKeyRangeMapper,
759) -> Vec<FileHandle> {
760 pick_count_first_where(sorted_runs, max_output_file_size, mapper, |_| true)
761}
762
763fn is_balanced_candidate(candidate: &Candidate) -> bool {
764 candidate.is_balanced() && candidate.has_balanced_level_rows()
765}
766
767fn pick_count_first_where(
768 sorted_runs: Vec<SortedRun<FileHandle>>,
769 max_output_file_size: Option<u64>,
770 mapper: &PrimaryKeyRangeMapper,
771 is_eligible: impl Fn(&Candidate) -> bool,
772) -> Vec<FileHandle> {
773 let files = ordered_files(&sorted_runs, mapper);
774
775 let mut best = None;
776 for left in 0..files.len() {
777 let mut candidate = Candidate::default();
778 let right_bound = left.saturating_add(*MAX_INPUT_FILES).min(files.len());
779 let mut participations: Vec<bool> = Vec::with_capacity(right_bound - left);
780 for right in left..right_bound {
781 candidate.absorb(&files[right], &files[left..right], &mut participations);
782 if candidate.num_files < 2
783 || !is_eligible(&candidate)
784 || !candidate.makes_progress(max_output_file_size)
785 {
786 continue;
787 }
788
789 let score = candidate.score(max_output_file_size);
790 if best
791 .as_ref()
792 .is_none_or(|(best_score, _)| score.is_better_than(best_score))
793 {
794 best = Some((score, &files[left..=right]));
795 }
796 }
797 }
798
799 let Some((_, best)) = best else {
800 return vec![];
801 };
802 best.iter().map(|file| file.file.clone()).collect()
803}
804
805fn ordered_files<'a>(
808 sorted_runs: &'a [SortedRun<FileHandle>],
809 mapper: &PrimaryKeyRangeMapper,
810) -> Vec<OrderedFile<'a>> {
811 let mut files = sorted_runs
812 .iter()
813 .enumerate()
814 .flat_map(|(run_id, run)| {
815 run.items()
816 .iter()
817 .enumerate()
818 .map(move |(position_in_run, file)| OrderedFile {
819 file,
820 run_id,
821 position_in_run,
822 primary_key_range: file.primary_key_range(mapper),
823 })
824 })
825 .collect::<Vec<_>>();
826 files.sort_unstable_by(|lhs, rhs| {
827 let (lhs_start, lhs_end) = lhs.file.range();
828 let (rhs_start, rhs_end) = rhs.file.range();
829 lhs_start
830 .cmp(&rhs_start)
831 .then_with(|| rhs_end.cmp(&lhs_end))
832 .then_with(|| lhs.run_id.cmp(&rhs.run_id))
833 .then_with(|| lhs.position_in_run.cmp(&rhs.position_in_run))
834 });
835 files
836}
837
838fn selected_overlaps_unselected(
839 selected: &[FileHandle],
840 window: &Window,
841 mapper: &PrimaryKeyRangeMapper,
842) -> bool {
843 let Some((span_start, span_end)) = selected
846 .iter()
847 .map(Ranged::range)
848 .reduce(|(start_a, end_a), (start_b, end_b)| (start_a.min(start_b), end_a.max(end_b)))
849 else {
850 return false;
851 };
852 let selected_file_ids = selected
853 .iter()
854 .map(FileHandle::file_id)
855 .collect::<HashSet<_>>();
856 window
857 .files()
858 .filter(|file| {
859 let (start, end) = file.range();
860 start <= span_end && span_start <= end
861 })
862 .filter(|file| !selected_file_ids.contains(&file.file_id()))
863 .any(|unselected| {
864 selected
865 .iter()
866 .any(|selected| files_overlap_inclusive(selected, unselected, mapper))
867 })
868}
869
870fn log_pick_result(
871 region_id: RegionId,
872 window: i64,
873 active_window: Option<i64>,
874 pick: &WindowPick,
875 file_num: usize,
876 max_output_file_size: Option<u64>,
877 filter_deleted: bool,
878) {
879 let input_file_str: Vec<String> = pick
880 .inputs
881 .iter()
882 .map(|f| {
883 let range = f.range();
884 let start = range.0.to_iso8601_string();
885 let end = range.1.to_iso8601_string();
886 let num_rows = f.num_rows();
887 format!(
888 "File{{id: {:?}, range: ({}, {}), size: {}, num rows: {} }}",
889 f.file_id(),
890 start,
891 end,
892 ReadableSize(f.size()),
893 num_rows
894 )
895 })
896 .collect();
897 let window_str = Timestamp::new_second(window).to_iso8601_string();
898 let active_window_str = active_window.map(|s| Timestamp::new_second(s).to_iso8601_string());
899 let max_output_file_size = max_output_file_size.map(|size| ReadableSize(size).to_string());
900 info!(
901 "Region ({:?}) compaction pick result: current window: {}, active window: {:?}, branch: {}, \
902 found runs: {}, file num: {}, max output file size: {:?}, filter deleted: {}, \
903 input files: {:?}",
904 region_id,
905 window_str,
906 active_window_str,
907 pick.branch,
908 pick.found_runs,
909 file_num,
910 max_output_file_size,
911 filter_deleted,
912 input_file_str
913 );
914}
915
916#[async_trait::async_trait]
917impl Picker for TwcsPicker {
918 async fn pick(&self, compaction_region: &CompactionRegion) -> Result<Option<PickerOutput>> {
919 self.pick_with_output_limit(compaction_region, self.max_background_tasks)
920 .await
921 }
922}
923
924impl TwcsPicker {
925 pub(crate) async fn pick_with_output_limit(
926 &self,
927 compaction_region: &CompactionRegion,
928 max_outputs: Option<usize>,
929 ) -> Result<Option<PickerOutput>> {
930 let region_id = compaction_region.region_id;
931 let picker = self.clone();
932 let compaction_region = compaction_region.clone();
933 let primary_key_mapper = compaction_region.current_version.ssts.primary_key_mapper();
934 let window_mapper = primary_key_mapper.clone();
935 let (expired_ssts, time_window_size, active_window, windows) =
936 common_runtime::spawn_blocking_compact(move || {
937 let levels = compaction_region.current_version.ssts.levels();
938 let expired_ssts = get_expired_ssts(
939 levels,
940 compaction_region.ttl,
941 Timestamp::current_millis(),
942 );
943 if !expired_ssts.is_empty() {
944 info!("Expired SSTs in region {}: {:?}", region_id, expired_ssts);
945 }
946 let expired_file_ids = expired_ssts
947 .iter()
948 .map(|file| file.file_id())
949 .collect::<HashSet<_>>();
950
951 let compaction_time_window = compaction_region
952 .current_version
953 .compaction_time_window
954 .map(|window| window.as_secs() as i64);
955 let time_window_size = compaction_time_window
956 .or(picker.time_window_seconds)
957 .unwrap_or_else(|| {
958 let inferred = infer_time_bucket(levels[0].files());
959 info!(
960 "Compaction window for region {} is not present, inferring from files: {:?}",
961 region_id, inferred
962 );
963 inferred
964 });
965
966 let windows = assign_to_windows(
967 levels
968 .iter()
969 .flat_map(LevelMeta::files)
970 .filter(|file| !expired_file_ids.contains(&file.file_id())),
971 time_window_size,
972 &window_mapper,
973 );
974 let active_window = find_active_window_by_sequence(
977 windows.values().flat_map(Window::files),
978 time_window_size,
979 )
980 .or_else(|| {
981 find_latest_window_in_seconds(
982 windows
983 .values()
984 .flat_map(Window::files)
985 .filter(|file| file.level() == 0),
986 time_window_size,
987 )
988 });
989
990 (expired_ssts, time_window_size, active_window, windows)
991 })
992 .await
993 .context(JoinSnafu)?;
994
995 let outputs = self
996 .build_output(
997 region_id,
998 windows,
999 WindowOutputContext {
1000 active_window,
1001 time_window_size: Some(time_window_size),
1002 max_outputs,
1003 primary_key_mapper,
1004 },
1005 )
1006 .await?;
1007
1008 if outputs.is_empty() && expired_ssts.is_empty() {
1009 return Ok(None);
1010 }
1011
1012 let max_file_size = self
1014 .max_output_file_size
1015 .filter(|size| *size > 0)
1016 .map(|size| size as usize);
1017 Ok(Some(PickerOutput {
1018 outputs,
1019 expired_ssts,
1020 time_window_size,
1021 max_file_size,
1022 }))
1023 }
1024}
1025
1026#[derive(Clone)]
1027struct Window {
1028 start: Timestamp,
1029 end: Timestamp,
1030 files: Vec<FileHandle>,
1031 time_window: i64,
1033 primary_key_range: Option<(bytes::Bytes, bytes::Bytes)>,
1034}
1035
1036impl Window {
1037 fn new_with_file(file: FileHandle, mapper: &PrimaryKeyRangeMapper) -> Self {
1039 let (start, end) = file.time_range();
1040 let primary_key_range = file.primary_key_range(mapper);
1041 Self {
1042 start,
1043 end,
1044 files: vec![file],
1045 time_window: 0,
1046 primary_key_range,
1047 }
1048 }
1049
1050 fn range(&self) -> (Timestamp, Timestamp) {
1052 (self.start, self.end)
1053 }
1054
1055 fn add_file(&mut self, file: FileHandle, mapper: &PrimaryKeyRangeMapper) {
1057 let (start, end) = file.time_range();
1058 self.start = self.start.min(start);
1059 self.end = self.end.max(end);
1060 self.primary_key_range = merge_primary_key_ranges(
1061 self.primary_key_range.take(),
1062 file.primary_key_range(mapper),
1063 );
1064 self.files.push(file);
1065 }
1066
1067 fn files(&self) -> impl Iterator<Item = &FileHandle> {
1068 self.files.iter()
1069 }
1070}
1071
1072fn is_settling_window(
1075 window: i64,
1076 active_window: Option<i64>,
1077 time_window_size: Option<i64>,
1078) -> bool {
1079 active_window
1080 .zip(time_window_size)
1081 .is_some_and(|(active, size)| {
1082 active
1084 .checked_sub(window)
1085 .is_some_and(|distance| (1..=size).contains(&distance))
1086 })
1087}
1088
1089fn is_quiet_window(files: &[FileHandle], region_sequence: u64) -> bool {
1095 if files.iter().any(|file| file.num_rows() == 0) {
1096 return false;
1097 }
1098 let Some(window_sequence) = files
1099 .iter()
1100 .filter_map(|file| file.meta_ref().sequence)
1101 .map(|sequence| sequence.get())
1102 .max()
1103 else {
1104 return false;
1105 };
1106 let rows: u64 = files.iter().map(|file| file.num_rows() as u64).sum();
1107 region_sequence.saturating_sub(window_sequence) >= rows.max(1)
1108}
1109
1110fn assign_to_windows<'a>(
1112 files: impl Iterator<Item = &'a FileHandle>,
1113 time_window_size: i64,
1114 mapper: &PrimaryKeyRangeMapper,
1115) -> BTreeMap<i64, Window> {
1116 let mut windows: HashMap<i64, Window> = HashMap::new();
1117 for f in files {
1119 if f.compacting() {
1120 continue;
1121 }
1122 let (_, end) = f.time_range();
1123 let time_window = time_window_start(end, time_window_size);
1124
1125 match windows.entry(time_window) {
1126 Entry::Occupied(mut e) => {
1127 e.get_mut().add_file(f.clone(), mapper);
1128 }
1129 Entry::Vacant(e) => {
1130 let mut window = Window::new_with_file(f.clone(), mapper);
1131 window.time_window = time_window;
1132 e.insert(window);
1133 }
1134 }
1135 }
1136 windows.into_iter().collect()
1137}
1138
1139fn time_window_start(timestamp: Timestamp, time_window_size: i64) -> i64 {
1142 timestamp
1144 .convert_to(TimeUnit::Second)
1145 .and_then(|ts| ts.value().align_by_bucket(time_window_size))
1146 .unwrap_or(i64::MIN)
1147}
1148
1149fn time_window_intersects_range(
1150 window_start: i64,
1151 time_window_size: i64,
1152 time_range: &TimestampRange,
1153) -> bool {
1154 if time_range.is_empty() {
1155 return false;
1156 }
1157 let first_window = time_range
1158 .start()
1159 .map_or(i64::MIN, |start| time_window_start(start, time_window_size));
1160 let last_window = match time_range.end() {
1161 None => i64::MAX,
1162 Some(end) => {
1163 let Some(last_second) = end
1165 .convert_to_ceil(TimeUnit::Second)
1166 .and_then(|timestamp| timestamp.value().checked_sub(1))
1167 else {
1168 return false;
1169 };
1170 time_window_start(Timestamp::new_second(last_second), time_window_size)
1171 }
1172 };
1173 (first_window..=last_window).contains(&window_start)
1174}
1175
1176fn window_has_overlap(this: &Window, windows: &BTreeMap<i64, Window>) -> bool {
1177 windows
1178 .values()
1179 .filter(|that| this.time_window != that.time_window)
1180 .any(|that| {
1181 overlaps(&this.range(), &that.range()) && {
1182 match (&this.primary_key_range, &that.primary_key_range) {
1183 (Some(l), Some(r)) => primary_key_ranges_overlap(l, r),
1184 _ => true,
1185 }
1186 }
1187 })
1188}
1189
1190fn find_latest_window_in_seconds<'a>(
1193 files: impl Iterator<Item = &'a FileHandle>,
1194 time_window_size: i64,
1195) -> Option<i64> {
1196 let mut latest_timestamp = None;
1197 for f in files {
1198 let (_, end) = f.time_range();
1199 if let Some(latest) = latest_timestamp {
1200 if end > latest {
1201 latest_timestamp = Some(end);
1202 }
1203 } else {
1204 latest_timestamp = Some(end);
1205 }
1206 }
1207 latest_timestamp.map(|ts| time_window_start(ts, time_window_size))
1208}
1209
1210fn find_active_window_by_sequence<'a>(
1219 files: impl Iterator<Item = &'a FileHandle>,
1220 time_window_size: i64,
1221) -> Option<i64> {
1222 files
1223 .filter(|f| f.meta_ref().sequence.is_some())
1224 .max_by_key(|f| f.meta_ref().sequence)
1225 .map(|f| time_window_start(f.time_range().1, time_window_size))
1226}
1227
1228#[cfg(test)]
1229mod tests {
1230 use std::collections::HashSet;
1231 use std::num::NonZeroU64;
1232 use std::sync::Arc;
1233 use std::time::Duration;
1234
1235 use common_base::Plugins;
1236 use common_time::range::TimestampRange;
1237 use store_api::storage::FileId;
1238
1239 use super::*;
1240 use crate::cache::CacheManager;
1241 use crate::compaction::compactor::CompactionVersion;
1242 use crate::compaction::test_util::{
1243 compaction_region_with_ssts, new_file_handle, new_file_handle_with_sequence,
1244 new_file_handle_with_size_and_sequence,
1245 new_file_handle_with_size_sequence_and_primary_key_range, pk_range,
1246 primary_key_mapper_for_test,
1247 };
1248 use crate::config::MitoConfig;
1249 use crate::region::options::RegionOptions;
1250 use crate::sst::file::{FileMeta, Level};
1251 use crate::sst::version::SstVersion;
1252 use crate::test_util::memtable_util::metadata_for_test;
1253 use crate::test_util::scheduler_util::SchedulerEnv;
1254
1255 fn assign_to_windows<'a>(
1256 files: impl Iterator<Item = &'a FileHandle>,
1257 time_window_size: i64,
1258 ) -> BTreeMap<i64, Window> {
1259 super::assign_to_windows(files, time_window_size, &primary_key_mapper_for_test())
1260 }
1261
1262 fn pick_count_first(
1263 sorted_runs: Vec<SortedRun<FileHandle>>,
1264 max_output_file_size: Option<u64>,
1265 ) -> Vec<FileHandle> {
1266 super::pick_count_first(
1267 sorted_runs,
1268 max_output_file_size,
1269 &primary_key_mapper_for_test(),
1270 )
1271 }
1272
1273 fn pick_mixed_count_first(
1274 sorted_runs: Vec<SortedRun<FileHandle>>,
1275 max_output_file_size: Option<u64>,
1276 ) -> Vec<FileHandle> {
1277 super::pick_mixed_count_first(
1278 sorted_runs,
1279 max_output_file_size,
1280 &primary_key_mapper_for_test(),
1281 )
1282 }
1283
1284 impl TwcsPicker {
1285 async fn build_output_with_time_range(
1286 &self,
1287 region_id: RegionId,
1288 time_windows: BTreeMap<i64, Window>,
1289 active_window: Option<i64>,
1290 time_window_size: Option<i64>,
1291 ) -> Result<Vec<CompactionOutput>> {
1292 self.build_output(
1293 region_id,
1294 time_windows,
1295 WindowOutputContext {
1296 active_window,
1297 time_window_size,
1298 max_outputs: self.max_background_tasks,
1299 primary_key_mapper: Arc::new(primary_key_mapper_for_test()),
1300 },
1301 )
1302 .await
1303 }
1304 }
1305
1306 #[test]
1307 fn test_cross_schema_pk_overlap_before_window_aggregation() {
1308 use crate::test_util::sst_util::{new_primary_key, sst_region_metadata};
1309
1310 let metadata = Arc::new(sst_region_metadata());
1311 let mut old = FileMeta {
1312 region_id: metadata.region_id,
1313 file_id: FileId::random(),
1314 time_range: (
1315 Timestamp::new_millisecond(0),
1316 Timestamp::new_millisecond(1000),
1317 ),
1318 primary_key_min: Some(new_primary_key(&["a"]).into()),
1319 primary_key_max: Some(new_primary_key(&["b"]).into()),
1320 ..Default::default()
1321 };
1322 let mut completed = new_primary_key(&["b"]);
1323 completed.push(0); let new = FileMeta {
1325 file_id: FileId::random(),
1326 time_range: (
1327 Timestamp::new_millisecond(500),
1328 Timestamp::new_millisecond(2000),
1329 ),
1330 primary_key_min: Some(completed.clone().into()),
1331 primary_key_max: Some(completed.clone().into()),
1332 ..old.clone()
1333 };
1334 assert!(old.primary_key_max < new.primary_key_min);
1335 let mut ssts = SstVersion::new(metadata);
1336 let old_id = old.file_id;
1337 let new_id = new.file_id;
1338 ssts.add_files(
1339 crate::test_util::new_noop_file_purger(),
1340 [old.clone(), new].into_iter(),
1341 );
1342 let old_file = &ssts.levels()[0].files[&old_id];
1343 let new_file = &ssts.levels()[0].files[&new_id];
1344 let ranges = ssts.primary_key_mapper();
1345 let windows = super::assign_to_windows([old_file, new_file].into_iter(), 1, &ranges);
1346 assert_eq!(2, windows.len());
1347 assert!(
1348 windows
1349 .values()
1350 .all(|window| window_has_overlap(window, &windows))
1351 );
1352
1353 let mut window = Window::new_with_file(old_file.clone(), &ranges);
1354 window.add_file(new_file.clone(), &ranges);
1355 assert!(selected_overlaps_unselected(
1356 std::slice::from_ref(new_file),
1357 &window,
1358 &ranges,
1359 ));
1360 assert_eq!(
1361 Some(completed.into()),
1362 window
1363 .primary_key_range
1364 .as_ref()
1365 .map(|range| range.1.clone())
1366 );
1367
1368 old.file_id = FileId::random();
1370 old.primary_key_max = old.primary_key_min.clone();
1371 let disjoint_id = old.file_id;
1372 ssts.add_files(crate::test_util::new_noop_file_purger(), [old].into_iter());
1373 let files = &ssts.levels()[0].files;
1374 assert!(!files_overlap_inclusive(
1375 &files[&disjoint_id],
1376 &files[&new_id],
1377 &ranges
1378 ));
1379 }
1380
1381 #[test]
1382 fn test_valid_max_input_files_env_overrides_default() {
1383 assert_eq!(64, parse_max_input_files(Some("64")));
1384 }
1385
1386 #[test]
1387 fn test_invalid_max_input_files_env_falls_back_to_default() {
1388 for env_value in [None, Some(""), Some("invalid"), Some("0"), Some("1")] {
1389 assert_eq!(16, parse_max_input_files(env_value));
1390 }
1391 }
1392
1393 async fn compaction_region_with_expired_sst() -> CompactionRegion {
1394 compaction_region_with_ssts(
1395 (1..=4).map(|sequence| FileMeta {
1396 file_id: FileId::random(),
1397 time_range: (
1398 Timestamp::new_millisecond(0),
1399 Timestamp::new_millisecond(10),
1400 ),
1401 level: 0,
1402 sequence: NonZeroU64::new(sequence),
1403 ..Default::default()
1404 }),
1405 Duration::from_millis(1),
1406 )
1407 .await
1408 }
1409
1410 #[tokio::test]
1411 async fn test_pick_normalizes_zero_output_size_to_unlimited() {
1412 let mut compaction_region = compaction_region_with_ssts(
1413 (1..=4).map(|sequence| new_file(0, 10, sequence, 100).meta_ref().clone()),
1414 Duration::from_secs(3600),
1415 )
1416 .await;
1417 compaction_region.ttl = None;
1418
1419 for (max_output_file_size, expected_max_file_size) in
1420 [(Some(0), None), (None, None), (Some(1024), Some(1024))]
1421 {
1422 let picker = TwcsPicker {
1423 trigger_file_num: 4,
1424 active_window_l1_merge_trigger: 8,
1425 inactive_window_trigger_file_num: 4,
1426 inactive_window_l1_merge_trigger: 8,
1427 time_window_seconds: Some(3),
1428 max_output_file_size,
1429 append_mode: false,
1430 merge_mode: MergeMode::LastRow,
1431 max_background_tasks: None,
1432 time_range: None,
1433 all_windows_inactive: false,
1434 };
1435
1436 let output = picker.pick(&compaction_region).await.unwrap().unwrap();
1437
1438 assert_eq!(output.outputs.len(), 1);
1439 assert_eq!(output.outputs[0].inputs.len(), 4);
1440 assert!(output.expired_ssts.is_empty());
1441 assert_eq!(output.max_file_size, expected_max_file_size);
1442 }
1443 }
1444
1445 #[tokio::test]
1446 async fn test_pick_expired_ssts_without_marking_compacting() {
1447 let picker = TwcsPicker {
1448 trigger_file_num: 4,
1449 active_window_l1_merge_trigger: 8,
1450 inactive_window_trigger_file_num: 4,
1451 inactive_window_l1_merge_trigger: 8,
1452 time_window_seconds: Some(3),
1453 max_output_file_size: None,
1454 append_mode: false,
1455 merge_mode: MergeMode::LastRow,
1456 max_background_tasks: None,
1457 time_range: None,
1458 all_windows_inactive: false,
1459 };
1460 let compaction_region = compaction_region_with_expired_sst().await;
1461
1462 let output = picker.pick(&compaction_region).await.unwrap().unwrap();
1463
1464 assert!(output.outputs.is_empty());
1465 assert!(!output.expired_ssts.is_empty());
1466 assert!(output.expired_ssts.iter().all(|file| !file.compacting()));
1467 }
1468
1469 #[tokio::test]
1470 async fn test_expired_sst_does_not_determine_active_window() {
1471 let now = Timestamp::current_millis().value();
1472 let files = [
1473 FileMeta {
1474 file_id: FileId::random(),
1475 time_range: (
1476 Timestamp::new_millisecond(0),
1477 Timestamp::new_millisecond(10),
1478 ),
1479 level: 0,
1480 sequence: NonZeroU64::new(100),
1481 ..Default::default()
1482 },
1483 FileMeta {
1484 file_id: FileId::random(),
1485 time_range: (
1486 Timestamp::new_millisecond(now - 1000),
1487 Timestamp::new_millisecond(now),
1488 ),
1489 level: 0,
1490 sequence: NonZeroU64::new(1),
1491 ..Default::default()
1492 },
1493 FileMeta {
1494 file_id: FileId::random(),
1495 time_range: (
1496 Timestamp::new_millisecond(now - 1000),
1497 Timestamp::new_millisecond(now),
1498 ),
1499 level: 0,
1500 sequence: NonZeroU64::new(2),
1501 ..Default::default()
1502 },
1503 ];
1504 let compaction_region = compaction_region_with_ssts(files, Duration::from_secs(60)).await;
1505 let picker = TwcsPicker {
1506 trigger_file_num: 4,
1507 active_window_l1_merge_trigger: 8,
1508 inactive_window_trigger_file_num: 2,
1509 inactive_window_l1_merge_trigger: 8,
1510 time_window_seconds: Some(3),
1511 max_output_file_size: None,
1512 append_mode: false,
1513 merge_mode: MergeMode::LastRow,
1514 max_background_tasks: None,
1515 time_range: None,
1516 all_windows_inactive: false,
1517 };
1518
1519 let output = picker.pick(&compaction_region).await.unwrap().unwrap();
1520
1521 assert_eq!(1, output.expired_ssts.len());
1522 assert!(output.outputs.is_empty());
1523 }
1524
1525 #[tokio::test]
1526 async fn test_active_window_survives_l0_consumption() {
1527 let env = SchedulerEnv::new().await;
1532 let metadata = metadata_for_test();
1533 let manifest_ctx = env.mock_manifest_context(metadata.clone()).await;
1534 let mut ssts = SstVersion::new(metadata.clone());
1535 ssts.add_files(
1536 Arc::new(crate::sst::file_purger::NoopFilePurger),
1537 [100, 101].into_iter().map(|sequence| FileMeta {
1538 region_id: metadata.region_id,
1539 file_id: FileId::random(),
1540 time_range: (
1541 Timestamp::new_millisecond(0),
1542 Timestamp::new_millisecond(10),
1543 ),
1544 level: 1,
1545 sequence: NonZeroU64::new(sequence),
1546 ..Default::default()
1547 }),
1548 );
1549 let compaction_region = CompactionRegion {
1550 region_id: metadata.region_id,
1551 region_options: RegionOptions::default(),
1552 engine_config: Arc::new(MitoConfig::default()),
1553 region_metadata: metadata.clone(),
1554 cache_manager: Arc::new(CacheManager::default()),
1555 access_layer: env.access_layer,
1556 manifest_ctx,
1557 current_version: CompactionVersion {
1558 metadata,
1559 options: RegionOptions::default(),
1560 ssts: Arc::new(ssts),
1561 memtable_min_sequence: None,
1562 compaction_time_window: None,
1563 },
1564 file_purger: None,
1565 ttl: None,
1566 max_parallelism: 1,
1567 plugins: Plugins::new(),
1568 };
1569 let picker = TwcsPicker {
1570 trigger_file_num: 4,
1571 active_window_l1_merge_trigger: 8,
1572 inactive_window_trigger_file_num: 2,
1573 inactive_window_l1_merge_trigger: 8,
1574 time_window_seconds: Some(3600),
1575 max_output_file_size: None,
1576 append_mode: false,
1577 merge_mode: MergeMode::LastRow,
1578 max_background_tasks: None,
1579 time_range: None,
1580 all_windows_inactive: false,
1581 };
1582
1583 assert!(picker.pick(&compaction_region).await.unwrap().is_none());
1584 }
1585
1586 #[test]
1587 fn test_find_active_window_by_sequence() {
1588 let files = [
1593 new_file_handle_with_sequence(FileId::random(), 0, 999, 0, 1),
1594 new_file_handle_with_sequence(FileId::random(), 2000, 2999, 1, 10),
1595 ];
1596 assert_eq!(Some(2), find_active_window_by_sequence(files.iter(), 1));
1597
1598 let files = [
1601 new_file_handle_with_sequence(FileId::random(), 0, 999, 0, 10),
1602 new_file_handle_with_sequence(FileId::random(), 2000, 2999, 0, 1),
1603 ];
1604 assert_eq!(Some(0), find_active_window_by_sequence(files.iter(), 1));
1605
1606 let files = [new_file_handle_with_sequence(
1609 FileId::random(),
1610 0,
1611 999,
1612 1,
1613 0,
1614 )];
1615 assert_eq!(None, find_active_window_by_sequence(files.iter(), 1));
1616 assert!(find_active_window_by_sequence(Vec::<FileHandle>::new().iter(), 1).is_none());
1617 }
1618
1619 #[test]
1620 fn test_active_window_falls_back_to_l0_rule_without_sequence() {
1621 let active_window_of = |files: &[FileHandle]| {
1622 find_active_window_by_sequence(files.iter(), 1).or_else(|| {
1623 find_latest_window_in_seconds(files.iter().filter(|f| f.level() == 0), 1)
1624 })
1625 };
1626
1627 let files = [
1629 new_file_handle_with_sequence(FileId::random(), 0, 999, 1, 0),
1630 new_file_handle_with_sequence(FileId::random(), 2000, 2999, 0, 0),
1631 ];
1632 assert_eq!(Some(2), active_window_of(&files));
1633
1634 let files = [new_file_handle_with_sequence(
1636 FileId::random(),
1637 0,
1638 999,
1639 1,
1640 0,
1641 )];
1642 assert_eq!(None, active_window_of(&files));
1643 }
1644
1645 #[test]
1646 fn test_get_latest_window_in_seconds() {
1647 assert_eq!(
1648 Some(0),
1649 find_latest_window_in_seconds([new_file_handle(FileId::random(), 0, 999, 0)].iter(), 1)
1650 );
1651 assert_eq!(
1652 Some(1),
1653 find_latest_window_in_seconds(
1654 [new_file_handle(FileId::random(), 0, 1000, 0)].iter(),
1655 1
1656 )
1657 );
1658
1659 assert_eq!(
1660 Some(-9223372036857600),
1661 find_latest_window_in_seconds(
1662 [new_file_handle(FileId::random(), i64::MIN, i64::MIN + 1, 0)].iter(),
1663 3600,
1664 )
1665 );
1666
1667 assert_eq!(
1668 (i64::MAX / 10000000) * 10000,
1669 find_latest_window_in_seconds(
1670 [new_file_handle(FileId::random(), i64::MIN, i64::MAX, 0)].iter(),
1671 10000,
1672 )
1673 .unwrap()
1674 );
1675
1676 assert_eq!(
1677 Some((i64::MAX / 3600000) * 3600),
1678 find_latest_window_in_seconds(
1679 [
1680 new_file_handle(FileId::random(), i64::MIN, i64::MAX, 0),
1681 new_file_handle(FileId::random(), 0, 1000, 0)
1682 ]
1683 .iter(),
1684 3600
1685 )
1686 );
1687 }
1688
1689 #[test]
1690 fn test_window_assignment_and_activity_at_boundaries() {
1691 for (unit, ticks_per_second) in [
1694 (TimeUnit::Second, 1),
1695 (TimeUnit::Millisecond, 1_000),
1696 (TimeUnit::Microsecond, 1_000_000),
1697 (TimeUnit::Nanosecond, 1_000_000_000),
1698 ] {
1699 for window_size in [1, 3, 86_400] {
1700 for boundary in [-window_size, 0, window_size] {
1701 for (offset, expected_window) in [
1702 (-1, boundary - window_size),
1703 (0, boundary),
1704 (ticks_per_second - 1, boundary),
1705 (ticks_per_second, boundary + i64::from(window_size == 1)),
1706 ] {
1707 let timestamp = Timestamp::new(boundary * ticks_per_second + offset, unit);
1708 let file = FileHandle::new(
1709 FileMeta {
1710 time_range: (timestamp, timestamp),
1711 sequence: NonZeroU64::new(1),
1712 ..Default::default()
1713 },
1714 crate::test_util::new_noop_file_purger(),
1715 );
1716 let files = [file];
1717 let windows = assign_to_windows(files.iter(), window_size);
1718 assert_eq!(
1719 vec![expected_window],
1720 windows.keys().copied().collect::<Vec<_>>(),
1721 "{timestamp:?}, window size {window_size}"
1722 );
1723 assert_eq!(
1724 Some(expected_window),
1725 find_active_window_by_sequence(files.iter(), window_size)
1726 );
1727 assert_eq!(
1728 Some(expected_window),
1729 find_latest_window_in_seconds(files.iter(), window_size)
1730 );
1731 }
1732 }
1733 }
1734 }
1735 }
1736
1737 #[test]
1738 fn test_settling_window_at_timestamp_limits() {
1739 assert!(is_settling_window(0, Some(60), Some(60)));
1740 assert!(!is_settling_window(60, Some(60), Some(60)));
1741 assert!(!is_settling_window(-60, Some(60), Some(60)));
1742 assert!(!is_settling_window(0, None, Some(60)));
1743 assert!(!is_settling_window(i64::MAX, Some(i64::MIN), Some(1)));
1745 assert!(!is_settling_window(i64::MIN, Some(i64::MAX), Some(1)));
1746 }
1747
1748 #[test]
1749 fn test_window_assignment_at_timestamp_limits() {
1750 for (seconds, expected_window) in [(i64::MIN, i64::MIN), (i64::MAX, i64::MAX - 1)] {
1751 let timestamp = Timestamp::new_second(seconds);
1752 let files = [FileHandle::new(
1753 FileMeta {
1754 time_range: (timestamp, timestamp),
1755 sequence: NonZeroU64::new(1),
1756 ..Default::default()
1757 },
1758 crate::test_util::new_noop_file_purger(),
1759 )];
1760 let windows = assign_to_windows(files.iter(), 3);
1761 assert_eq!(
1762 vec![expected_window],
1763 windows.keys().copied().collect::<Vec<_>>()
1764 );
1765 assert_eq!(
1766 Some(expected_window),
1767 find_active_window_by_sequence(files.iter(), 3)
1768 );
1769 assert_eq!(
1770 Some(expected_window),
1771 find_latest_window_in_seconds(files.iter(), 3)
1772 );
1773 assert!(time_window_intersects_range(
1774 expected_window,
1775 3,
1776 &TimestampRange::single(timestamp)
1777 ));
1778 }
1779 }
1780
1781 #[tokio::test]
1782 async fn test_midnight_file_is_not_compacted_with_previous_day() {
1783 let midnight = 86_400_000;
1785 CompactionPickerTestCase {
1786 window_size: 86_400,
1787 input_files: [
1788 (midnight - 240_000, midnight - 180_000),
1789 (midnight - 180_000, midnight - 90_000),
1790 (midnight - 90_000, midnight - 30_000),
1791 (midnight, midnight),
1792 ]
1793 .into_iter()
1794 .enumerate()
1795 .map(|(index, (start, end))| {
1796 new_file_handle_with_sequence(FileId::random(), start, end, 0, index as u64 + 1)
1797 })
1798 .collect(),
1799 expected_outputs: vec![ExpectedOutput {
1800 input_files: vec![0, 1, 2],
1801 output_level: 1,
1802 }],
1803 }
1804 .check()
1805 .await;
1806 }
1807
1808 #[test]
1809 fn test_assign_to_windows() {
1810 let windows = assign_to_windows(
1811 [
1812 new_file_handle(FileId::random(), 0, 999, 0),
1813 new_file_handle(FileId::random(), 0, 999, 0),
1814 new_file_handle(FileId::random(), 0, 999, 0),
1815 new_file_handle(FileId::random(), 0, 999, 0),
1816 new_file_handle(FileId::random(), 0, 999, 0),
1817 ]
1818 .iter(),
1819 3,
1820 );
1821 let fgs = &windows.get(&0).unwrap().files;
1822 assert_eq!(5, fgs.len());
1823
1824 let files = [FileId::random(); 3];
1825 let windows = assign_to_windows(
1826 [
1827 new_file_handle(files[0], -2000, -3, 0),
1828 new_file_handle(files[1], 0, 2999, 0),
1829 new_file_handle(files[2], 50, 10001, 0),
1830 ]
1831 .iter(),
1832 3,
1833 );
1834 assert_eq!(
1835 files[0],
1836 windows
1837 .get(&-3)
1838 .unwrap()
1839 .files()
1840 .next()
1841 .unwrap()
1842 .file_id()
1843 .file_id()
1844 );
1845 assert_eq!(
1846 files[1],
1847 windows
1848 .get(&0)
1849 .unwrap()
1850 .files()
1851 .next()
1852 .unwrap()
1853 .file_id()
1854 .file_id()
1855 );
1856 assert_eq!(
1857 files[2],
1858 windows
1859 .get(&9)
1860 .unwrap()
1861 .files()
1862 .next()
1863 .unwrap()
1864 .file_id()
1865 .file_id()
1866 );
1867 }
1868
1869 #[test]
1870 fn test_assign_files_to_windows() {
1871 let files = [
1872 FileId::random(),
1873 FileId::random(),
1874 FileId::random(),
1875 FileId::random(),
1876 ];
1877 let windows = assign_to_windows(
1878 [
1879 new_file_handle_with_sequence(files[0], 0, 999, 0, 1),
1880 new_file_handle_with_sequence(files[1], 0, 999, 0, 1),
1881 new_file_handle_with_sequence(files[2], 0, 999, 0, 2),
1882 new_file_handle_with_sequence(files[3], 0, 999, 0, 2),
1883 ]
1884 .iter(),
1885 3,
1886 );
1887 assert_eq!(windows.len(), 1);
1888 let window_files = &windows.get(&0).unwrap().files;
1889 assert_eq!(4, window_files.len());
1890 assert_eq!(
1891 window_files
1892 .iter()
1893 .map(|f| f.file_id().file_id())
1894 .collect::<HashSet<_>>(),
1895 files.into_iter().collect()
1896 );
1897 }
1898
1899 #[test]
1900 fn test_assign_compacting_to_windows() {
1901 let files = [
1902 new_file_handle(FileId::random(), 0, 999, 0),
1903 new_file_handle(FileId::random(), 0, 999, 0),
1904 new_file_handle(FileId::random(), 0, 999, 0),
1905 new_file_handle(FileId::random(), 0, 999, 0),
1906 new_file_handle(FileId::random(), 0, 999, 0),
1907 ];
1908 files[0].set_compacting(true);
1909 files[2].set_compacting(true);
1910 let mut windows = assign_to_windows(files.iter(), 3);
1911 let window0 = windows.remove(&0).unwrap();
1912 assert_eq!(3, window0.files.len());
1913 let candidates = window0
1914 .files
1915 .iter()
1916 .map(|f| f.file_id().file_id())
1917 .collect::<HashSet<_>>();
1918 assert_eq!(candidates.len(), 3);
1919 assert_eq!(
1920 candidates,
1921 [
1922 files[1].file_id().file_id(),
1923 files[3].file_id().file_id(),
1924 files[4].file_id().file_id()
1925 ]
1926 .into_iter()
1927 .collect::<HashSet<_>>()
1928 );
1929 }
1930
1931 type ExpectedWindowSpec = (i64, bool, Vec<(i64, i64)>);
1933
1934 fn check_assign_to_windows_with_overlapping(
1935 file_time_ranges: &[(i64, i64)],
1936 time_window: i64,
1937 expected_files: &[ExpectedWindowSpec],
1938 ) {
1939 let files: Vec<_> = (0..file_time_ranges.len())
1940 .map(|_| FileId::random())
1941 .collect();
1942
1943 let file_handles = files
1944 .iter()
1945 .zip(file_time_ranges.iter())
1946 .map(|(file_id, range)| new_file_handle(*file_id, range.0, range.1, 0))
1947 .collect::<Vec<_>>();
1948
1949 let windows = assign_to_windows(file_handles.iter(), time_window);
1950 assert_eq!(expected_files.len(), windows.len());
1951
1952 for (expected_window, overlapping, window_files) in expected_files {
1953 let actual_window = windows.get(expected_window).unwrap();
1954 let actual_overlapping = window_has_overlap(actual_window, &windows);
1955 assert_eq!(*overlapping, actual_overlapping);
1956 let mut file_ranges = actual_window
1957 .files
1958 .iter()
1959 .map(|f| {
1960 let (s, e) = f.time_range();
1961 (s.value(), e.value())
1962 })
1963 .collect::<Vec<_>>();
1964 file_ranges.sort_unstable_by(|l, r| l.0.cmp(&r.0).then(l.1.cmp(&r.1)));
1965 assert_eq!(window_files, &file_ranges);
1966 }
1967 }
1968
1969 #[test]
1970 fn test_assign_to_windows_with_overlapping() {
1971 check_assign_to_windows_with_overlapping(
1972 &[(0, 999), (1000, 1999), (2000, 2999)],
1973 2,
1974 &[
1975 (0, false, vec![(0, 999), (1000, 1999)]),
1976 (2, false, vec![(2000, 2999)]),
1977 ],
1978 );
1979
1980 check_assign_to_windows_with_overlapping(
1981 &[(0, 1), (0, 999), (100, 2999)],
1982 2,
1983 &[
1984 (0, true, vec![(0, 1), (0, 999)]),
1985 (2, true, vec![(100, 2999)]),
1986 ],
1987 );
1988
1989 check_assign_to_windows_with_overlapping(
1990 &[(0, 999), (1000, 1999), (2000, 2999), (3000, 3999)],
1991 2,
1992 &[
1993 (0, false, vec![(0, 999), (1000, 1999)]),
1994 (2, false, vec![(2000, 2999), (3000, 3999)]),
1995 ],
1996 );
1997
1998 check_assign_to_windows_with_overlapping(
1999 &[
2000 (0, 999),
2001 (1000, 1999),
2002 (2000, 2999),
2003 (3000, 3999),
2004 (0, 3999),
2005 ],
2006 2,
2007 &[
2008 (0, true, vec![(0, 999), (1000, 1999)]),
2009 (2, true, vec![(0, 3999), (2000, 2999), (3000, 3999)]),
2010 ],
2011 );
2012
2013 check_assign_to_windows_with_overlapping(
2014 &[
2015 (0, 999),
2016 (1000, 1999),
2017 (2000, 2999),
2018 (3000, 3999),
2019 (1999, 3999),
2020 ],
2021 2,
2022 &[
2023 (0, true, vec![(0, 999), (1000, 1999)]),
2024 (2, true, vec![(1999, 3999), (2000, 2999), (3000, 3999)]),
2025 ],
2026 );
2027
2028 check_assign_to_windows_with_overlapping(
2029 &[
2030 (0, 999), (1000, 1999), (2000, 2999), (3000, 3999), (2999, 3999), ],
2036 2,
2037 &[
2038 (0, false, vec![(0, 999), (1000, 1999)]),
2040 (2, false, vec![(2000, 2999), (2999, 3999), (3000, 3999)]),
2041 ],
2042 );
2043
2044 check_assign_to_windows_with_overlapping(
2045 &[
2046 (0, 999), (1000, 1999), (2000, 2999), (3000, 3999), (0, 2000), ],
2052 2,
2053 &[
2054 (0, true, vec![(0, 999), (1000, 1999)]),
2055 (2, true, vec![(0, 2000), (2000, 2999), (3000, 3999)]),
2056 ],
2057 );
2058 }
2059
2060 #[test]
2061 fn test_assign_to_windows_not_overlapping_when_pk_disjoint() {
2062 let files = [
2063 new_file_handle_with_size_sequence_and_primary_key_range(
2064 FileId::random(),
2065 0,
2066 1000,
2067 0,
2068 1,
2069 10,
2070 pk_range(b"a", b"f"),
2071 ),
2072 new_file_handle_with_size_sequence_and_primary_key_range(
2073 FileId::random(),
2074 500,
2075 2999,
2076 0,
2077 2,
2078 10,
2079 pk_range(b"x", b"z"),
2080 ),
2081 ];
2082
2083 let windows = assign_to_windows(files.iter(), 2);
2084
2085 assert_eq!(2, windows.len());
2086 let overlapping = window_has_overlap(windows.get(&0).unwrap(), &windows);
2087 assert!(!overlapping);
2088 }
2089
2090 #[test]
2091 fn test_assign_to_windows_pk_unknown_in_earlier_window_does_not_poison_later_windows() {
2092 let files = [
2093 new_file_handle(FileId::random(), 0, 1999, 0),
2094 new_file_handle_with_size_sequence_and_primary_key_range(
2095 FileId::random(),
2096 2000,
2097 3999,
2098 0,
2099 1,
2100 10,
2101 pk_range(b"a", b"f"),
2102 ),
2103 new_file_handle_with_size_sequence_and_primary_key_range(
2104 FileId::random(),
2105 3000,
2106 4999,
2107 0,
2108 2,
2109 10,
2110 pk_range(b"x", b"z"),
2111 ),
2112 ];
2113
2114 let windows = assign_to_windows(files.iter(), 2);
2115
2116 let overlapping = window_has_overlap(windows.get(&4).unwrap(), &windows);
2117 assert!(!overlapping);
2118 }
2119
2120 struct CompactionPickerTestCase {
2121 window_size: i64,
2122 input_files: Vec<FileHandle>,
2123 expected_outputs: Vec<ExpectedOutput>,
2124 }
2125
2126 impl CompactionPickerTestCase {
2127 async fn check(&self) {
2128 let file_id_to_idx = self
2129 .input_files
2130 .iter()
2131 .enumerate()
2132 .map(|(idx, file)| (file.file_id(), idx))
2133 .collect::<HashMap<_, _>>();
2134 let windows = assign_to_windows(self.input_files.iter(), self.window_size);
2135 let active_window =
2136 find_active_window_by_sequence(self.input_files.iter(), self.window_size).or_else(
2137 || find_latest_window_in_seconds(self.input_files.iter(), self.window_size),
2138 );
2139 let output = TwcsPicker {
2140 trigger_file_num: 2,
2141 active_window_l1_merge_trigger: 8,
2142 inactive_window_trigger_file_num: 2,
2143 inactive_window_l1_merge_trigger: 2,
2144 time_window_seconds: None,
2145 max_output_file_size: None,
2146 append_mode: false,
2147 merge_mode: MergeMode::LastRow,
2148 max_background_tasks: None,
2149 time_range: None,
2150 all_windows_inactive: false,
2151 }
2152 .build_output_with_time_range(RegionId::from_u64(0), windows, active_window, None)
2153 .await
2154 .unwrap();
2155
2156 let output = output
2157 .iter()
2158 .map(|o| {
2159 let input_file_ids = o
2160 .inputs
2161 .iter()
2162 .map(|f| file_id_to_idx.get(&f.file_id()).copied().unwrap())
2163 .collect::<HashSet<_>>();
2164 (input_file_ids, o.output_level)
2165 })
2166 .collect::<Vec<_>>();
2167
2168 let expected = self
2169 .expected_outputs
2170 .iter()
2171 .map(|o| {
2172 let input_file_ids = o.input_files.iter().copied().collect::<HashSet<_>>();
2173 (input_file_ids, o.output_level)
2174 })
2175 .collect::<Vec<_>>();
2176 assert_eq!(expected, output);
2177 }
2178 }
2179
2180 struct ExpectedOutput {
2181 input_files: Vec<usize>,
2182 output_level: Level,
2183 }
2184
2185 #[tokio::test]
2186 async fn test_newer_windows_are_placed_last_for_execution() {
2187 let file_ids = (0..4).map(|_| FileId::random()).collect::<Vec<_>>();
2188
2189 CompactionPickerTestCase {
2191 window_size: 3,
2192 input_files: [
2193 new_file_handle_with_sequence(file_ids[0], -2000, -3, 0, 1),
2194 new_file_handle_with_sequence(file_ids[1], -3000, -100, 0, 2),
2195 new_file_handle_with_sequence(file_ids[2], 0, 2999, 0, 3), new_file_handle_with_sequence(file_ids[3], 50, 2998, 0, 4), ]
2198 .to_vec(),
2199 expected_outputs: vec![
2200 ExpectedOutput {
2201 input_files: vec![0, 1],
2202 output_level: 1,
2203 },
2204 ExpectedOutput {
2205 input_files: vec![2, 3],
2206 output_level: 1,
2207 },
2208 ],
2209 }
2210 .check()
2211 .await;
2212
2213 let file_ids = (0..6).map(|_| FileId::random()).collect::<Vec<_>>();
2220 CompactionPickerTestCase {
2221 window_size: 3,
2222 input_files: [
2223 new_file_handle_with_sequence(file_ids[0], -2000, -3, 0, 1),
2224 new_file_handle_with_sequence(file_ids[1], -3000, -100, 0, 2),
2225 new_file_handle_with_sequence(file_ids[2], 0, 2999, 0, 3),
2226 new_file_handle_with_sequence(file_ids[3], 50, 2998, 0, 4),
2227 new_file_handle_with_sequence(file_ids[4], 11, 2990, 0, 5),
2228 ]
2229 .to_vec(),
2230 expected_outputs: vec![
2231 ExpectedOutput {
2232 input_files: vec![0, 1],
2233 output_level: 1,
2234 },
2235 ExpectedOutput {
2236 input_files: vec![2, 3, 4],
2237 output_level: 1,
2238 },
2239 ],
2240 }
2241 .check()
2242 .await;
2243
2244 let file_ids = (0..6).map(|_| FileId::random()).collect::<Vec<_>>();
2249 CompactionPickerTestCase {
2250 window_size: 3,
2251 input_files: [
2252 new_file_handle_with_sequence(file_ids[0], 0, 2999, 1, 1),
2253 new_file_handle_with_sequence(file_ids[1], 0, 2998, 1, 1),
2254 new_file_handle_with_sequence(file_ids[2], 3000, 5999, 1, 2),
2255 new_file_handle_with_sequence(file_ids[3], 3000, 5000, 1, 2),
2256 new_file_handle_with_sequence(file_ids[4], 11, 2990, 0, 3),
2257 ]
2258 .to_vec(),
2259 expected_outputs: vec![ExpectedOutput {
2260 input_files: vec![2, 3],
2261 output_level: 1,
2262 }],
2263 }
2264 .check()
2265 .await;
2266 }
2267
2268 #[tokio::test]
2269 async fn test_build_output_skips_pk_disjoint_files() {
2270 let files = [
2271 new_file_handle_with_size_sequence_and_primary_key_range(
2272 FileId::random(),
2273 0,
2274 2999,
2275 0,
2276 1,
2277 10,
2278 pk_range(b"a", b"f"),
2279 ),
2280 new_file_handle_with_size_sequence_and_primary_key_range(
2281 FileId::random(),
2282 50,
2283 2998,
2284 0,
2285 2,
2286 10,
2287 pk_range(b"x", b"z"),
2288 ),
2289 ];
2290 let windows = assign_to_windows(files.iter(), 3);
2291 let active_window = find_latest_window_in_seconds(files.iter(), 3);
2292 let output = TwcsPicker {
2293 trigger_file_num: 4,
2294 active_window_l1_merge_trigger: 8,
2295 inactive_window_trigger_file_num: 4,
2296 inactive_window_l1_merge_trigger: 8,
2297 time_window_seconds: None,
2298 max_output_file_size: None,
2299 append_mode: false,
2300 merge_mode: MergeMode::LastRow,
2301 max_background_tasks: None,
2302 time_range: None,
2303 all_windows_inactive: false,
2304 }
2305 .build_output_with_time_range(RegionId::from_u64(0), windows, active_window, None)
2306 .await
2307 .unwrap();
2308
2309 assert!(output.is_empty());
2310 }
2311
2312 #[test]
2313 fn test_append_mode_filter_large_files() {
2314 let file_ids = (0..4).map(|_| FileId::random()).collect::<Vec<_>>();
2315 let max_output_file_size = 1000u64;
2316
2317 let small_file_1 = new_file_handle_with_size_and_sequence(file_ids[0], 0, 999, 0, 1, 500);
2319 let large_file_1 = new_file_handle_with_size_and_sequence(file_ids[1], 0, 999, 0, 2, 1500);
2320 let small_file_2 = new_file_handle_with_size_and_sequence(file_ids[2], 0, 999, 0, 3, 800);
2321 let large_file_2 = new_file_handle_with_size_and_sequence(file_ids[3], 0, 999, 0, 4, 2000);
2322
2323 let mut files_to_merge = vec![small_file_1, large_file_1, small_file_2, large_file_2];
2324
2325 let original_count = files_to_merge.len();
2327
2328 files_to_merge.retain(|file| file.size() <= max_output_file_size);
2330
2331 assert_eq!(files_to_merge.len(), 2);
2333 assert_eq!(original_count, 4);
2334
2335 for file in &files_to_merge {
2337 assert!(
2338 file.size() <= max_output_file_size,
2339 "File size {} should be <= {}",
2340 file.size(),
2341 max_output_file_size
2342 );
2343 }
2344 }
2345
2346 #[tokio::test]
2347 async fn test_build_output_multiple_windows_with_zero_runs() {
2348 let file_ids = (0..7).map(|_| FileId::random()).collect::<Vec<_>>();
2349
2350 let files = [
2351 new_file_handle_with_sequence(file_ids[0], 0, 999, 0, 1),
2353 new_file_handle_with_sequence(file_ids[1], 0, 999, 0, 2),
2354 new_file_handle_with_sequence(file_ids[2], 0, 999, 0, 3),
2355 new_file_handle_with_sequence(file_ids[3], 3000, 3999, 0, 4),
2357 new_file_handle_with_sequence(file_ids[4], 3000, 3999, 0, 5),
2358 new_file_handle_with_sequence(file_ids[5], 3000, 3999, 0, 6),
2359 new_file_handle_with_sequence(file_ids[6], 3000, 3999, 0, 7),
2360 ];
2361
2362 let windows = assign_to_windows(files.iter(), 3);
2363
2364 let picker = TwcsPicker {
2366 trigger_file_num: 4, active_window_l1_merge_trigger: 8,
2368 inactive_window_trigger_file_num: 4,
2369 inactive_window_l1_merge_trigger: 8,
2370 time_window_seconds: Some(3),
2371 max_output_file_size: None,
2372 append_mode: false,
2373 merge_mode: MergeMode::LastRow,
2374 max_background_tasks: None,
2375 time_range: None,
2376 all_windows_inactive: false,
2377 };
2378
2379 let active_window = find_latest_window_in_seconds(files.iter(), 3);
2380 let output = picker
2381 .build_output_with_time_range(RegionId::from_u64(123), windows, active_window, None)
2382 .await
2383 .unwrap();
2384
2385 assert!(
2386 !output.is_empty(),
2387 "Should have output from windows with runs, even when one window has 0 runs"
2388 );
2389
2390 let all_output_files: Vec<_> = output
2391 .iter()
2392 .flat_map(|o| o.inputs.iter())
2393 .map(|f| f.file_id().file_id())
2394 .collect();
2395
2396 assert!(
2397 all_output_files.contains(&file_ids[3])
2398 || all_output_files.contains(&file_ids[4])
2399 || all_output_files.contains(&file_ids[5]),
2400 "Output should contain files from the window with runs"
2401 );
2402 }
2403
2404 #[tokio::test]
2405 async fn test_build_output_single_window_zero_runs() {
2406 let file_ids = (0..2).map(|_| FileId::random()).collect::<Vec<_>>();
2407
2408 let large_file_1 = new_file_handle_with_size_and_sequence(file_ids[0], 0, 999, 0, 1, 2000); let large_file_2 = new_file_handle_with_size_and_sequence(file_ids[1], 0, 999, 0, 2, 2500); let files = [large_file_1, large_file_2];
2412
2413 let windows = assign_to_windows(files.iter(), 3);
2414
2415 let picker = TwcsPicker {
2416 trigger_file_num: 2,
2417 active_window_l1_merge_trigger: 8,
2418 inactive_window_trigger_file_num: 2,
2419 inactive_window_l1_merge_trigger: 8,
2420 time_window_seconds: Some(3),
2421 max_output_file_size: Some(1000),
2422 append_mode: true,
2423 merge_mode: MergeMode::LastRow,
2424 max_background_tasks: None,
2425 time_range: None,
2426 all_windows_inactive: false,
2427 };
2428
2429 let active_window = find_latest_window_in_seconds(files.iter(), 3);
2430 let output = picker
2431 .build_output_with_time_range(RegionId::from_u64(456), windows, active_window, None)
2432 .await
2433 .unwrap();
2434
2435 assert!(
2437 output.is_empty(),
2438 "Should return empty output when no runs are found after filtering"
2439 );
2440 }
2441
2442 #[tokio::test]
2443 async fn test_append_mode_can_pick_remaining_single_level_files() {
2444 let files = [
2445 new_file_handle_with_size_and_sequence(FileId::random(), 0, 9, 0, 1, 100),
2446 new_file_handle_with_size_and_sequence(FileId::random(), 20, 29, 0, 2, 100),
2447 new_file_handle_with_size_and_sequence(FileId::random(), 40, 49, 0, 3, 100),
2448 new_file_handle_with_size_and_sequence(FileId::random(), 60, 69, 0, 4, 2_000),
2449 ];
2450 let windows = assign_to_windows(files.iter(), 1);
2451 let picker = TwcsPicker {
2452 trigger_file_num: 4,
2453 active_window_l1_merge_trigger: 8,
2454 inactive_window_trigger_file_num: 4,
2455 inactive_window_l1_merge_trigger: 8,
2456 time_window_seconds: Some(1),
2457 max_output_file_size: Some(1_000),
2458 append_mode: true,
2459 merge_mode: MergeMode::LastRow,
2460 max_background_tasks: None,
2461 time_range: None,
2462 all_windows_inactive: false,
2463 };
2464
2465 let output = picker
2466 .build_output_with_time_range(RegionId::from_u64(1), windows, Some(1), None)
2467 .await
2468 .unwrap();
2469
2470 assert_eq!(1, output.len());
2471 assert_eq!(3, output[0].inputs.len());
2472 }
2473
2474 #[tokio::test]
2475 async fn test_max_background_tasks_truncation() {
2476 let file_ids = (0..10).map(|_| FileId::random()).collect::<Vec<_>>();
2477 let max_background_tasks = 3;
2478
2479 let files = [
2481 new_file_handle_with_sequence(file_ids[0], 0, 999, 0, 1),
2483 new_file_handle_with_sequence(file_ids[1], 0, 999, 0, 2),
2484 new_file_handle_with_sequence(file_ids[2], 0, 999, 0, 3),
2485 new_file_handle_with_sequence(file_ids[3], 0, 999, 0, 4),
2486 new_file_handle_with_sequence(file_ids[4], 3000, 3999, 0, 5),
2488 new_file_handle_with_sequence(file_ids[5], 3000, 3999, 0, 6),
2489 new_file_handle_with_sequence(file_ids[6], 3000, 3999, 0, 7),
2490 new_file_handle_with_sequence(file_ids[7], 3000, 3999, 0, 8),
2491 new_file_handle_with_sequence(file_ids[8], 6000, 6999, 0, 9),
2493 new_file_handle_with_sequence(file_ids[9], 6000, 6999, 0, 10),
2494 ];
2495
2496 let windows = assign_to_windows(files.iter(), 3);
2497
2498 let picker = TwcsPicker {
2499 trigger_file_num: 4,
2500 active_window_l1_merge_trigger: 8,
2501 inactive_window_trigger_file_num: 4,
2502 inactive_window_l1_merge_trigger: 8,
2503 time_window_seconds: Some(3),
2504 max_output_file_size: None,
2505 append_mode: false,
2506 merge_mode: MergeMode::LastRow,
2507 max_background_tasks: Some(max_background_tasks),
2508 time_range: None,
2509 all_windows_inactive: false,
2510 };
2511
2512 let active_window = find_latest_window_in_seconds(files.iter(), 3);
2513 let output = picker
2514 .build_output_with_time_range(RegionId::from_u64(123), windows, active_window, None)
2515 .await
2516 .unwrap();
2517
2518 assert!(
2520 output.len() <= max_background_tasks,
2521 "Output should be truncated to max_background_tasks: expected <= {}, got {}",
2522 max_background_tasks,
2523 output.len()
2524 );
2525
2526 let picker_no_limit = TwcsPicker {
2528 trigger_file_num: 4,
2529 active_window_l1_merge_trigger: 8,
2530 inactive_window_trigger_file_num: 4,
2531 inactive_window_l1_merge_trigger: 8,
2532 time_window_seconds: Some(3),
2533 max_output_file_size: None,
2534 append_mode: false,
2535 merge_mode: MergeMode::LastRow,
2536 max_background_tasks: None,
2537 time_range: None,
2538 all_windows_inactive: false,
2539 };
2540
2541 let windows_no_limit = assign_to_windows(files.iter(), 3);
2542 let output_no_limit = picker_no_limit
2543 .build_output_with_time_range(
2544 RegionId::from_u64(123),
2545 windows_no_limit,
2546 active_window,
2547 None,
2548 )
2549 .await
2550 .unwrap();
2551
2552 if output_no_limit.len() > max_background_tasks {
2554 assert!(
2555 output_no_limit.len() > output.len(),
2556 "Without limit should have more outputs than with limit"
2557 );
2558 }
2559 }
2560
2561 #[tokio::test]
2562 async fn test_max_background_tasks_no_truncation_when_under_limit() {
2563 let file_ids = (0..4).map(|_| FileId::random()).collect::<Vec<_>>();
2564 let max_background_tasks = 10; let files = [
2568 new_file_handle_with_sequence(file_ids[0], 0, 999, 0, 1),
2569 new_file_handle_with_sequence(file_ids[1], 0, 999, 0, 2),
2570 new_file_handle_with_sequence(file_ids[2], 0, 999, 0, 3),
2571 new_file_handle_with_sequence(file_ids[3], 0, 999, 0, 4),
2572 ];
2573
2574 let windows = assign_to_windows(files.iter(), 3);
2575
2576 let picker = TwcsPicker {
2577 trigger_file_num: 4,
2578 active_window_l1_merge_trigger: 8,
2579 inactive_window_trigger_file_num: 4,
2580 inactive_window_l1_merge_trigger: 8,
2581 time_window_seconds: Some(3),
2582 max_output_file_size: None,
2583 append_mode: false,
2584 merge_mode: MergeMode::LastRow,
2585 max_background_tasks: Some(max_background_tasks),
2586 time_range: None,
2587 all_windows_inactive: false,
2588 };
2589
2590 let active_window = find_latest_window_in_seconds(files.iter(), 3);
2591 let output = picker
2592 .build_output_with_time_range(RegionId::from_u64(123), windows, active_window, None)
2593 .await
2594 .unwrap();
2595
2596 assert!(
2598 output.len() <= max_background_tasks,
2599 "Output should be within limit"
2600 );
2601 assert!(!output.is_empty(), "Should have at least one output");
2603 }
2604
2605 #[tokio::test]
2606 async fn test_pick_multiple_runs() {
2607 common_telemetry::init_default_ut_logging();
2608
2609 let num_files = 8;
2610 let file_ids = (0..num_files).map(|_| FileId::random()).collect::<Vec<_>>();
2611
2612 let files: Vec<_> = file_ids
2614 .iter()
2615 .enumerate()
2616 .map(|(idx, file_id)| {
2617 new_file_handle_with_size_and_sequence(
2618 *file_id,
2619 0,
2620 999,
2621 0,
2622 (idx + 1) as u64,
2623 1024 * 1024,
2624 )
2625 })
2626 .collect();
2627
2628 let windows = assign_to_windows(files.iter(), 3);
2629
2630 let picker = TwcsPicker {
2631 trigger_file_num: 4,
2632 active_window_l1_merge_trigger: 8,
2633 inactive_window_trigger_file_num: 4,
2634 inactive_window_l1_merge_trigger: 8,
2635 time_window_seconds: Some(3),
2636 max_output_file_size: None,
2637 append_mode: false,
2638 merge_mode: MergeMode::LastRow,
2639 max_background_tasks: None,
2640 time_range: None,
2641 all_windows_inactive: false,
2642 };
2643
2644 let active_window = find_latest_window_in_seconds(files.iter(), 3);
2645 let output = picker
2646 .build_output_with_time_range(RegionId::from_u64(123), windows, active_window, None)
2647 .await
2648 .unwrap();
2649
2650 assert_eq!(1, output.len());
2651 assert_eq!(output[0].inputs.len(), num_files);
2652 }
2653
2654 #[tokio::test]
2655 async fn test_window_trigger_can_exceed_input_limit() {
2656 common_telemetry::init_default_ut_logging();
2657
2658 let num_files = 50;
2659 let file_ids = (0..num_files).map(|_| FileId::random()).collect::<Vec<_>>();
2660
2661 let files: Vec<_> = file_ids
2663 .iter()
2664 .enumerate()
2665 .map(|(idx, file_id)| {
2666 new_file_handle_with_size_and_sequence(
2667 *file_id,
2668 (idx / 2 * 10) as i64,
2669 (idx / 2 * 10 + 5) as i64,
2670 0,
2671 (idx + 1) as u64,
2672 1024 * 1024,
2673 )
2674 })
2675 .collect();
2676
2677 let windows = assign_to_windows(files.iter(), 3);
2678
2679 let picker = TwcsPicker {
2680 trigger_file_num: num_files,
2681 active_window_l1_merge_trigger: 8,
2682 inactive_window_trigger_file_num: num_files,
2683 inactive_window_l1_merge_trigger: 8,
2684 time_window_seconds: Some(3),
2685 max_output_file_size: None,
2686 append_mode: false,
2687 merge_mode: MergeMode::LastRow,
2688 max_background_tasks: None,
2689 time_range: None,
2690 all_windows_inactive: false,
2691 };
2692
2693 let active_window = find_latest_window_in_seconds(files.iter(), 3);
2694 let output = picker
2695 .build_output_with_time_range(RegionId::from_u64(123), windows, active_window, None)
2696 .await
2697 .unwrap();
2698
2699 assert_eq!(1, output.len());
2700 assert_eq!(output[0].inputs.len(), num_files.min(*MAX_INPUT_FILES));
2701 }
2702
2703 #[tokio::test]
2704 async fn test_limit_max_input_files_keeps_deletion_markers() {
2705 common_telemetry::init_default_ut_logging();
2706
2707 let mut files = vec![new_file_handle_with_size_and_sequence(
2710 FileId::random(),
2711 0,
2712 3_000_000,
2713 0,
2714 1,
2715 1024 * 1024 * 1024,
2716 )];
2717 files.extend((0..DEFAULT_MAX_INPUT_FILES as i64).map(|idx| {
2718 new_file_handle_with_size_and_sequence(
2719 FileId::random(),
2720 (idx + 1) * 10_000,
2721 (idx + 1) * 10_000 + 1_000,
2722 0,
2723 (idx + 2) as u64,
2724 1024,
2725 )
2726 }));
2727
2728 let windows = assign_to_windows(files.iter(), 3600);
2729
2730 let picker = TwcsPicker {
2731 trigger_file_num: 4,
2732 active_window_l1_merge_trigger: 8,
2733 inactive_window_trigger_file_num: 4,
2734 inactive_window_l1_merge_trigger: 8,
2735 time_window_seconds: Some(3600),
2736 max_output_file_size: None,
2737 append_mode: false,
2738 merge_mode: MergeMode::LastRow,
2739 max_background_tasks: None,
2740 time_range: None,
2741 all_windows_inactive: false,
2742 };
2743
2744 let active_window = find_latest_window_in_seconds(files.iter(), 3600);
2745 let output = picker
2746 .build_output_with_time_range(RegionId::from_u64(123), windows, active_window, None)
2747 .await
2748 .unwrap();
2749
2750 assert_eq!(1, output.len());
2751 assert_eq!(DEFAULT_MAX_INPUT_FILES, output[0].inputs.len());
2754 assert!(
2755 !output[0].filter_deleted,
2756 "deletion markers must be kept once the file num limit drops files they may mask"
2757 );
2758 }
2759
2760 #[tokio::test]
2761 async fn test_limit_max_input_files_still_filters_without_overlap() {
2762 common_telemetry::init_default_ut_logging();
2763
2764 let files: Vec<_> = (0..DEFAULT_MAX_INPUT_FILES as i64 + 8)
2767 .map(|idx| {
2768 new_file_handle_with_size_and_sequence(
2769 FileId::random(),
2770 (idx + 1) * 10_000,
2771 (idx + 1) * 10_000 + 1_000,
2772 0,
2773 (idx + 1) as u64,
2774 1024,
2775 )
2776 })
2777 .collect();
2778
2779 let windows = assign_to_windows(files.iter(), 3600);
2780
2781 let picker = TwcsPicker {
2782 trigger_file_num: 4,
2783 active_window_l1_merge_trigger: 8,
2784 inactive_window_trigger_file_num: 4,
2785 inactive_window_l1_merge_trigger: 8,
2786 time_window_seconds: Some(3600),
2787 max_output_file_size: Some(1024 * 1024 * 1024),
2788 append_mode: false,
2789 merge_mode: MergeMode::LastRow,
2790 max_background_tasks: None,
2791 time_range: None,
2792 all_windows_inactive: false,
2793 };
2794
2795 let active_window = find_latest_window_in_seconds(files.iter(), 3600);
2796 let output = picker
2797 .build_output_with_time_range(RegionId::from_u64(123), windows, active_window, None)
2798 .await
2799 .unwrap();
2800
2801 assert_eq!(1, output.len());
2802 assert_eq!(DEFAULT_MAX_INPUT_FILES, output[0].inputs.len());
2803 assert!(output[0].filter_deleted);
2804 }
2805
2806 #[tokio::test]
2807 async fn test_newer_windows_have_priority() {
2808 let older_file_ids = [FileId::random(), FileId::random()];
2809 let newer_file_ids = [FileId::random(), FileId::random()];
2810 let files = [
2811 new_file_handle_with_sequence(older_file_ids[0], 1_000, 1_999, 0, 1),
2812 new_file_handle_with_sequence(older_file_ids[1], 1_000, 1_999, 0, 2),
2813 new_file_handle_with_sequence(newer_file_ids[0], 7_000, 7_999, 0, 3),
2814 new_file_handle_with_sequence(newer_file_ids[1], 7_000, 7_999, 0, 4),
2815 ];
2816 let windows = assign_to_windows(files.iter(), 3);
2817 let picker = TwcsPicker {
2818 trigger_file_num: 2,
2819 active_window_l1_merge_trigger: 8,
2820 inactive_window_trigger_file_num: 2,
2821 inactive_window_l1_merge_trigger: 8,
2822 time_window_seconds: Some(3),
2823 max_output_file_size: None,
2824 append_mode: false,
2825 merge_mode: MergeMode::LastRow,
2826 max_background_tasks: Some(1),
2827 time_range: None,
2828 all_windows_inactive: false,
2829 };
2830
2831 let output = picker
2832 .build_output_with_time_range(RegionId::from_u64(123), windows, Some(6), None)
2833 .await
2834 .unwrap();
2835
2836 assert_eq!(1, output.len());
2837 assert_eq!(
2838 newer_file_ids.into_iter().collect::<HashSet<_>>(),
2839 output[0]
2840 .inputs
2841 .iter()
2842 .map(|file| file.file_id().file_id())
2843 .collect::<HashSet<_>>()
2844 );
2845 }
2846
2847 #[test]
2848 fn test_filter_time_windows_by_time_range() {
2849 for (start, end, expected_windows) in [
2852 (1_200, 1_800, vec![0]),
2853 (0, 3_000, vec![0]),
2854 (3_000, 6_000, vec![3]),
2855 (2_999, 3_000, vec![0]),
2856 (2_999, 3_001, vec![0, 3]),
2857 (3_000, 3_001, vec![3]),
2858 (3_001, 3_002, vec![3]),
2859 (-3_000, 0, vec![-3]),
2860 (-3_001, -2_999, vec![-6, -3]),
2861 (0, 0, vec![]),
2862 (1_500, 1_500, vec![]),
2863 ] {
2864 let time_range = TimestampRange::with_unit(start, end, TimeUnit::Millisecond).unwrap();
2865 let selected = [-6, -3, 0, 3, 6]
2866 .into_iter()
2867 .filter(|window| time_window_intersects_range(*window, 3, &time_range))
2868 .collect::<Vec<_>>();
2869 assert_eq!(expected_windows, selected, "{time_range}");
2870 }
2871 }
2872
2873 #[test]
2874 fn test_filter_time_windows_with_unbounded_and_extreme_ranges() {
2875 for (range, expected_windows) in [
2876 (TimestampRange::min_to_max(), vec![-3, 0, 3]),
2877 (
2878 TimestampRange::from_start(Timestamp::new_second(0)),
2879 vec![0, 3],
2880 ),
2881 (
2882 TimestampRange::until_end(Timestamp::new_second(0), false),
2883 vec![-3],
2884 ),
2885 ] {
2886 let selected = [-3, 0, 3]
2887 .into_iter()
2888 .filter(|window| time_window_intersects_range(*window, 3, &range))
2889 .collect::<Vec<_>>();
2890 assert_eq!(expected_windows, selected, "{range}");
2891 }
2892
2893 let upper_range = TimestampRange::new(
2894 Timestamp::new_second(i64::MAX - 1),
2895 Timestamp::new_second(i64::MAX),
2896 )
2897 .unwrap();
2898 assert!(!time_window_intersects_range(0, 4, &upper_range));
2899 assert!(time_window_intersects_range(i64::MAX - 3, 4, &upper_range));
2900
2901 let lower_range = TimestampRange::until_end(Timestamp::new_second(i64::MIN), false);
2902 assert!(!time_window_intersects_range(i64::MIN, 3, &lower_range));
2903 }
2904
2905 #[tokio::test]
2906 async fn test_time_range_filter_precedes_background_task_limit() {
2907 let early_file_ids = [FileId::random(), FileId::random()];
2908 let selected_file_ids = [FileId::random(), FileId::random()];
2909 let files = [
2910 new_file_handle_with_sequence(early_file_ids[0], 1_000, 1_999, 0, 1),
2911 new_file_handle_with_sequence(early_file_ids[1], 1_000, 1_999, 0, 2),
2912 new_file_handle_with_sequence(selected_file_ids[0], 7_000, 7_999, 0, 3),
2913 new_file_handle_with_sequence(selected_file_ids[1], 7_000, 7_999, 0, 4),
2914 ];
2915 let windows = assign_to_windows(files.iter(), 3);
2916 let picker = TwcsPicker {
2917 trigger_file_num: 2,
2918 active_window_l1_merge_trigger: 8,
2919 inactive_window_trigger_file_num: 2,
2920 inactive_window_l1_merge_trigger: 8,
2921 time_window_seconds: Some(3),
2922 max_output_file_size: None,
2923 append_mode: false,
2924 merge_mode: MergeMode::LastRow,
2925 max_background_tasks: Some(1),
2926 time_range: TimestampRange::new(
2927 Timestamp::new_millisecond(7_200),
2928 Timestamp::new_millisecond(7_800),
2929 ),
2930 all_windows_inactive: false,
2931 };
2932
2933 let output = picker
2934 .build_output_with_time_range(RegionId::from_u64(123), windows, Some(6), Some(3))
2935 .await
2936 .unwrap();
2937
2938 assert_eq!(1, output.len());
2939 assert_eq!(
2940 selected_file_ids.into_iter().collect::<HashSet<_>>(),
2941 output[0]
2942 .inputs
2943 .iter()
2944 .map(|file| file.file_id().file_id())
2945 .collect::<HashSet<_>>()
2946 );
2947 }
2948
2949 #[tokio::test]
2950 async fn test_inactive_window_uses_its_trigger_file_num() {
2951 let files = [
2952 new_file_handle_with_size_and_sequence(FileId::random(), 0, 10, 0, 1, 10),
2953 new_file_handle_with_size_and_sequence(FileId::random(), 20, 30, 0, 2, 10),
2954 ];
2955 let windows = assign_to_windows(files.iter(), 100);
2956 let picker = TwcsPicker {
2957 trigger_file_num: 4,
2958 active_window_l1_merge_trigger: 8,
2959 inactive_window_trigger_file_num: 2,
2960 inactive_window_l1_merge_trigger: 8,
2961 time_window_seconds: Some(100),
2962 max_output_file_size: None,
2963 append_mode: false,
2964 merge_mode: MergeMode::LastRow,
2965 max_background_tasks: None,
2966 time_range: None,
2967 all_windows_inactive: false,
2968 };
2969
2970 let output = picker
2971 .build_output_with_time_range(RegionId::from_u64(1), windows, Some(100), None)
2972 .await
2973 .unwrap();
2974
2975 assert_eq!(1, output.len());
2976 assert_eq!(2, output[0].inputs.len());
2977 }
2978
2979 #[tokio::test]
2980 async fn test_inactive_window_requires_a_level_trigger_before_mixed_fallback() {
2981 let files = [
2982 new_file_handle_with_size_and_sequence(FileId::random(), 0, 10, 0, 1, 10),
2983 new_file_handle_with_size_and_sequence(FileId::random(), 0, 10, 1, 2, 10),
2984 ];
2985 let windows = assign_to_windows(files.iter(), 100);
2986 let picker = TwcsPicker {
2987 trigger_file_num: 4,
2988 active_window_l1_merge_trigger: 8,
2989 inactive_window_trigger_file_num: 8,
2990 inactive_window_l1_merge_trigger: 2,
2991 time_window_seconds: Some(100),
2992 max_output_file_size: Some(1_000),
2993 append_mode: false,
2994 merge_mode: MergeMode::LastRow,
2995 max_background_tasks: None,
2996 time_range: None,
2997 all_windows_inactive: false,
2998 };
2999
3000 let output = picker
3001 .build_output_with_time_range(RegionId::from_u64(1), windows, Some(100), None)
3002 .await
3003 .unwrap();
3004
3005 assert!(output.is_empty());
3006 }
3007
3008 #[tokio::test]
3009 async fn test_count_first_prefers_more_files_over_smaller_overlap() {
3010 let files = [
3011 new_file_handle_with_size_and_sequence(FileId::random(), 0, 10, 0, 1, 10),
3012 new_file_handle_with_size_and_sequence(FileId::random(), 20, 30, 0, 2, 10),
3013 new_file_handle_with_size_and_sequence(FileId::random(), 40, 50, 0, 3, 10),
3014 new_file_handle_with_size_and_sequence(FileId::random(), 5, 15, 0, 4, 10),
3015 ];
3016 let windows = assign_to_windows(files.iter(), 100);
3017 let picker = TwcsPicker {
3018 trigger_file_num: 2,
3019 active_window_l1_merge_trigger: 8,
3020 inactive_window_trigger_file_num: 2,
3021 inactive_window_l1_merge_trigger: 8,
3022 time_window_seconds: Some(100),
3023 max_output_file_size: None,
3024 append_mode: false,
3025 merge_mode: MergeMode::LastRow,
3026 max_background_tasks: None,
3027 time_range: None,
3028 all_windows_inactive: false,
3029 };
3030
3031 let output = picker
3032 .build_output_with_time_range(RegionId::from_u64(1), windows, Some(0), None)
3033 .await
3034 .unwrap();
3035
3036 assert_eq!(1, output.len());
3037 assert_eq!(4, output[0].inputs.len());
3038 }
3039
3040 #[tokio::test]
3041 async fn test_count_first_trigger_counts_physical_ssts() {
3042 let files = [
3043 new_file_handle_with_size_and_sequence(FileId::random(), 0, 10, 0, 1, 10),
3044 new_file_handle_with_size_and_sequence(FileId::random(), 0, 10, 0, 1, 10),
3045 new_file_handle_with_size_and_sequence(FileId::random(), 20, 30, 0, 2, 10),
3046 ];
3047 let windows = assign_to_windows(files.iter(), 100);
3048 let picker = TwcsPicker {
3049 trigger_file_num: 3,
3050 active_window_l1_merge_trigger: 8,
3051 inactive_window_trigger_file_num: 3,
3052 inactive_window_l1_merge_trigger: 8,
3053 time_window_seconds: Some(100),
3054 max_output_file_size: None,
3055 append_mode: false,
3056 merge_mode: MergeMode::LastRow,
3057 max_background_tasks: None,
3058 time_range: None,
3059 all_windows_inactive: false,
3060 };
3061
3062 let output = picker
3063 .build_output_with_time_range(RegionId::from_u64(1), windows, Some(0), None)
3064 .await
3065 .unwrap();
3066
3067 assert_eq!(1, output.len());
3068 assert_eq!(3, output[0].inputs.len());
3069 }
3070
3071 #[tokio::test]
3072 async fn test_count_first_does_not_compact_overlap_below_trigger() {
3073 let files = [
3074 new_file_handle_with_size_and_sequence(FileId::random(), 0, 20, 0, 1, 10),
3075 new_file_handle_with_size_and_sequence(FileId::random(), 10, 30, 0, 2, 10),
3076 ];
3077 let windows = assign_to_windows(files.iter(), 100);
3078 let picker = TwcsPicker {
3079 trigger_file_num: 3,
3080 active_window_l1_merge_trigger: 8,
3081 inactive_window_trigger_file_num: 3,
3082 inactive_window_l1_merge_trigger: 8,
3083 time_window_seconds: Some(100),
3084 max_output_file_size: None,
3085 append_mode: false,
3086 merge_mode: MergeMode::LastRow,
3087 max_background_tasks: None,
3088 time_range: None,
3089 all_windows_inactive: false,
3090 };
3091
3092 let output = picker
3093 .build_output_with_time_range(RegionId::from_u64(1), windows, Some(0), None)
3094 .await
3095 .unwrap();
3096
3097 assert!(output.is_empty());
3098 }
3099
3100 #[tokio::test]
3101 async fn test_filter_deleted_is_false_when_selected_files_overlap_unselected_file() {
3102 let mut files = (0..32)
3103 .map(|idx| {
3104 new_file_handle_with_size_and_sequence(
3105 FileId::random(),
3106 idx * 10,
3107 idx * 10 + 9,
3108 0,
3109 idx as u64 + 1,
3110 10,
3111 )
3112 })
3113 .collect::<Vec<_>>();
3114 files.push(new_file_handle_with_size_and_sequence(
3115 FileId::random(),
3116 0,
3117 320,
3118 0,
3119 33,
3120 10,
3121 ));
3122 let windows = assign_to_windows(files.iter(), 1000);
3123 let picker = TwcsPicker {
3124 trigger_file_num: 2,
3125 active_window_l1_merge_trigger: 8,
3126 inactive_window_trigger_file_num: 2,
3127 inactive_window_l1_merge_trigger: 8,
3128 time_window_seconds: Some(1000),
3129 max_output_file_size: None,
3130 append_mode: false,
3131 merge_mode: MergeMode::LastRow,
3132 max_background_tasks: None,
3133 time_range: None,
3134 all_windows_inactive: false,
3135 };
3136
3137 let output = picker
3138 .build_output_with_time_range(RegionId::from_u64(1), windows, Some(0), None)
3139 .await
3140 .unwrap();
3141
3142 assert_eq!(1, output.len());
3143 assert_eq!(DEFAULT_MAX_INPUT_FILES, output[0].inputs.len());
3144 assert!(!output[0].filter_deleted);
3145 }
3146
3147 fn new_file(start: i64, end: i64, sequence: u64, file_size: u64) -> FileHandle {
3148 new_file_handle_with_size_and_sequence(FileId::random(), start, end, 0, sequence, file_size)
3149 }
3150
3151 fn new_file_with_level_and_rows(
3152 start: i64,
3153 end: i64,
3154 level: Level,
3155 sequence: u64,
3156 file_size: u64,
3157 num_rows: u64,
3158 ) -> FileHandle {
3159 let file = new_file_handle_with_size_and_sequence(
3160 FileId::random(),
3161 start,
3162 end,
3163 level,
3164 sequence,
3165 file_size,
3166 );
3167 let mut meta = file.meta_ref().clone();
3168 meta.num_rows = num_rows;
3169 FileHandle::new(meta, crate::test_util::new_noop_file_purger())
3170 }
3171
3172 fn picked_ranges(files: &[FileHandle]) -> Vec<(i64, i64)> {
3173 files
3174 .iter()
3175 .map(|file| {
3176 let (start, end) = file.range();
3177 (start.value(), end.value())
3178 })
3179 .collect()
3180 }
3181
3182 #[test]
3183 fn test_count_first_rejects_dominant_historical_file() {
3184 let picked = pick_count_first(
3185 vec![SortedRun::from(vec![
3186 new_file(0, 9, 1, 400),
3187 new_file(20, 29, 2, 100),
3188 ])],
3189 None,
3190 );
3191
3192 assert!(picked.is_empty());
3193 }
3194
3195 #[test]
3196 fn test_count_first_accepts_balanced_historical_file() {
3197 let picked = pick_count_first(
3198 vec![SortedRun::from(vec![
3199 new_file(0, 9, 1, 400),
3200 new_file(20, 29, 2, 100),
3201 new_file(40, 49, 3, 100),
3202 new_file(60, 69, 4, 100),
3203 new_file(80, 89, 5, 100),
3204 ])],
3205 None,
3206 );
3207
3208 assert_eq!(
3209 vec![(0, 9), (20, 29), (40, 49), (60, 69), (80, 89)],
3210 picked_ranges(&picked)
3211 );
3212 }
3213
3214 #[test]
3215 fn test_count_first_finds_smaller_balanced_interval_when_larger_one_is_unbalanced() {
3216 let picked = pick_count_first(
3217 vec![SortedRun::from(vec![
3218 new_file(0, 9, 1, 1000),
3219 new_file(20, 29, 2, 100),
3220 new_file(40, 49, 3, 100),
3221 new_file(60, 69, 4, 100),
3222 new_file(80, 89, 5, 100),
3223 ])],
3224 None,
3225 );
3226
3227 assert_eq!(
3228 vec![(20, 29), (40, 49), (60, 69), (80, 89)],
3229 picked_ranges(&picked)
3230 );
3231 }
3232
3233 #[test]
3234 fn test_count_first_prefers_overlap_participants_when_file_counts_match() {
3235 let first_run = (0..DEFAULT_MAX_INPUT_FILES)
3236 .map(|idx| {
3237 let start = idx as i64 * 20;
3238 let end = if idx + 1 == DEFAULT_MAX_INPUT_FILES {
3239 700
3240 } else {
3241 start + 9
3242 };
3243 new_file(start, end, idx as u64 + 1, 10)
3244 })
3245 .collect::<Vec<_>>();
3246 let overlapping = new_file(690, 710, 100, 10);
3247
3248 let picked = pick_count_first(
3249 vec![
3250 SortedRun::from(first_run),
3251 SortedRun::from(vec![overlapping]),
3252 ],
3253 None,
3254 );
3255 let ranges = picked_ranges(&picked);
3256
3257 assert_eq!(DEFAULT_MAX_INPUT_FILES, ranges.len());
3258 assert!(!ranges.contains(&(0, 9)));
3259 assert!(ranges.contains(&(690, 710)));
3260 }
3261
3262 #[tokio::test]
3263 async fn test_picker_avoids_chained_l1_rewrites_by_compacting_levels_separately() {
3264 let mut enough_l0 = (0..DEFAULT_MAX_INPUT_FILES)
3265 .map(|idx| {
3266 let start = idx as i64 * 20;
3267 new_file_handle_with_size_and_sequence(
3268 FileId::random(),
3269 start,
3270 start + 9,
3271 0,
3272 idx as u64 + 1,
3273 10,
3274 )
3275 })
3276 .collect::<Vec<_>>();
3277 enough_l0.push(new_file_handle_with_size_and_sequence(
3278 FileId::random(),
3279 0,
3280 700,
3281 1,
3282 100,
3283 100,
3284 ));
3285 let mut enough_l1 = (0..4)
3286 .map(|idx| {
3287 new_file_handle_with_size_and_sequence(
3288 FileId::random(),
3289 idx * 100,
3290 idx * 100 + 99,
3291 1,
3292 idx as u64 + 1,
3293 100,
3294 )
3295 })
3296 .collect::<Vec<_>>();
3297 enough_l1.extend((0..3).map(|idx| {
3298 new_file_handle_with_size_and_sequence(
3299 FileId::random(),
3300 idx * 20,
3301 idx * 20 + 9,
3302 0,
3303 idx as u64 + 10,
3304 10,
3305 )
3306 }));
3307 let picker = TwcsPicker {
3308 trigger_file_num: 4,
3309 active_window_l1_merge_trigger: 8,
3310 inactive_window_trigger_file_num: 4,
3311 inactive_window_l1_merge_trigger: 4,
3312 time_window_seconds: Some(1),
3313 max_output_file_size: None,
3314 append_mode: false,
3315 merge_mode: MergeMode::LastRow,
3316 max_background_tasks: None,
3317 time_range: None,
3318 all_windows_inactive: false,
3319 };
3320
3321 for (case, files, expected_level, expected_len) in [
3322 ("L0 reaches trigger", enough_l0, 0, DEFAULT_MAX_INPUT_FILES),
3323 ("L0 fallback precedes triggered L1", enough_l1, 0, 3),
3324 ] {
3325 let windows = assign_to_windows(files.iter(), 1);
3326 let output = picker
3327 .build_output_with_time_range(RegionId::from_u64(1), windows, Some(1), None)
3328 .await
3329 .unwrap();
3330
3331 assert_eq!(1, output.len(), "{case}");
3332 assert_eq!(expected_len, output[0].inputs.len(), "{case}");
3333 assert!(
3334 output[0]
3335 .inputs
3336 .iter()
3337 .all(|file| file.level() == expected_level),
3338 "{case}"
3339 );
3340 }
3341 }
3342
3343 #[tokio::test]
3344 async fn test_active_window_defers_l1_below_safety_trigger() {
3345 let files = (0..7)
3346 .map(|idx| {
3347 let start = idx * 100;
3348 new_file_handle_with_size_and_sequence(
3349 FileId::random(),
3350 start,
3351 start + 99,
3352 1,
3353 idx as u64 + 1,
3354 100,
3355 )
3356 })
3357 .collect::<Vec<_>>();
3358 let windows = assign_to_windows(files.iter(), 1);
3359 let active_window = windows.keys().next().copied();
3360 let picker = TwcsPicker {
3361 trigger_file_num: 4,
3362 active_window_l1_merge_trigger: 8,
3363 inactive_window_trigger_file_num: 2,
3364 inactive_window_l1_merge_trigger: 8,
3365 time_window_seconds: Some(1),
3366 max_output_file_size: None,
3367 append_mode: false,
3368 merge_mode: MergeMode::LastRow,
3369 max_background_tasks: None,
3370 time_range: None,
3371 all_windows_inactive: false,
3372 };
3373
3374 let output = picker
3375 .build_output_with_time_range(RegionId::from_u64(1), windows, active_window, None)
3376 .await
3377 .unwrap();
3378
3379 assert!(output.is_empty());
3380 }
3381
3382 #[tokio::test]
3383 async fn test_active_window_l1_safety_trigger_is_independent_of_l0_trigger() {
3384 let files = (0..8)
3385 .map(|idx| {
3386 let start = idx * 100;
3387 new_file_handle_with_size_and_sequence(
3388 FileId::random(),
3389 start,
3390 start + 99,
3391 1,
3392 idx as u64 + 1,
3393 100,
3394 )
3395 })
3396 .collect::<Vec<_>>();
3397 let windows = assign_to_windows(files.iter(), 1);
3398 let active_window = windows.keys().next().copied();
3399 let picker = TwcsPicker {
3400 trigger_file_num: 16,
3401 active_window_l1_merge_trigger: 8,
3402 inactive_window_trigger_file_num: 2,
3403 inactive_window_l1_merge_trigger: 8,
3404 time_window_seconds: Some(1),
3405 max_output_file_size: None,
3406 append_mode: false,
3407 merge_mode: MergeMode::LastRow,
3408 max_background_tasks: None,
3409 time_range: None,
3410 all_windows_inactive: false,
3411 };
3412
3413 let output = picker
3414 .build_output_with_time_range(RegionId::from_u64(1), windows, active_window, None)
3415 .await
3416 .unwrap();
3417
3418 assert_eq!(1, output.len());
3419 assert_eq!(8, output[0].inputs.len());
3420 assert!(output[0].inputs.iter().all(|file| file.level() == 1));
3421 }
3422
3423 #[tokio::test]
3424 async fn test_picker_falls_back_to_l1_when_triggered_l0_cannot_make_progress() {
3425 let mut files = (0..4)
3426 .map(|idx| {
3427 let start = idx * 20;
3428 new_file_handle_with_size_and_sequence(
3429 FileId::random(),
3430 start,
3431 start + 9,
3432 0,
3433 idx as u64 + 1,
3434 600,
3435 )
3436 })
3437 .collect::<Vec<_>>();
3438 files.extend((0..8).map(|idx| {
3439 let start = idx * 20 + 100;
3440 new_file_handle_with_size_and_sequence(
3441 FileId::random(),
3442 start,
3443 start + 9,
3444 1,
3445 idx as u64 + 10,
3446 50,
3447 )
3448 }));
3449 let windows = assign_to_windows(files.iter(), 1);
3450 let active_window = windows.keys().next().copied();
3451 let picker = TwcsPicker {
3452 trigger_file_num: 4,
3453 active_window_l1_merge_trigger: 8,
3454 inactive_window_trigger_file_num: 4,
3455 inactive_window_l1_merge_trigger: 8,
3456 time_window_seconds: Some(1),
3457 max_output_file_size: Some(512),
3458 append_mode: false,
3459 merge_mode: MergeMode::LastRow,
3460 max_background_tasks: None,
3461 time_range: None,
3462 all_windows_inactive: false,
3463 };
3464
3465 let output = picker
3466 .build_output_with_time_range(RegionId::from_u64(1), windows, active_window, None)
3467 .await
3468 .unwrap();
3469
3470 assert_eq!(1, output.len());
3471 assert_eq!(8, output[0].inputs.len());
3472 assert!(output[0].inputs.iter().all(|file| file.level() == 1));
3473 }
3474
3475 fn priority_test_files(
3476 window: i64,
3477 level: Level,
3478 file_size: u64,
3479 overlap: bool,
3480 ) -> [FileHandle; 2] {
3481 let start = window * 1_000;
3482 let ranges = if overlap {
3483 [(start, start + 499), (start + 250, start + 749)]
3484 } else {
3485 [(start, start + 99), (start + 200, start + 299)]
3486 };
3487 ranges.map(|(start, end)| {
3488 new_file_handle_with_size_and_sequence(
3489 FileId::random(),
3490 start,
3491 end,
3492 level,
3493 window as u64 + 1,
3494 file_size,
3495 )
3496 })
3497 }
3498
3499 fn priority_test_picker(max_background_tasks: usize) -> TwcsPicker {
3500 TwcsPicker {
3501 trigger_file_num: 2,
3502 active_window_l1_merge_trigger: 2,
3503 inactive_window_trigger_file_num: 2,
3504 inactive_window_l1_merge_trigger: 2,
3505 time_window_seconds: Some(1),
3506 max_output_file_size: Some(100),
3507 append_mode: false,
3508 merge_mode: MergeMode::LastRow,
3509 max_background_tasks: Some(max_background_tasks),
3510 time_range: None,
3511 all_windows_inactive: false,
3512 }
3513 }
3514
3515 fn output_window(output: &CompactionOutput) -> i64 {
3516 output.inputs[0]
3517 .time_range()
3518 .1
3519 .convert_to(TimeUnit::Second)
3520 .unwrap()
3521 .value()
3522 }
3523
3524 #[tokio::test]
3525 async fn test_manual_compaction_resolves_active_window_overlaps() {
3526 let files = priority_test_files(5, 1, 100, true);
3527 for (all_windows_inactive, expected) in [(false, 0), (true, 1)] {
3528 let picker = TwcsPicker {
3529 all_windows_inactive,
3530 ..priority_test_picker(1)
3531 };
3532 let output = picker
3533 .build_output_with_time_range(
3534 RegionId::from_u64(1),
3535 assign_to_windows(files.iter(), 1),
3536 Some(5),
3537 None,
3538 )
3539 .await
3540 .unwrap();
3541 assert_eq!(expected, output.len(), "manual: {all_windows_inactive}");
3542 }
3543 }
3544
3545 #[tokio::test]
3546 async fn test_shared_boundary_l1_files_resolve_in_inactive_and_manual_windows() {
3547 let files = [(b"a", b"m"), (b"m", b"z")].map(|(min, max)| {
3548 new_file_handle_with_size_sequence_and_primary_key_range(
3549 FileId::random(),
3550 0,
3551 100,
3552 1,
3553 1,
3554 100,
3555 pk_range(min, max),
3556 )
3557 });
3558 for (active_window, all_windows_inactive, expected) in
3559 [(0, false, 0), (0, true, 1), (3, false, 1)]
3560 {
3561 let picker = TwcsPicker {
3562 all_windows_inactive,
3563 ..priority_test_picker(1)
3564 };
3565 let output = picker
3566 .build_output_with_time_range(
3567 RegionId::from_u64(1),
3568 assign_to_windows(files.iter(), 1),
3569 Some(active_window),
3570 Some(1),
3571 )
3572 .await
3573 .unwrap();
3574 assert_eq!(
3575 expected,
3576 output.len(),
3577 "active: {active_window}, manual: {all_windows_inactive}"
3578 );
3579 if let Some(output) = output.first() {
3580 assert_eq!(2, output.inputs.len());
3581 }
3582 }
3583 }
3584
3585 #[tokio::test]
3586 async fn test_settling_window_defers_overlap_resolution() {
3587 let files = priority_test_files(3, 1, 100, true);
3588 for (active_window, all_windows_inactive, expected) in
3589 [(4, false, 0), (5, false, 1), (2, false, 1), (4, true, 1)]
3590 {
3591 let picker = TwcsPicker {
3592 all_windows_inactive,
3593 ..priority_test_picker(1)
3594 };
3595 let output = picker
3596 .build_output_with_time_range(
3597 RegionId::from_u64(1),
3598 assign_to_windows(files.iter(), 1),
3599 Some(active_window),
3600 Some(1),
3601 )
3602 .await
3603 .unwrap();
3604 assert_eq!(
3605 expected,
3606 output.len(),
3607 "active: {active_window}, manual: {all_windows_inactive}"
3608 );
3609 }
3610 }
3611
3612 #[tokio::test]
3613 async fn test_inactive_windows_merge_small_l1_without_large_peer() {
3614 let large =
3615 new_file_handle_with_size_and_sequence(FileId::random(), 3_000, 3_999, 1, 1, 1_000);
3616 let small = priority_test_files(3, 1, 20, true);
3617 let files = [vec![large.clone()], small.to_vec()].concat();
3618 for (active_window, merge_mode, trigger, expected) in [
3620 (4, MergeMode::LastRow, 2, 1),
3621 (5, MergeMode::LastRow, 2, 1),
3622 (4, MergeMode::LastNonNull, 2, 0),
3623 (5, MergeMode::LastNonNull, 2, 0),
3624 (4, MergeMode::LastRow, 4, 0),
3625 ] {
3626 let picker = TwcsPicker {
3627 merge_mode,
3628 inactive_window_l1_merge_trigger: trigger,
3629 ..priority_test_picker(1)
3630 };
3631 let output = picker
3632 .build_output_with_time_range(
3633 RegionId::from_u64(1),
3634 assign_to_windows(files.iter(), 1),
3635 Some(active_window),
3636 Some(1),
3637 )
3638 .await
3639 .unwrap();
3640 assert_eq!(
3641 expected,
3642 output.len(),
3643 "active: {active_window}, mode: {merge_mode:?}, trigger: {trigger}"
3644 );
3645 if expected == 0 {
3646 continue;
3647 }
3648 let inputs: HashSet<_> = output[0].inputs.iter().map(FileHandle::file_id).collect();
3649 assert_eq!(
3650 small
3651 .iter()
3652 .map(FileHandle::file_id)
3653 .collect::<HashSet<_>>(),
3654 inputs
3655 );
3656 assert!(
3657 !output[0].filter_deleted,
3658 "the unselected large file still overlaps"
3659 );
3660 }
3661 }
3662
3663 #[tokio::test]
3664 async fn test_small_l1_fallback_checks_large_append_mode_peer() {
3665 let files = [
3668 (0, 20, b"a", b"m", 20),
3669 (10, 30, b"l", b"z", 20),
3670 (0, 5, b"y", b"z", 1_000),
3671 ]
3672 .map(|(start, end, min, max, size)| {
3673 new_file_handle_with_size_sequence_and_primary_key_range(
3674 FileId::random(),
3675 start,
3676 end,
3677 1,
3678 1,
3679 size,
3680 pk_range(min, max),
3681 )
3682 });
3683 for append_mode in [false, true] {
3684 let picker = TwcsPicker {
3685 append_mode,
3686 ..priority_test_picker(1)
3687 };
3688 let output = picker
3689 .build_output_with_time_range(
3690 RegionId::from_u64(1),
3691 assign_to_windows(files.iter(), 1),
3692 Some(1),
3693 Some(1),
3694 )
3695 .await
3696 .unwrap();
3697 assert!(output.is_empty(), "append mode: {append_mode}");
3698 }
3699 }
3700
3701 #[tokio::test]
3702 async fn test_settling_window_does_not_rewrite_l1_for_late_data() {
3703 let mut files = priority_test_files(3, 1, 40, false).to_vec();
3705 files.push(new_file_handle_with_size_and_sequence(
3706 FileId::random(),
3707 3_000,
3708 3_299,
3709 0,
3710 10,
3711 10,
3712 ));
3713 for (active_window, expected) in [(4, 0), (5, 1)] {
3714 let output = priority_test_picker(1)
3715 .build_output_with_time_range(
3716 RegionId::from_u64(1),
3717 assign_to_windows(files.iter(), 1),
3718 Some(active_window),
3719 Some(1),
3720 )
3721 .await
3722 .unwrap();
3723 assert_eq!(expected, output.len(), "active: {active_window}");
3724 }
3725 }
3726
3727 #[tokio::test]
3728 async fn test_older_l0_candidate_has_priority_over_newer_pure_l1_candidate() {
3729 let files = priority_test_files(0, 0, 40, false)
3730 .into_iter()
3731 .chain(priority_test_files(1, 1, 40, false))
3732 .collect::<Vec<_>>();
3733 let windows = assign_to_windows(files.iter(), 1);
3734
3735 let output = priority_test_picker(1)
3736 .build_output_with_time_range(RegionId::from_u64(1), windows, Some(1), None)
3737 .await
3738 .unwrap();
3739
3740 assert_eq!(1, output.len());
3741 assert_eq!(0, output_window(&output[0]));
3742 assert!(output[0].inputs.iter().all(|file| file.level() == 0));
3743 }
3744
3745 #[tokio::test]
3746 async fn test_older_l1_file_reduction_has_priority_over_newer_overlap_resolution() {
3747 let files = priority_test_files(0, 1, 40, false)
3748 .into_iter()
3749 .chain(priority_test_files(1, 1, 100, true))
3750 .collect::<Vec<_>>();
3751 let windows = assign_to_windows(files.iter(), 1);
3752
3753 let output = priority_test_picker(1)
3754 .build_output_with_time_range(RegionId::from_u64(1), windows, Some(1), None)
3755 .await
3756 .unwrap();
3757
3758 assert_eq!(1, output.len());
3759 assert_eq!(0, output_window(&output[0]));
3760 }
3761
3762 #[tokio::test]
3763 async fn test_multiple_slots_follow_class_priority_and_newer_first_within_class() {
3764 let files = [
3765 priority_test_files(0, 0, 40, false),
3766 priority_test_files(1, 0, 40, false),
3767 priority_test_files(2, 1, 40, false),
3768 priority_test_files(3, 1, 40, false),
3769 priority_test_files(4, 1, 100, true),
3770 priority_test_files(5, 1, 100, true),
3771 ]
3772 .into_iter()
3773 .flatten()
3774 .collect::<Vec<_>>();
3775 let windows = assign_to_windows(files.iter(), 1);
3776
3777 let output = priority_test_picker(5)
3778 .build_output_with_time_range(RegionId::from_u64(1), windows, Some(5), None)
3779 .await
3780 .unwrap();
3781 let pop_priority = output
3782 .iter()
3783 .rev()
3784 .map(|output| (output.inputs[0].level(), output_window(output)))
3785 .collect::<Vec<_>>();
3786
3787 assert_eq!(vec![(0, 1), (0, 0), (1, 3), (1, 2), (1, 4)], pop_priority);
3790 let units = crate::compaction::unit::CompactionUnit::from_picker(PickerOutput {
3791 outputs: output,
3792 time_window_size: 1,
3793 ..Default::default()
3794 })
3795 .unwrap();
3796 let unit_priority = units
3797 .iter()
3798 .map(|unit| {
3799 (
3800 unit.outputs[0].inputs[0].level(),
3801 output_window(&unit.outputs[0]),
3802 )
3803 })
3804 .collect::<Vec<_>>();
3805 assert_eq!(
3806 pop_priority, unit_priority,
3807 "local FIFO must follow the picker's execution priority"
3808 );
3809 }
3810
3811 #[tokio::test]
3812 async fn test_inactive_window_mixed_fallback_merges_within_rewrite_budget() {
3813 let files = [
3814 new_file_with_level_and_rows(0, 99, 1, 1, 1_000, 1_000_000),
3815 new_file_with_level_and_rows(0, 9, 0, 2, 10, 1),
3816 ];
3817 let windows = assign_to_windows(files.iter(), 100);
3818 let picker = TwcsPicker {
3819 trigger_file_num: 4,
3820 active_window_l1_merge_trigger: 8,
3821 inactive_window_trigger_file_num: 2,
3822 inactive_window_l1_merge_trigger: 8,
3823 time_window_seconds: Some(100),
3824 max_output_file_size: Some(2_000),
3825 append_mode: false,
3826 merge_mode: MergeMode::LastRow,
3827 max_background_tasks: None,
3828 time_range: None,
3829 all_windows_inactive: false,
3830 };
3831
3832 let output = picker
3833 .build_output_with_time_range(RegionId::from_u64(1), windows, Some(100), None)
3834 .await
3835 .unwrap();
3836
3837 assert_eq!(1, output.len());
3838 assert_eq!(2, output[0].inputs.len());
3839 }
3840
3841 #[tokio::test]
3842 async fn test_inactive_window_over_budget_merges_l0_only() {
3843 let files = [
3846 new_file_handle_with_size_and_sequence(FileId::random(), 0, 9, 0, 1, 10_000),
3847 new_file_handle_with_size_and_sequence(FileId::random(), 20, 29, 0, 2, 10),
3848 new_file_handle_with_size_and_sequence(FileId::random(), 0, 99, 1, 3, 1_000_000),
3849 ];
3850 let windows = assign_to_windows(files.iter(), 100);
3851 let picker = TwcsPicker {
3852 trigger_file_num: 4,
3853 active_window_l1_merge_trigger: 8,
3854 inactive_window_trigger_file_num: 2,
3855 inactive_window_l1_merge_trigger: 8,
3856 time_window_seconds: Some(100),
3857 max_output_file_size: Some(100_000),
3858 append_mode: false,
3859 merge_mode: MergeMode::LastRow,
3860 max_background_tasks: None,
3861 time_range: None,
3862 all_windows_inactive: false,
3863 };
3864
3865 let output = picker
3866 .build_output_with_time_range(RegionId::from_u64(1), windows, Some(100), None)
3867 .await
3868 .unwrap();
3869
3870 assert_eq!(1, output.len());
3871 assert_eq!(2, output[0].inputs.len());
3872 assert!(output[0].inputs.iter().all(|file| file.level() == 0));
3873 }
3874
3875 #[tokio::test]
3876 async fn test_inactive_window_over_budget_without_l0_pair_does_not_merge() {
3877 let files = [
3880 new_file_handle_with_size_and_sequence(FileId::random(), 0, 9, 0, 1, 10),
3881 new_file_handle_with_size_and_sequence(FileId::random(), 0, 99, 1, 2, 1_000_000),
3882 ];
3883 let windows = assign_to_windows(files.iter(), 100);
3884 let picker = TwcsPicker {
3885 trigger_file_num: 4,
3886 active_window_l1_merge_trigger: 8,
3887 inactive_window_trigger_file_num: 2,
3888 inactive_window_l1_merge_trigger: 8,
3889 time_window_seconds: Some(100),
3890 max_output_file_size: Some(100_000),
3891 append_mode: false,
3892 merge_mode: MergeMode::LastRow,
3893 max_background_tasks: None,
3894 time_range: None,
3895 all_windows_inactive: false,
3896 };
3897
3898 let output = picker
3899 .build_output_with_time_range(RegionId::from_u64(1), windows, Some(100), None)
3900 .await
3901 .unwrap();
3902
3903 assert!(output.is_empty());
3904 }
3905
3906 #[tokio::test]
3907 async fn test_inactive_window_falls_through_when_triggered_l0_pick_fails() {
3908 let files = [
3912 new_file_handle_with_size_and_sequence(FileId::random(), 0, 9, 0, 1, 100_000),
3913 new_file_handle_with_size_and_sequence(FileId::random(), 20, 29, 0, 2, 10),
3914 new_file_handle_with_size_and_sequence(FileId::random(), 0, 99, 1, 3, 50_000),
3915 ];
3916 let windows = assign_to_windows(files.iter(), 100);
3917 let picker = TwcsPicker {
3918 trigger_file_num: 4,
3919 active_window_l1_merge_trigger: 8,
3920 inactive_window_trigger_file_num: 2,
3921 inactive_window_l1_merge_trigger: 8,
3922 time_window_seconds: Some(100),
3923 max_output_file_size: Some(1_000_000),
3924 append_mode: false,
3925 merge_mode: MergeMode::LastRow,
3926 max_background_tasks: None,
3927 time_range: None,
3928 all_windows_inactive: false,
3929 };
3930
3931 let output = picker
3932 .build_output_with_time_range(RegionId::from_u64(1), windows, Some(100), None)
3933 .await
3934 .unwrap();
3935
3936 assert_eq!(1, output.len());
3937 assert_eq!(3, output[0].inputs.len());
3938 }
3939
3940 #[test]
3941 fn test_mixed_candidate_row_balance() {
3942 for (case, l1_rows, l0_rows, expected_len) in [
3943 ("L1 rows dominate", 1_000_000, 10_000, 0),
3944 ("L1 rows meet ratio", 60_000, 10_000, 4),
3945 ("L0 rows are unknown", 1_000_000, 0, 4),
3946 ] {
3947 let files = vec![
3948 new_file_with_level_and_rows(0, 99, 1, 1, 100, l1_rows),
3949 new_file_with_level_and_rows(0, 9, 0, 2, 100, l0_rows),
3950 new_file_with_level_and_rows(20, 29, 0, 3, 100, l0_rows),
3951 new_file_with_level_and_rows(40, 49, 0, 4, 100, l0_rows),
3952 ];
3953
3954 let picked = pick_mixed_count_first(vec![SortedRun::from(files)], None);
3955
3956 assert_eq!(expected_len, picked.len(), "{case}");
3957 }
3958 }
3959
3960 #[test]
3961 fn test_count_first_prefers_smaller_bytes_when_file_counts_match() {
3962 let files = (0..=DEFAULT_MAX_INPUT_FILES)
3963 .map(|idx| {
3964 let start = idx as i64 * 20;
3965 let size = if idx == 0 {
3966 100
3967 } else if idx == DEFAULT_MAX_INPUT_FILES {
3968 1
3969 } else {
3970 10
3971 };
3972 new_file(start, start + 9, idx as u64 + 1, size)
3973 })
3974 .collect::<Vec<_>>();
3975
3976 let picked = pick_count_first(vec![SortedRun::from(files)], None);
3977 let ranges = picked_ranges(&picked);
3978
3979 assert_eq!(DEFAULT_MAX_INPUT_FILES, ranges.len());
3980 assert_eq!(Some(&(20, 29)), ranges.first());
3981 let last_start = DEFAULT_MAX_INPUT_FILES as i64 * 20;
3982 assert_eq!(Some(&(last_start, last_start + 9)), ranges.last());
3983 }
3984
3985 #[test]
3986 fn test_count_first_skips_pure_rewrite_without_progress() {
3987 let picked = pick_count_first(
3991 vec![SortedRun::from(vec![
3992 new_file(0, 9, 1, 600),
3993 new_file(20, 29, 2, 600),
3994 new_file(40, 49, 3, 600),
3995 new_file(60, 69, 4, 600),
3996 ])],
3997 Some(512),
3998 );
3999
4000 assert!(picked.is_empty());
4001 }
4002
4003 #[test]
4004 fn test_count_first_allows_overlap_resolution_without_file_reduction() {
4005 let picked = pick_count_first(
4009 vec![
4010 SortedRun::from(vec![new_file(0, 19, 1, 600)]),
4011 SortedRun::from(vec![new_file(10, 29, 2, 600)]),
4012 ],
4013 Some(512),
4014 );
4015
4016 assert_eq!(vec![(0, 19), (10, 29)], picked_ranges(&picked));
4017 }
4018
4019 #[tokio::test]
4020 async fn test_l1_partial_pk_overlap_does_not_rewrite_unrelated_files() {
4021 let a_files = [
4026 (4, 5249), (7, 5218), (1, 5255), (5, 5263), (6, 5131), (3, 5143), (2, 5197), (8, 4716), ]
4035 .into_iter()
4036 .enumerate()
4037 .map(|(idx, (start, size))| {
4038 let key = b'a' + idx as u8;
4039 new_file_handle_with_size_sequence_and_primary_key_range(
4040 FileId::random(),
4041 start,
4042 10_000,
4043 1,
4044 100,
4045 size * 1024 * 1024 / 10,
4046 pk_range(&[key, b'0'], &[key, b'9']),
4047 )
4048 })
4049 .collect::<Vec<_>>();
4050 let mapper = primary_key_mapper_for_test();
4051 for (idx, file) in a_files.iter().enumerate() {
4052 for other in &a_files[idx + 1..] {
4053 assert!(file.overlap_inclusive(other)); assert!(!files_overlap_inclusive(file, other, &mapper));
4055 }
4056 }
4057
4058 let picker = TwcsPicker {
4059 trigger_file_num: 4,
4060 active_window_l1_merge_trigger: 4,
4061 inactive_window_trigger_file_num: 2,
4062 inactive_window_l1_merge_trigger: 8,
4063 time_window_seconds: Some(60),
4064 max_output_file_size: Some(512 * 1024 * 1024),
4065 append_mode: false,
4066 merge_mode: MergeMode::LastRow,
4067 max_background_tasks: Some(1),
4068 time_range: None,
4069 all_windows_inactive: false,
4070 };
4071
4072 for (case, key_range, extra_size, overlapping_a, expected_inputs) in [
4073 (
4074 "A3 and E skip the unrelated A files",
4075 Some((b'c', b'c')),
4076 5155,
4077 &[2][..],
4078 &[2, 8][..],
4079 ),
4080 (
4081 "A8 and E must not pull in A2",
4082 Some((b'h', b'h')),
4083 5155,
4084 &[7][..],
4085 &[7, 8][..],
4086 ),
4087 (
4088 "E overlaps no A: no progress",
4089 Some((b'z', b'z')),
4090 5155,
4091 &[][..],
4092 &[] as &[usize],
4093 ),
4094 (
4095 "A3 and E may split into one extra tail file",
4096 Some((b'c', b'c')),
4097 5255,
4098 &[2][..],
4099 &[2, 8][..],
4100 ),
4101 (
4102 "two A files exceed E's run budget",
4103 Some((b'b', b'c')),
4104 5155,
4105 &[1, 2][..],
4106 &[][..],
4107 ),
4108 (
4109 "sufficient E bytes allow two A files separated in time order",
4110 Some((b'b', b'c')),
4111 6000,
4112 &[1, 2][..],
4113 &[1, 2, 8][..],
4114 ),
4115 (
4116 "unknown PK bounds include the whole dominant run in the budget",
4117 None,
4118 5155,
4119 &[0, 1, 2, 3, 4, 5, 6, 7][..],
4120 &[][..],
4121 ),
4122 ] {
4123 let extra = new_file_handle_with_size_sequence_and_primary_key_range(
4124 FileId::random(),
4125 1_000,
4126 9_000,
4127 1,
4128 100,
4129 extra_size * 1024 * 1024 / 10,
4130 key_range.and_then(|(min, max)| pk_range(&[min, b'0'], &[max, b'9'])),
4131 );
4132 for (idx, file) in a_files.iter().enumerate() {
4133 assert!(file.overlap_inclusive(&extra));
4134 assert_eq!(
4135 overlapping_a.contains(&idx),
4136 files_overlap_inclusive(file, &extra, &mapper),
4137 "{case}: A{}",
4138 idx + 1,
4139 );
4140 }
4141
4142 let mut files = a_files.clone();
4143 files.push(extra);
4144 for active_window in [Some(0), None] {
4145 let expected_inputs = if active_window.is_some() {
4148 &[][..]
4149 } else {
4150 expected_inputs
4151 };
4152 let expected = expected_inputs
4153 .iter()
4154 .map(|&idx| files[idx].file_id())
4155 .collect::<HashSet<_>>();
4156 let windows = assign_to_windows(files.iter(), 60);
4157 let outputs = picker
4158 .build_output_with_time_range(
4159 RegionId::from_u64(0),
4160 windows,
4161 active_window,
4162 Some(60),
4163 )
4164 .await
4165 .unwrap();
4166
4167 assert_eq!(
4168 usize::from(!expected_inputs.is_empty()),
4169 outputs.len(),
4170 "{case}, active window: {active_window:?}"
4171 );
4172 let actual = outputs
4173 .iter()
4174 .flat_map(|output| output.inputs.iter().map(FileHandle::file_id))
4175 .collect::<HashSet<_>>();
4176 assert_eq!(expected, actual, "{case}, active window: {active_window:?}");
4177 }
4178 }
4179 }
4180
4181 #[rstest::rstest]
4182 #[case::active_filtered(Some(0), false, 1000, &[0, 2])]
4183 #[case::inactive_filtered(None, false, 1000, &[0, 2])]
4184 #[case::older_reduction_before_newer_overlap(Some(3), true, 1000, &[0, 2])]
4185 #[case::contiguous_has_more_reduction(Some(0), false, 90, &[0, 1, 2, 3])]
4186 #[case::filtered_has_equal_reduction_with_less_io(Some(0), false, 600, &[0, 2])]
4187 #[tokio::test]
4188 async fn test_l1_file_reduction_considers_filtered_candidates(
4189 #[case] active_window: Option<i64>,
4190 #[case] newer_overlap_window: bool,
4191 #[case] unrelated_size: u64,
4192 #[case] expected_indices: &[usize],
4193 ) {
4194 let mut files = [
4197 (b'a', 100),
4198 (b'b', unrelated_size),
4199 (b'a', 100),
4200 (b'c', unrelated_size),
4201 ]
4202 .into_iter()
4203 .enumerate()
4204 .map(|(idx, (key, size))| {
4205 new_file_handle_with_size_sequence_and_primary_key_range(
4206 FileId::random(),
4207 idx as i64 * 100,
4208 749,
4209 1,
4210 1,
4211 size,
4212 pk_range(&[key], &[key]),
4213 )
4214 })
4215 .collect::<Vec<_>>();
4216 let expected = expected_indices
4217 .iter()
4218 .map(|&idx| files[idx].file_id())
4219 .collect::<Vec<_>>();
4220 if newer_overlap_window {
4221 files.extend(priority_test_files(1, 1, 512, true));
4224 }
4225 let picker = TwcsPicker {
4226 max_output_file_size: Some(512),
4227 ..priority_test_picker(1)
4228 };
4229 let outputs = picker
4230 .build_output_with_time_range(
4231 RegionId::from_u64(0),
4232 assign_to_windows(files.iter(), 1),
4233 active_window,
4234 Some(1),
4235 )
4236 .await
4237 .unwrap();
4238
4239 assert_eq!(1, outputs.len());
4240 assert_eq!(
4241 expected,
4242 outputs[0]
4243 .inputs
4244 .iter()
4245 .map(FileHandle::file_id)
4246 .collect::<Vec<_>>()
4247 );
4248 }
4249
4250 #[tokio::test]
4251 async fn test_overlap_resolution_limits_dominant_run_rewrites() {
4252 let file_size = 512 * 1024 * 1024;
4253 for (case, peer_sizes, max_output_size, should_pick) in [
4255 (
4256 "small peer cannot rewrite any slice of the large run",
4257 vec![file_size],
4258 file_size,
4259 false,
4260 ),
4261 (
4262 "one byte below the run budget",
4263 [vec![file_size; 6], vec![file_size - 1]].concat(),
4264 file_size,
4265 false,
4266 ),
4267 (
4268 "exact run budget boundary",
4269 vec![file_size; 7],
4270 file_size,
4271 true,
4272 ),
4273 ("equal sized runs", vec![file_size; 8], file_size, true),
4274 (
4275 "file reduction keeps its own admission",
4276 vec![file_size],
4277 2 * file_size,
4278 true,
4279 ),
4280 ] {
4281 let mut files = (0..8)
4282 .map(|idx| {
4283 let key = b'a' + idx as u8;
4284 new_file_handle_with_size_sequence_and_primary_key_range(
4285 FileId::random(),
4286 idx,
4287 10_000,
4288 1,
4289 100,
4290 file_size,
4291 pk_range(&[key, b'0'], &[key, b'9']),
4292 )
4293 })
4294 .collect::<Vec<_>>();
4295 files.extend(peer_sizes.into_iter().enumerate().map(|(idx, size)| {
4296 let start = 1_000 * (idx as i64 + 1);
4297 new_file_handle_with_size_sequence_and_primary_key_range(
4298 FileId::random(),
4299 start,
4300 start + 999,
4301 1,
4302 100,
4303 size,
4304 pk_range(b"a0", b"h9"),
4305 )
4306 }));
4307 let reduces_files = max_output_size > file_size;
4310 let picker = TwcsPicker {
4311 trigger_file_num: 4,
4312 active_window_l1_merge_trigger: 4,
4313 inactive_window_trigger_file_num: 2,
4314 inactive_window_l1_merge_trigger: 8,
4315 time_window_seconds: Some(60),
4316 max_output_file_size: Some(max_output_size),
4317 append_mode: false,
4318 merge_mode: MergeMode::LastRow,
4319 max_background_tasks: Some(1),
4320 time_range: None,
4321 all_windows_inactive: false,
4322 };
4323
4324 for active_window in [Some(0), None] {
4325 let expected = if should_pick && (reduces_files || active_window.is_none()) {
4326 files
4327 .iter()
4328 .map(FileHandle::file_id)
4329 .collect::<HashSet<_>>()
4330 } else {
4331 HashSet::new()
4332 };
4333 let outputs = picker
4334 .build_output_with_time_range(
4335 RegionId::from_u64(0),
4336 assign_to_windows(files.iter(), 60),
4337 active_window,
4338 Some(60),
4339 )
4340 .await
4341 .unwrap();
4342 let actual = outputs
4343 .iter()
4344 .flat_map(|output| output.inputs.iter().map(FileHandle::file_id))
4345 .collect::<HashSet<_>>();
4346 assert_eq!(expected, actual, "{case}, active window: {active_window:?}");
4347 }
4348 }
4349 }
4350
4351 #[tokio::test]
4352 async fn test_quiet_or_manual_window_resolves_dominant_run_overlap() {
4353 let file_size = 512 * 1024 * 1024;
4354 let l1_file =
4355 |start, end, sequence, size, range: Option<(bytes::Bytes, bytes::Bytes)>, num_rows| {
4356 let file = new_file_handle_with_size_sequence_and_primary_key_range(
4357 FileId::random(),
4358 start,
4359 end,
4360 1,
4361 sequence,
4362 size,
4363 range.clone(),
4364 );
4365 FileHandle::new_with_primary_key_range(
4366 FileMeta {
4367 num_rows,
4368 ..file.meta_ref().clone()
4369 },
4370 crate::test_util::new_noop_file_purger(),
4371 range,
4372 )
4373 };
4374
4375 for (newer_sequence, num_rows, manual, resolves) in [
4379 (108, 1, false, false),
4380 (109, 1, false, true),
4381 (109, 0, false, false),
4382 (100, 1, true, true),
4383 ] {
4384 let mut window_files = (0..8)
4387 .map(|idx| {
4388 let key = b'a' + idx as u8;
4389 let range = pk_range(&[key, b'0'], &[key, b'9']);
4390 l1_file(idx, 10_000, 100, file_size, range, num_rows)
4391 })
4392 .collect::<Vec<_>>();
4393 window_files.push(l1_file(
4394 1_000,
4395 1_999,
4396 100,
4397 file_size / 8,
4398 pk_range(b"a0", b"h9"),
4399 num_rows,
4400 ));
4401 let expected = window_files
4402 .iter()
4403 .map(FileHandle::file_id)
4404 .collect::<HashSet<_>>();
4405 let picker = TwcsPicker {
4406 trigger_file_num: 4,
4407 active_window_l1_merge_trigger: 4,
4408 inactive_window_trigger_file_num: 2,
4409 inactive_window_l1_merge_trigger: 16,
4410 time_window_seconds: Some(60),
4411 max_output_file_size: Some(file_size),
4412 append_mode: false,
4413 merge_mode: MergeMode::LastRow,
4414 max_background_tasks: Some(1),
4415 time_range: None,
4416 all_windows_inactive: manual,
4417 };
4418 let mut files = window_files;
4419 files.push(l1_file(
4420 120_000,
4421 120_999,
4422 newer_sequence,
4423 file_size,
4424 pk_range(b"a0", b"h9"),
4425 1,
4426 ));
4427 let outputs = picker
4428 .build_output_with_time_range(
4429 RegionId::from_u64(0),
4430 assign_to_windows(files.iter(), 60),
4431 manual.then_some(0),
4432 Some(60),
4433 )
4434 .await
4435 .unwrap();
4436 let actual = outputs
4437 .iter()
4438 .flat_map(|output| output.inputs.iter().map(FileHandle::file_id))
4439 .collect::<HashSet<_>>();
4440 let case = format!("sequence {newer_sequence}, rows {num_rows}, manual {manual}");
4441 if resolves {
4442 assert_eq!(expected, actual, "{case}");
4443 } else {
4444 assert!(actual.is_empty(), "{case}: {actual:?}");
4445 }
4446 }
4447 }
4448
4449 #[test]
4450 fn test_count_first_prefers_guaranteed_reduction_over_pure_rewrite() {
4451 let picked = pick_count_first(
4455 vec![SortedRun::from(vec![
4456 new_file(0, 9, 1, 600),
4457 new_file(10, 19, 2, 600),
4458 new_file(20, 29, 3, 600),
4459 new_file(30, 39, 4, 10),
4460 new_file(40, 49, 5, 10),
4461 new_file(50, 59, 6, 10),
4462 ])],
4463 Some(512),
4464 );
4465
4466 assert_eq!(vec![(30, 39), (40, 49), (50, 59)], picked_ranges(&picked));
4467 }
4468
4469 #[test]
4470 fn test_count_first_prefers_earlier_time_on_exact_tie() {
4471 let files = (0..=DEFAULT_MAX_INPUT_FILES)
4472 .map(|idx| {
4473 let start = idx as i64 * 20;
4474 new_file(start, start + 9, idx as u64 + 1, 10)
4475 })
4476 .collect::<Vec<_>>();
4477
4478 let picked = pick_count_first(vec![SortedRun::from(files)], None);
4479 let ranges = picked_ranges(&picked);
4480
4481 assert_eq!(DEFAULT_MAX_INPUT_FILES, ranges.len());
4482 assert_eq!(Some(&(0, 9)), ranges.first());
4483 let last_start = (DEFAULT_MAX_INPUT_FILES as i64 - 1) * 20;
4484 assert_eq!(Some(&(last_start, last_start + 9)), ranges.last());
4485 }
4486
4487 #[test]
4488 fn test_count_first_keeps_each_interleaved_run_contiguous() {
4489 let picked = pick_count_first(
4490 vec![
4491 SortedRun::from(vec![
4492 new_file(0, 9, 1, 1),
4493 new_file(20, 29, 2, 1),
4494 new_file(40, 49, 3, 1),
4495 ]),
4496 SortedRun::from(vec![new_file(10, 19, 4, 1), new_file(30, 39, 5, 1)]),
4497 ],
4498 None,
4499 );
4500
4501 assert_eq!(
4502 vec![(0, 9), (10, 19), (20, 29), (30, 39), (40, 49)],
4503 picked_ranges(&picked)
4504 );
4505 }
4506
4507 #[test]
4514 fn test_count_first_candidate_spans_same_sequence_files() {
4515 let picked = pick_count_first(
4516 vec![SortedRun::from(vec![
4517 new_file(0, 9, 1, 10),
4518 new_file(10, 19, 2, 10),
4520 new_file(10, 19, 2, 10),
4521 new_file(10, 19, 2, 10),
4522 new_file(20, 29, 3, 10),
4523 ])],
4524 None,
4525 );
4526
4527 assert_eq!(
4528 vec![(0, 9), (10, 19), (10, 19), (10, 19), (20, 29)],
4529 picked_ranges(&picked)
4530 );
4531 }
4532
4533 #[tokio::test]
4539 async fn test_count_first_merges_window_with_interleaved_flush_groups() {
4540 let files = [
4541 new_file_handle_with_size_and_sequence(FileId::random(), 0, 9, 0, 1, 10),
4543 new_file_handle_with_size_and_sequence(FileId::random(), 10, 19, 0, 2, 10),
4545 new_file_handle_with_size_and_sequence(FileId::random(), 10, 19, 0, 2, 10),
4546 new_file_handle_with_size_and_sequence(FileId::random(), 10, 19, 0, 2, 10),
4547 new_file_handle_with_size_and_sequence(FileId::random(), 20, 29, 0, 3, 10),
4549 ];
4550 let windows = assign_to_windows(files.iter(), 100);
4551 let picker = TwcsPicker {
4552 trigger_file_num: 3,
4553 active_window_l1_merge_trigger: 8,
4554 inactive_window_trigger_file_num: 3,
4555 inactive_window_l1_merge_trigger: 8,
4556 time_window_seconds: Some(100),
4557 max_output_file_size: None,
4558 append_mode: false,
4559 merge_mode: MergeMode::LastRow,
4560 max_background_tasks: None,
4561 time_range: None,
4562 all_windows_inactive: false,
4563 };
4564
4565 let output = picker
4566 .build_output_with_time_range(RegionId::from_u64(1), windows, Some(0), None)
4567 .await
4568 .unwrap();
4569
4570 assert_eq!(1, output.len());
4571 assert_eq!(5, output[0].inputs.len());
4573 }
4574}