1use std::collections::hash_map::Entry;
16use std::collections::{BTreeMap, HashMap, HashSet};
17use std::fmt::Debug;
18use std::sync::{Arc, LazyLock};
19
20use common_base::readable_size::ReadableSize;
21use common_telemetry::{debug, info};
22use common_time::Timestamp;
23use common_time::range::TimestampRange;
24use common_time::timestamp::TimeUnit;
25use common_time::timestamp_millis::BucketAligned;
26use snafu::ResultExt;
27use store_api::storage::RegionId;
28
29use crate::compaction::CompactionOutput;
30use crate::compaction::buckets::infer_time_bucket;
31use crate::compaction::compactor::CompactionRegion;
32use crate::compaction::picker::{Picker, PickerOutput, get_expired_ssts};
33use crate::compaction::run::{
34 Ranged, SortedRun, files_overlap, files_overlap_inclusive, find_sorted_runs,
35 find_sorted_runs_by_time_range, merge_primary_key_ranges, primary_key_ranges_overlap,
36};
37use crate::error::{JoinSnafu, Result};
38use crate::sst::file::{FileHandle, Level, overlaps};
39use crate::sst::primary_key::PrimaryKeyRangeMapper;
40use crate::sst::version::LevelMeta;
41
42const LEVEL_COMPACTED: Level = 1;
43
44#[derive(Clone, Copy, Debug)]
45enum PickPhase {
46 HasL0,
47 L1FileReduction,
48 L1OverlapOnly,
49}
50
51impl PickPhase {
52 fn applies_to(self, window: &Window) -> bool {
53 match self {
54 Self::HasL0 => window.files().any(|file| file.level() == 0),
55 Self::L1FileReduction | Self::L1OverlapOnly => {
56 window.files().any(|file| file.level() != 0)
57 }
58 }
59 }
60}
61
62const PICK_PHASES: [PickPhase; 3] = [
63 PickPhase::HasL0,
64 PickPhase::L1FileReduction,
65 PickPhase::L1OverlapOnly,
66];
67
68struct WindowPickContext<'a> {
69 active_window: Option<i64>,
70 files: &'a Window,
71 windows: &'a BTreeMap<i64, Window>,
72 phase: PickPhase,
73 primary_key_mapper: &'a PrimaryKeyRangeMapper,
74}
75
76struct WindowOutputContext {
77 active_window: Option<i64>,
78 time_window_size: Option<i64>,
79 max_outputs: Option<usize>,
80 primary_key_mapper: Arc<PrimaryKeyRangeMapper>,
81}
82
83const MAX_L1_L0_ROW_RATIO: usize = 2;
85
86const DEFAULT_MAX_INPUT_FILES: usize = 16;
88
89const MAX_INPUT_FILES_ENV: &str = "GREPTIME_TWCS_MAX_INPUT_FILES";
90
91static MAX_INPUT_FILES: LazyLock<usize> = LazyLock::new(|| {
94 let env_value = std::env::var(MAX_INPUT_FILES_ENV).ok();
95 parse_max_input_files(env_value.as_deref())
96});
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 max_background_tasks: Option<usize>,
126 pub(crate) time_range: Option<TimestampRange>,
128}
129
130impl TwcsPicker {
131 async fn build_output(
132 &self,
133 region_id: RegionId,
134 time_windows: BTreeMap<i64, Window>,
135 context: WindowOutputContext,
136 ) -> Result<Vec<CompactionOutput>> {
137 let WindowOutputContext {
138 active_window,
139 time_window_size,
140 max_outputs,
141 primary_key_mapper,
142 } = context;
143 let mut output = vec![];
144 let windows = time_windows
145 .values()
146 .rev()
147 .filter(|window| {
148 !window.files.is_empty()
149 && self.time_range.as_ref().is_none_or(|time_range| {
150 time_window_size.is_none_or(|time_window_size| {
151 time_window_intersects_range(
152 window.time_window,
153 time_window_size,
154 time_range,
155 )
156 })
157 })
158 })
159 .map(|window| window.time_window)
160 .collect::<Vec<_>>();
161 let time_windows = Arc::new(time_windows);
162 let chunk_size = self.max_background_tasks.unwrap_or(windows.len()).max(1);
165 let mut selected_windows = HashSet::new();
166 'phases: for phase in PICK_PHASES {
167 for chunk in windows.chunks(chunk_size) {
168 let mut handles = Vec::with_capacity(chunk.len());
169 for window in chunk {
170 if selected_windows.contains(window) {
171 continue;
172 }
173 if !time_windows
174 .get(window)
175 .is_some_and(|files| phase.applies_to(files))
176 {
177 continue;
178 }
179 let picker = self.clone();
180 let time_windows = time_windows.clone();
181 let window = *window;
182 let primary_key_mapper = primary_key_mapper.clone();
183 handles.push(common_runtime::spawn_blocking_compact(move || {
184 time_windows.get(&window).map(|files| {
185 (
186 window,
187 picker.find_inputs(
188 region_id,
189 WindowPickContext {
190 active_window,
191 files,
192 windows: &time_windows,
193 phase,
194 primary_key_mapper: &primary_key_mapper,
195 },
196 ),
197 )
198 })
199 }));
200 tokio::task::yield_now().await;
201 }
202 for result in futures::future::join_all(handles).await {
203 let Some((window, (inputs, filter_deleted))) = result.context(JoinSnafu)?
204 else {
205 continue;
206 };
207 if inputs.is_empty() {
208 continue;
209 }
210
211 selected_windows.insert(window);
212 output.push(CompactionOutput {
213 output_level: LEVEL_COMPACTED, inputs,
215 filter_deleted,
216 output_time_range: None, });
218
219 if let Some(max_outputs) = max_outputs
220 && output.len() >= max_outputs
221 {
222 debug!(
223 "Region ({:?}) compaction output limit ({}) reached, remaining candidates discarded",
224 region_id, max_outputs
225 );
226 break 'phases;
227 }
228 }
229 }
230 }
231 output.reverse();
234 Ok(output)
235 }
236
237 fn find_inputs(
238 &self,
239 region_id: RegionId,
240 context: WindowPickContext<'_>,
241 ) -> (Vec<FileHandle>, bool) {
242 let WindowPickContext {
243 active_window,
244 files,
245 windows,
246 phase,
247 primary_key_mapper,
248 } = context;
249 let is_active_window = active_window == Some(files.time_window);
250 let window = &files.time_window;
251 let mut files_to_merge: Vec<_> = files.files().cloned().collect();
252
253 if self.append_mode
255 && let Some(max_size) = self.max_output_file_size
256 {
257 let (kept_files, ignored_files) = files_to_merge
258 .into_iter()
259 .partition(|file| file.size() <= max_size);
260 files_to_merge = kept_files;
261 if !ignored_files.is_empty() {
262 info!(
263 "Skipped {} large files in append mode for region {}, window {}, max_size: {}",
264 ignored_files.len(),
265 region_id,
266 window,
267 max_size
268 );
269 }
270 }
271
272 let (l0_files, l1_files): (Vec<_>, Vec<_>) = files_to_merge
273 .into_iter()
274 .partition(|file| file.level() == 0);
275 let num_l0_files = l0_files.len();
276 let num_l1_files = l1_files.len();
277 if !is_active_window
278 && files.files.len() < self.inactive_window_trigger_file_num
279 && num_l1_files < self.inactive_window_l1_merge_trigger
280 {
281 return (vec![], false);
282 }
283 let (inputs, found_runs) = if is_active_window {
284 match phase {
285 PickPhase::HasL0 if num_l0_files >= self.trigger_file_num => pick_candidate_files(
286 l0_files,
287 self.max_output_file_size,
288 pick_count_first,
289 primary_key_mapper,
290 ),
291 PickPhase::L1FileReduction | PickPhase::L1OverlapOnly
292 if num_l1_files >= self.active_window_l1_merge_trigger =>
293 {
294 pick_l1_candidate_files(
295 l1_files,
296 self.max_output_file_size,
297 phase,
298 primary_key_mapper,
299 )
300 }
301 _ => (vec![], 0),
302 }
303 } else {
304 pick_inactive_window_files(
305 l0_files,
306 l1_files,
307 InactiveWindowPick {
308 l0_file_num: self.inactive_window_trigger_file_num,
309 l1_file_num: self.inactive_window_l1_merge_trigger,
310 phase,
311 primary_key_mapper,
312 },
313 self.max_output_file_size,
314 )
315 };
316 if inputs.is_empty() {
317 return (inputs, false);
318 }
319
320 let filter_deleted = !self.append_mode
321 && !window_has_overlap(files, windows)
322 && !selected_overlaps_unselected(&inputs, files, primary_key_mapper);
323
324 if inputs.len() > 1 {
325 log_pick_result(
327 region_id,
328 *window,
329 active_window,
330 found_runs,
331 files.files.len(),
332 self.max_output_file_size,
333 filter_deleted,
334 &inputs,
335 );
336 }
337 (inputs, filter_deleted)
338 }
339}
340
341#[derive(Debug, Clone, Copy)]
355struct InactiveWindowPick<'a> {
356 l0_file_num: usize,
357 l1_file_num: usize,
358 phase: PickPhase,
359 primary_key_mapper: &'a PrimaryKeyRangeMapper,
360}
361
362fn pick_inactive_window_files(
363 l0_files: Vec<FileHandle>,
364 l1_files: Vec<FileHandle>,
365 pick: InactiveWindowPick<'_>,
366 max_output_file_size: Option<u64>,
367) -> (Vec<FileHandle>, usize) {
368 if !matches!(pick.phase, PickPhase::HasL0) {
369 return if l1_files.len() >= pick.l1_file_num {
370 pick_l1_candidate_files(
371 l1_files,
372 max_output_file_size,
373 pick.phase,
374 pick.primary_key_mapper,
375 )
376 } else {
377 (vec![], 0)
378 };
379 }
380
381 if l0_files.len() >= pick.l0_file_num {
382 let candidate = pick_candidate_files(
383 l0_files.clone(),
384 max_output_file_size,
385 pick_count_first,
386 pick.primary_key_mapper,
387 );
388 if !candidate.0.is_empty() {
389 return candidate;
390 }
391 }
392
393 let candidate = pick_candidate_files(
394 l0_files.clone(),
395 max_output_file_size,
396 pick_count_first,
397 pick.primary_key_mapper,
398 );
399 if !candidate.0.is_empty() {
400 return candidate;
401 }
402 let mut all_files = l0_files.clone();
403 all_files.extend(l1_files);
404 let candidate = pick_candidate_files(
405 all_files,
406 max_output_file_size,
407 pick_mixed_within_budget,
408 pick.primary_key_mapper,
409 );
410 if !candidate.0.is_empty() {
411 return candidate;
412 }
413
414 pick_candidate_files(
415 l0_files,
416 max_output_file_size,
417 pick_unbalanced_count_first,
418 pick.primary_key_mapper,
419 )
420}
421
422fn pick_l1_candidate_files(
423 l1_files: Vec<FileHandle>,
424 max_output_file_size: Option<u64>,
425 phase: PickPhase,
426 mapper: &PrimaryKeyRangeMapper,
427) -> (Vec<FileHandle>, usize) {
428 let picker = match phase {
429 PickPhase::L1FileReduction => pick_l1_file_reduction,
430 PickPhase::L1OverlapOnly => pick_l1_overlap_only,
431 PickPhase::HasL0 => return (vec![], 0),
432 };
433 pick_candidate_files(l1_files, max_output_file_size, picker, mapper)
434}
435
436type CandidatePicker =
437 fn(Vec<SortedRun<FileHandle>>, Option<u64>, &PrimaryKeyRangeMapper) -> Vec<FileHandle>;
438
439fn pick_candidate_files(
440 mut files: Vec<FileHandle>,
441 max_output_file_size: Option<u64>,
442 picker: CandidatePicker,
443 mapper: &PrimaryKeyRangeMapper,
444) -> (Vec<FileHandle>, usize) {
445 let sorted_runs = if files.len() < 1024 {
446 find_sorted_runs(&mut files, |lhs, rhs| files_overlap(lhs, rhs, mapper))
447 } else {
448 find_sorted_runs_by_time_range(&mut files)
449 };
450 let found_runs = sorted_runs.len();
451 (
452 picker(sorted_runs, max_output_file_size, mapper),
453 found_runs,
454 )
455}
456
457#[derive(Debug)]
458struct OrderedFile<'a> {
459 file: &'a FileHandle,
460 run_id: usize,
461 position_in_run: usize,
462}
463
464#[derive(Debug, Default)]
467struct Candidate {
468 num_files: usize,
470 total_size: usize,
472 largest_file_size: usize,
474 overlap_participants: usize,
477 l0_rows: usize,
479 l1_rows: usize,
481 has_l0: bool,
482 has_l1: bool,
483 has_unknown_rows: bool,
485}
486
487impl Candidate {
488 fn absorb(
492 &mut self,
493 file: &OrderedFile,
494 preceding: &[OrderedFile],
495 participations: &mut Vec<bool>,
496 mapper: &PrimaryKeyRangeMapper,
497 ) {
498 self.num_files += 1;
499 let file_size = file.file.size() as usize;
500 self.total_size += file_size;
501 self.largest_file_size = self.largest_file_size.max(file_size);
502 self.absorb_level_rows(file.file);
503
504 let mut participates = false;
505 for (offset, other) in preceding.iter().enumerate() {
506 if file.run_id != other.run_id && files_overlap_inclusive(file.file, other.file, mapper)
507 {
508 if !participations[offset] {
509 participations[offset] = true;
510 self.overlap_participants += 1;
511 }
512 participates = true;
513 }
514 }
515 participations.push(participates);
516 if participates {
517 self.overlap_participants += 1;
518 }
519 }
520
521 fn absorb_level_rows(&mut self, file: &FileHandle) {
522 let num_rows = file.num_rows();
523 self.has_unknown_rows |= num_rows == 0;
524 if file.level() == 0 {
525 self.has_l0 = true;
526 self.l0_rows = self.l0_rows.saturating_add(num_rows);
527 } else {
528 self.has_l1 = true;
529 self.l1_rows = self.l1_rows.saturating_add(num_rows);
530 }
531 }
532
533 fn predicted_output_files(&self, max_output_file_size: Option<u64>) -> usize {
538 match max_output_file_size {
539 Some(max) if max > 0 => self.total_size.div_ceil(max as usize).max(1),
540 _ => 1,
541 }
542 }
543
544 fn file_reduction(&self, max_output_file_size: Option<u64>) -> usize {
548 self.num_files
549 .saturating_sub(self.predicted_output_files(max_output_file_size))
550 }
551
552 fn is_balanced(&self) -> bool {
555 self.largest_file_size <= self.total_size - self.largest_file_size
556 }
557
558 fn has_balanced_level_rows(&self) -> bool {
561 !self.has_l0
562 || !self.has_l1
563 || self.has_unknown_rows
564 || self.l1_rows <= self.l0_rows.saturating_mul(MAX_L1_L0_ROW_RATIO)
565 }
566
567 fn has_mixed_levels(&self) -> bool {
568 self.has_l0 && self.has_l1
569 }
570
571 fn within_rewrite_budget(&self, max_output_file_size: Option<u64>) -> bool {
576 match max_output_file_size {
577 Some(limit) if limit > 0 => self.total_size <= limit as usize,
578 _ => true,
579 }
580 }
581
582 fn makes_progress(&self, max_output_file_size: Option<u64>) -> bool {
587 self.file_reduction(max_output_file_size) > 0 || self.overlap_participants > 0
588 }
589
590 fn score(&self, max_output_file_size: Option<u64>) -> CandidateScore {
591 CandidateScore {
592 file_reduction: self.file_reduction(max_output_file_size),
593 overlap_participants: self.overlap_participants,
594 total_size: self.total_size,
595 }
596 }
597}
598
599#[derive(Debug)]
603struct CandidateScore {
604 file_reduction: usize,
605 overlap_participants: usize,
606 total_size: usize,
607}
608
609impl CandidateScore {
610 fn is_better_than(&self, other: &Self) -> bool {
611 self.file_reduction
612 .cmp(&other.file_reduction)
613 .then_with(|| self.overlap_participants.cmp(&other.overlap_participants))
614 .then_with(|| other.total_size.cmp(&self.total_size))
615 .is_gt()
616 }
617}
618
619fn pick_count_first(
635 sorted_runs: Vec<SortedRun<FileHandle>>,
636 max_output_file_size: Option<u64>,
637 mapper: &PrimaryKeyRangeMapper,
638) -> Vec<FileHandle> {
639 pick_count_first_where(
640 sorted_runs,
641 max_output_file_size,
642 mapper,
643 is_balanced_candidate,
644 )
645}
646
647fn pick_l1_file_reduction(
648 sorted_runs: Vec<SortedRun<FileHandle>>,
649 max_output_file_size: Option<u64>,
650 mapper: &PrimaryKeyRangeMapper,
651) -> Vec<FileHandle> {
652 pick_count_first_where(sorted_runs, max_output_file_size, mapper, |candidate| {
653 is_balanced_candidate(candidate) && candidate.file_reduction(max_output_file_size) > 0
654 })
655}
656
657fn pick_l1_overlap_only(
658 sorted_runs: Vec<SortedRun<FileHandle>>,
659 max_output_file_size: Option<u64>,
660 mapper: &PrimaryKeyRangeMapper,
661) -> Vec<FileHandle> {
662 pick_count_first_where(sorted_runs, max_output_file_size, mapper, |candidate| {
663 is_balanced_candidate(candidate)
664 && candidate.file_reduction(max_output_file_size) == 0
665 && candidate.overlap_participants > 0
666 })
667}
668
669#[cfg(test)]
670fn pick_mixed_count_first(
671 sorted_runs: Vec<SortedRun<FileHandle>>,
672 max_output_file_size: Option<u64>,
673 mapper: &PrimaryKeyRangeMapper,
674) -> Vec<FileHandle> {
675 pick_count_first_where(sorted_runs, max_output_file_size, mapper, |candidate| {
676 candidate.has_mixed_levels() && is_balanced_candidate(candidate)
677 })
678}
679
680fn pick_mixed_within_budget(
683 sorted_runs: Vec<SortedRun<FileHandle>>,
684 max_output_file_size: Option<u64>,
685 mapper: &PrimaryKeyRangeMapper,
686) -> Vec<FileHandle> {
687 pick_count_first_where(sorted_runs, max_output_file_size, mapper, |candidate| {
688 candidate.has_mixed_levels() && candidate.within_rewrite_budget(max_output_file_size)
689 })
690}
691
692fn pick_unbalanced_count_first(
696 sorted_runs: Vec<SortedRun<FileHandle>>,
697 max_output_file_size: Option<u64>,
698 mapper: &PrimaryKeyRangeMapper,
699) -> Vec<FileHandle> {
700 pick_count_first_where(sorted_runs, max_output_file_size, mapper, |_| true)
701}
702
703fn is_balanced_candidate(candidate: &Candidate) -> bool {
704 candidate.is_balanced() && candidate.has_balanced_level_rows()
705}
706
707fn pick_count_first_where(
708 sorted_runs: Vec<SortedRun<FileHandle>>,
709 max_output_file_size: Option<u64>,
710 mapper: &PrimaryKeyRangeMapper,
711 is_eligible: impl Fn(&Candidate) -> bool,
712) -> Vec<FileHandle> {
713 let files = ordered_files(&sorted_runs);
714
715 let mut best = None;
716 for left in 0..files.len() {
717 let mut candidate = Candidate::default();
718 let right_bound = left.saturating_add(*MAX_INPUT_FILES).min(files.len());
719 let mut participations: Vec<bool> = Vec::with_capacity(right_bound - left);
720 for right in left..right_bound {
721 candidate.absorb(
722 &files[right],
723 &files[left..right],
724 &mut participations,
725 mapper,
726 );
727 if candidate.num_files < 2
728 || !is_eligible(&candidate)
729 || !candidate.makes_progress(max_output_file_size)
730 {
731 continue;
732 }
733
734 let score = candidate.score(max_output_file_size);
735 if best
736 .as_ref()
737 .is_none_or(|(best_score, _)| score.is_better_than(best_score))
738 {
739 best = Some((score, &files[left..=right]));
740 }
741 }
742 }
743
744 let Some((_, best)) = best else {
745 return vec![];
746 };
747 best.iter().map(|file| file.file.clone()).collect()
748}
749
750fn ordered_files(sorted_runs: &[SortedRun<FileHandle>]) -> Vec<OrderedFile<'_>> {
753 let mut files = sorted_runs
754 .iter()
755 .enumerate()
756 .flat_map(|(run_id, run)| {
757 run.items()
758 .iter()
759 .enumerate()
760 .map(move |(position_in_run, file)| OrderedFile {
761 file,
762 run_id,
763 position_in_run,
764 })
765 })
766 .collect::<Vec<_>>();
767 files.sort_unstable_by(|lhs, rhs| {
768 let (lhs_start, lhs_end) = lhs.file.range();
769 let (rhs_start, rhs_end) = rhs.file.range();
770 lhs_start
771 .cmp(&rhs_start)
772 .then_with(|| rhs_end.cmp(&lhs_end))
773 .then_with(|| lhs.run_id.cmp(&rhs.run_id))
774 .then_with(|| lhs.position_in_run.cmp(&rhs.position_in_run))
775 });
776 files
777}
778
779fn selected_overlaps_unselected(
780 selected: &[FileHandle],
781 window: &Window,
782 mapper: &PrimaryKeyRangeMapper,
783) -> bool {
784 let Some((span_start, span_end)) = selected
787 .iter()
788 .map(Ranged::range)
789 .reduce(|(start_a, end_a), (start_b, end_b)| (start_a.min(start_b), end_a.max(end_b)))
790 else {
791 return false;
792 };
793 let selected_file_ids = selected
794 .iter()
795 .map(FileHandle::file_id)
796 .collect::<HashSet<_>>();
797 window
798 .files()
799 .filter(|file| {
800 let (start, end) = file.range();
801 start <= span_end && span_start <= end
802 })
803 .filter(|file| !selected_file_ids.contains(&file.file_id()))
804 .any(|unselected| {
805 selected
806 .iter()
807 .any(|selected| files_overlap_inclusive(selected, unselected, mapper))
808 })
809}
810
811#[allow(clippy::too_many_arguments)]
812fn log_pick_result(
813 region_id: RegionId,
814 window: i64,
815 active_window: Option<i64>,
816 found_runs: usize,
817 file_num: usize,
818 max_output_file_size: Option<u64>,
819 filter_deleted: bool,
820 inputs: &[FileHandle],
821) {
822 let input_file_str: Vec<String> = inputs
823 .iter()
824 .map(|f| {
825 let range = f.range();
826 let start = range.0.to_iso8601_string();
827 let end = range.1.to_iso8601_string();
828 let num_rows = f.num_rows();
829 format!(
830 "File{{id: {:?}, range: ({}, {}), size: {}, num rows: {} }}",
831 f.file_id(),
832 start,
833 end,
834 ReadableSize(f.size()),
835 num_rows
836 )
837 })
838 .collect();
839 let window_str = Timestamp::new_second(window).to_iso8601_string();
840 let active_window_str = active_window.map(|s| Timestamp::new_second(s).to_iso8601_string());
841 let max_output_file_size = max_output_file_size.map(|size| ReadableSize(size).to_string());
842 info!(
843 "Region ({:?}) compaction pick result: current window: {}, active window: {:?}, \
844 found runs: {}, file num: {}, max output file size: {:?}, filter deleted: {}, \
845 input files: {:?}",
846 region_id,
847 window_str,
848 active_window_str,
849 found_runs,
850 file_num,
851 max_output_file_size,
852 filter_deleted,
853 input_file_str
854 );
855}
856
857#[async_trait::async_trait]
858impl Picker for TwcsPicker {
859 async fn pick(&self, compaction_region: &CompactionRegion) -> Result<Option<PickerOutput>> {
860 self.pick_with_output_limit(compaction_region, self.max_background_tasks)
861 .await
862 }
863}
864
865impl TwcsPicker {
866 pub(crate) async fn pick_with_output_limit(
867 &self,
868 compaction_region: &CompactionRegion,
869 max_outputs: Option<usize>,
870 ) -> Result<Option<PickerOutput>> {
871 let region_id = compaction_region.region_id;
872 let picker = self.clone();
873 let compaction_region = compaction_region.clone();
874 let primary_key_mapper = compaction_region.current_version.ssts.primary_key_mapper();
875 let window_mapper = primary_key_mapper.clone();
876 let (expired_ssts, time_window_size, active_window, windows) =
877 common_runtime::spawn_blocking_compact(move || {
878 let levels = compaction_region.current_version.ssts.levels();
879 let expired_ssts = get_expired_ssts(
880 levels,
881 compaction_region.ttl,
882 Timestamp::current_millis(),
883 );
884 if !expired_ssts.is_empty() {
885 info!("Expired SSTs in region {}: {:?}", region_id, expired_ssts);
886 }
887 let expired_file_ids = expired_ssts
888 .iter()
889 .map(|file| file.file_id())
890 .collect::<HashSet<_>>();
891
892 let compaction_time_window = compaction_region
893 .current_version
894 .compaction_time_window
895 .map(|window| window.as_secs() as i64);
896 let time_window_size = compaction_time_window
897 .or(picker.time_window_seconds)
898 .unwrap_or_else(|| {
899 let inferred = infer_time_bucket(levels[0].files());
900 info!(
901 "Compaction window for region {} is not present, inferring from files: {:?}",
902 region_id, inferred
903 );
904 inferred
905 });
906
907 let windows = assign_to_windows(
908 levels
909 .iter()
910 .flat_map(LevelMeta::files)
911 .filter(|file| !expired_file_ids.contains(&file.file_id())),
912 time_window_size,
913 &window_mapper,
914 );
915 let active_window = find_active_window_by_sequence(
918 windows.values().flat_map(Window::files),
919 time_window_size,
920 )
921 .or_else(|| {
922 find_latest_window_in_seconds(
923 windows
924 .values()
925 .flat_map(Window::files)
926 .filter(|file| file.level() == 0),
927 time_window_size,
928 )
929 });
930
931 (expired_ssts, time_window_size, active_window, windows)
932 })
933 .await
934 .context(JoinSnafu)?;
935
936 let outputs = self
937 .build_output(
938 region_id,
939 windows,
940 WindowOutputContext {
941 active_window,
942 time_window_size: Some(time_window_size),
943 max_outputs,
944 primary_key_mapper,
945 },
946 )
947 .await?;
948
949 if outputs.is_empty() && expired_ssts.is_empty() {
950 return Ok(None);
951 }
952
953 let max_file_size = self
955 .max_output_file_size
956 .filter(|size| *size > 0)
957 .map(|size| size as usize);
958 Ok(Some(PickerOutput {
959 outputs,
960 expired_ssts,
961 time_window_size,
962 max_file_size,
963 }))
964 }
965}
966
967#[derive(Clone)]
968struct Window {
969 start: Timestamp,
970 end: Timestamp,
971 files: Vec<FileHandle>,
972 time_window: i64,
974 primary_key_range: Option<(bytes::Bytes, bytes::Bytes)>,
975}
976
977impl Window {
978 fn new_with_file(file: FileHandle, mapper: &PrimaryKeyRangeMapper) -> Self {
980 let (start, end) = file.time_range();
981 let primary_key_range = file.primary_key_range(mapper);
982 Self {
983 start,
984 end,
985 files: vec![file],
986 time_window: 0,
987 primary_key_range,
988 }
989 }
990
991 fn range(&self) -> (Timestamp, Timestamp) {
993 (self.start, self.end)
994 }
995
996 fn add_file(&mut self, file: FileHandle, mapper: &PrimaryKeyRangeMapper) {
998 let (start, end) = file.time_range();
999 self.start = self.start.min(start);
1000 self.end = self.end.max(end);
1001 self.primary_key_range = merge_primary_key_ranges(
1002 self.primary_key_range.take(),
1003 file.primary_key_range(mapper),
1004 );
1005 self.files.push(file);
1006 }
1007
1008 fn files(&self) -> impl Iterator<Item = &FileHandle> {
1009 self.files.iter()
1010 }
1011}
1012
1013fn assign_to_windows<'a>(
1015 files: impl Iterator<Item = &'a FileHandle>,
1016 time_window_size: i64,
1017 mapper: &PrimaryKeyRangeMapper,
1018) -> BTreeMap<i64, Window> {
1019 let mut windows: HashMap<i64, Window> = HashMap::new();
1020 for f in files {
1022 if f.compacting() {
1023 continue;
1024 }
1025 let (_, end) = f.time_range();
1026 let time_window = time_window_start(end, time_window_size);
1027
1028 match windows.entry(time_window) {
1029 Entry::Occupied(mut e) => {
1030 e.get_mut().add_file(f.clone(), mapper);
1031 }
1032 Entry::Vacant(e) => {
1033 let mut window = Window::new_with_file(f.clone(), mapper);
1034 window.time_window = time_window;
1035 e.insert(window);
1036 }
1037 }
1038 }
1039 windows.into_iter().collect()
1040}
1041
1042fn time_window_start(timestamp: Timestamp, time_window_size: i64) -> i64 {
1045 timestamp
1047 .convert_to(TimeUnit::Second)
1048 .and_then(|ts| ts.value().align_by_bucket(time_window_size))
1049 .unwrap_or(i64::MIN)
1050}
1051
1052fn time_window_intersects_range(
1053 window_start: i64,
1054 time_window_size: i64,
1055 time_range: &TimestampRange,
1056) -> bool {
1057 if time_range.is_empty() {
1058 return false;
1059 }
1060 let first_window = time_range
1061 .start()
1062 .map_or(i64::MIN, |start| time_window_start(start, time_window_size));
1063 let last_window = match time_range.end() {
1064 None => i64::MAX,
1065 Some(end) => {
1066 let Some(last_second) = end
1068 .convert_to_ceil(TimeUnit::Second)
1069 .and_then(|timestamp| timestamp.value().checked_sub(1))
1070 else {
1071 return false;
1072 };
1073 time_window_start(Timestamp::new_second(last_second), time_window_size)
1074 }
1075 };
1076 (first_window..=last_window).contains(&window_start)
1077}
1078
1079fn window_has_overlap(this: &Window, windows: &BTreeMap<i64, Window>) -> bool {
1080 windows
1081 .values()
1082 .filter(|that| this.time_window != that.time_window)
1083 .any(|that| {
1084 overlaps(&this.range(), &that.range()) && {
1085 match (&this.primary_key_range, &that.primary_key_range) {
1086 (Some(l), Some(r)) => primary_key_ranges_overlap(l, r),
1087 _ => true,
1088 }
1089 }
1090 })
1091}
1092
1093fn find_latest_window_in_seconds<'a>(
1096 files: impl Iterator<Item = &'a FileHandle>,
1097 time_window_size: i64,
1098) -> Option<i64> {
1099 let mut latest_timestamp = None;
1100 for f in files {
1101 let (_, end) = f.time_range();
1102 if let Some(latest) = latest_timestamp {
1103 if end > latest {
1104 latest_timestamp = Some(end);
1105 }
1106 } else {
1107 latest_timestamp = Some(end);
1108 }
1109 }
1110 latest_timestamp.map(|ts| time_window_start(ts, time_window_size))
1111}
1112
1113fn find_active_window_by_sequence<'a>(
1122 files: impl Iterator<Item = &'a FileHandle>,
1123 time_window_size: i64,
1124) -> Option<i64> {
1125 files
1126 .filter(|f| f.meta_ref().sequence.is_some())
1127 .max_by_key(|f| f.meta_ref().sequence)
1128 .map(|f| time_window_start(f.time_range().1, time_window_size))
1129}
1130
1131#[cfg(test)]
1132mod tests {
1133 use std::collections::HashSet;
1134 use std::num::NonZeroU64;
1135 use std::sync::Arc;
1136 use std::time::Duration;
1137
1138 use common_base::Plugins;
1139 use common_time::range::TimestampRange;
1140 use store_api::storage::FileId;
1141
1142 use super::*;
1143 use crate::cache::CacheManager;
1144 use crate::compaction::compactor::CompactionVersion;
1145 use crate::compaction::test_util::{
1146 compaction_region_with_ssts, new_file_handle, new_file_handle_with_sequence,
1147 new_file_handle_with_size_and_sequence,
1148 new_file_handle_with_size_sequence_and_primary_key_range, pk_range,
1149 primary_key_mapper_for_test,
1150 };
1151 use crate::config::MitoConfig;
1152 use crate::region::options::RegionOptions;
1153 use crate::sst::file::{FileMeta, Level};
1154 use crate::sst::version::SstVersion;
1155 use crate::test_util::memtable_util::metadata_for_test;
1156 use crate::test_util::scheduler_util::SchedulerEnv;
1157
1158 fn assign_to_windows<'a>(
1159 files: impl Iterator<Item = &'a FileHandle>,
1160 time_window_size: i64,
1161 ) -> BTreeMap<i64, Window> {
1162 super::assign_to_windows(files, time_window_size, &primary_key_mapper_for_test())
1163 }
1164
1165 fn pick_count_first(
1166 sorted_runs: Vec<SortedRun<FileHandle>>,
1167 max_output_file_size: Option<u64>,
1168 ) -> Vec<FileHandle> {
1169 super::pick_count_first(
1170 sorted_runs,
1171 max_output_file_size,
1172 &primary_key_mapper_for_test(),
1173 )
1174 }
1175
1176 fn pick_mixed_count_first(
1177 sorted_runs: Vec<SortedRun<FileHandle>>,
1178 max_output_file_size: Option<u64>,
1179 ) -> Vec<FileHandle> {
1180 super::pick_mixed_count_first(
1181 sorted_runs,
1182 max_output_file_size,
1183 &primary_key_mapper_for_test(),
1184 )
1185 }
1186
1187 impl TwcsPicker {
1188 async fn build_output_with_time_range(
1189 &self,
1190 region_id: RegionId,
1191 time_windows: BTreeMap<i64, Window>,
1192 active_window: Option<i64>,
1193 time_window_size: Option<i64>,
1194 ) -> Result<Vec<CompactionOutput>> {
1195 self.build_output(
1196 region_id,
1197 time_windows,
1198 WindowOutputContext {
1199 active_window,
1200 time_window_size,
1201 max_outputs: self.max_background_tasks,
1202 primary_key_mapper: Arc::new(primary_key_mapper_for_test()),
1203 },
1204 )
1205 .await
1206 }
1207 }
1208
1209 #[test]
1210 fn test_cross_schema_pk_overlap_before_window_aggregation() {
1211 use crate::test_util::sst_util::{new_primary_key, sst_region_metadata};
1212
1213 let metadata = Arc::new(sst_region_metadata());
1214 let mut old = FileMeta {
1215 region_id: metadata.region_id,
1216 file_id: FileId::random(),
1217 time_range: (
1218 Timestamp::new_millisecond(0),
1219 Timestamp::new_millisecond(1000),
1220 ),
1221 primary_key_min: Some(new_primary_key(&["a"]).into()),
1222 primary_key_max: Some(new_primary_key(&["b"]).into()),
1223 ..Default::default()
1224 };
1225 let mut completed = new_primary_key(&["b"]);
1226 completed.push(0); let new = FileMeta {
1228 file_id: FileId::random(),
1229 time_range: (
1230 Timestamp::new_millisecond(500),
1231 Timestamp::new_millisecond(2000),
1232 ),
1233 primary_key_min: Some(completed.clone().into()),
1234 primary_key_max: Some(completed.clone().into()),
1235 ..old.clone()
1236 };
1237 assert!(old.primary_key_max < new.primary_key_min);
1238 let mut ssts = SstVersion::new(metadata);
1239 let old_id = old.file_id;
1240 let new_id = new.file_id;
1241 ssts.add_files(
1242 crate::test_util::new_noop_file_purger(),
1243 [old.clone(), new].into_iter(),
1244 );
1245 let old_file = &ssts.levels()[0].files[&old_id];
1246 let new_file = &ssts.levels()[0].files[&new_id];
1247 let ranges = ssts.primary_key_mapper();
1248 let windows = super::assign_to_windows([old_file, new_file].into_iter(), 1, &ranges);
1249 assert_eq!(2, windows.len());
1250 assert!(
1251 windows
1252 .values()
1253 .all(|window| window_has_overlap(window, &windows))
1254 );
1255
1256 let mut window = Window::new_with_file(old_file.clone(), &ranges);
1257 window.add_file(new_file.clone(), &ranges);
1258 assert!(selected_overlaps_unselected(
1259 std::slice::from_ref(new_file),
1260 &window,
1261 &ranges,
1262 ));
1263 assert_eq!(
1264 Some(completed.into()),
1265 window
1266 .primary_key_range
1267 .as_ref()
1268 .map(|range| range.1.clone())
1269 );
1270
1271 old.file_id = FileId::random();
1273 old.primary_key_max = old.primary_key_min.clone();
1274 let disjoint_id = old.file_id;
1275 ssts.add_files(crate::test_util::new_noop_file_purger(), [old].into_iter());
1276 let files = &ssts.levels()[0].files;
1277 assert!(!files_overlap_inclusive(
1278 &files[&disjoint_id],
1279 &files[&new_id],
1280 &ranges
1281 ));
1282 }
1283
1284 #[test]
1285 fn test_valid_max_input_files_env_overrides_default() {
1286 assert_eq!(64, parse_max_input_files(Some("64")));
1287 }
1288
1289 #[test]
1290 fn test_invalid_max_input_files_env_falls_back_to_default() {
1291 for env_value in [None, Some(""), Some("invalid"), Some("0"), Some("1")] {
1292 assert_eq!(16, parse_max_input_files(env_value));
1293 }
1294 }
1295
1296 async fn compaction_region_with_expired_sst() -> CompactionRegion {
1297 compaction_region_with_ssts(
1298 (1..=4).map(|sequence| FileMeta {
1299 file_id: FileId::random(),
1300 time_range: (
1301 Timestamp::new_millisecond(0),
1302 Timestamp::new_millisecond(10),
1303 ),
1304 level: 0,
1305 sequence: NonZeroU64::new(sequence),
1306 ..Default::default()
1307 }),
1308 Duration::from_millis(1),
1309 )
1310 .await
1311 }
1312
1313 #[tokio::test]
1314 async fn test_pick_normalizes_zero_output_size_to_unlimited() {
1315 let mut compaction_region = compaction_region_with_ssts(
1316 (1..=4).map(|sequence| new_file(0, 10, sequence, 100).meta_ref().clone()),
1317 Duration::from_secs(3600),
1318 )
1319 .await;
1320 compaction_region.ttl = None;
1321
1322 for (max_output_file_size, expected_max_file_size) in
1323 [(Some(0), None), (None, None), (Some(1024), Some(1024))]
1324 {
1325 let picker = TwcsPicker {
1326 trigger_file_num: 4,
1327 active_window_l1_merge_trigger: 8,
1328 inactive_window_trigger_file_num: 4,
1329 inactive_window_l1_merge_trigger: 8,
1330 time_window_seconds: Some(3),
1331 max_output_file_size,
1332 append_mode: false,
1333 max_background_tasks: None,
1334 time_range: None,
1335 };
1336
1337 let output = picker.pick(&compaction_region).await.unwrap().unwrap();
1338
1339 assert_eq!(output.outputs.len(), 1);
1340 assert_eq!(output.outputs[0].inputs.len(), 4);
1341 assert!(output.expired_ssts.is_empty());
1342 assert_eq!(output.max_file_size, expected_max_file_size);
1343 }
1344 }
1345
1346 #[tokio::test]
1347 async fn test_pick_expired_ssts_without_marking_compacting() {
1348 let picker = TwcsPicker {
1349 trigger_file_num: 4,
1350 active_window_l1_merge_trigger: 8,
1351 inactive_window_trigger_file_num: 4,
1352 inactive_window_l1_merge_trigger: 8,
1353 time_window_seconds: Some(3),
1354 max_output_file_size: None,
1355 append_mode: false,
1356 max_background_tasks: None,
1357 time_range: None,
1358 };
1359 let compaction_region = compaction_region_with_expired_sst().await;
1360
1361 let output = picker.pick(&compaction_region).await.unwrap().unwrap();
1362
1363 assert!(output.outputs.is_empty());
1364 assert!(!output.expired_ssts.is_empty());
1365 assert!(output.expired_ssts.iter().all(|file| !file.compacting()));
1366 }
1367
1368 #[tokio::test]
1369 async fn test_expired_sst_does_not_determine_active_window() {
1370 let now = Timestamp::current_millis().value();
1371 let files = [
1372 FileMeta {
1373 file_id: FileId::random(),
1374 time_range: (
1375 Timestamp::new_millisecond(0),
1376 Timestamp::new_millisecond(10),
1377 ),
1378 level: 0,
1379 sequence: NonZeroU64::new(100),
1380 ..Default::default()
1381 },
1382 FileMeta {
1383 file_id: FileId::random(),
1384 time_range: (
1385 Timestamp::new_millisecond(now - 1000),
1386 Timestamp::new_millisecond(now),
1387 ),
1388 level: 0,
1389 sequence: NonZeroU64::new(1),
1390 ..Default::default()
1391 },
1392 FileMeta {
1393 file_id: FileId::random(),
1394 time_range: (
1395 Timestamp::new_millisecond(now - 1000),
1396 Timestamp::new_millisecond(now),
1397 ),
1398 level: 0,
1399 sequence: NonZeroU64::new(2),
1400 ..Default::default()
1401 },
1402 ];
1403 let compaction_region = compaction_region_with_ssts(files, Duration::from_secs(60)).await;
1404 let picker = TwcsPicker {
1405 trigger_file_num: 4,
1406 active_window_l1_merge_trigger: 8,
1407 inactive_window_trigger_file_num: 2,
1408 inactive_window_l1_merge_trigger: 8,
1409 time_window_seconds: Some(3),
1410 max_output_file_size: None,
1411 append_mode: false,
1412 max_background_tasks: None,
1413 time_range: None,
1414 };
1415
1416 let output = picker.pick(&compaction_region).await.unwrap().unwrap();
1417
1418 assert_eq!(1, output.expired_ssts.len());
1419 assert!(output.outputs.is_empty());
1420 }
1421
1422 #[tokio::test]
1423 async fn test_active_window_survives_l0_consumption() {
1424 let env = SchedulerEnv::new().await;
1429 let metadata = metadata_for_test();
1430 let manifest_ctx = env.mock_manifest_context(metadata.clone()).await;
1431 let mut ssts = SstVersion::new(metadata.clone());
1432 ssts.add_files(
1433 Arc::new(crate::sst::file_purger::NoopFilePurger),
1434 [100, 101].into_iter().map(|sequence| FileMeta {
1435 region_id: metadata.region_id,
1436 file_id: FileId::random(),
1437 time_range: (
1438 Timestamp::new_millisecond(0),
1439 Timestamp::new_millisecond(10),
1440 ),
1441 level: 1,
1442 sequence: NonZeroU64::new(sequence),
1443 ..Default::default()
1444 }),
1445 );
1446 let compaction_region = CompactionRegion {
1447 region_id: metadata.region_id,
1448 region_options: RegionOptions::default(),
1449 engine_config: Arc::new(MitoConfig::default()),
1450 region_metadata: metadata.clone(),
1451 cache_manager: Arc::new(CacheManager::default()),
1452 access_layer: env.access_layer,
1453 manifest_ctx,
1454 current_version: CompactionVersion {
1455 metadata,
1456 options: RegionOptions::default(),
1457 ssts: Arc::new(ssts),
1458 memtable_min_sequence: None,
1459 compaction_time_window: None,
1460 },
1461 file_purger: None,
1462 ttl: None,
1463 max_parallelism: 1,
1464 plugins: Plugins::new(),
1465 };
1466 let picker = TwcsPicker {
1467 trigger_file_num: 4,
1468 active_window_l1_merge_trigger: 8,
1469 inactive_window_trigger_file_num: 2,
1470 inactive_window_l1_merge_trigger: 8,
1471 time_window_seconds: Some(3600),
1472 max_output_file_size: None,
1473 append_mode: false,
1474 max_background_tasks: None,
1475 time_range: None,
1476 };
1477
1478 assert!(picker.pick(&compaction_region).await.unwrap().is_none());
1479 }
1480
1481 #[test]
1482 fn test_find_active_window_by_sequence() {
1483 let files = [
1488 new_file_handle_with_sequence(FileId::random(), 0, 999, 0, 1),
1489 new_file_handle_with_sequence(FileId::random(), 2000, 2999, 1, 10),
1490 ];
1491 assert_eq!(Some(2), find_active_window_by_sequence(files.iter(), 1));
1492
1493 let files = [
1496 new_file_handle_with_sequence(FileId::random(), 0, 999, 0, 10),
1497 new_file_handle_with_sequence(FileId::random(), 2000, 2999, 0, 1),
1498 ];
1499 assert_eq!(Some(0), find_active_window_by_sequence(files.iter(), 1));
1500
1501 let files = [new_file_handle_with_sequence(
1504 FileId::random(),
1505 0,
1506 999,
1507 1,
1508 0,
1509 )];
1510 assert_eq!(None, find_active_window_by_sequence(files.iter(), 1));
1511 assert!(find_active_window_by_sequence(Vec::<FileHandle>::new().iter(), 1).is_none());
1512 }
1513
1514 #[test]
1515 fn test_active_window_falls_back_to_l0_rule_without_sequence() {
1516 let active_window_of = |files: &[FileHandle]| {
1517 find_active_window_by_sequence(files.iter(), 1).or_else(|| {
1518 find_latest_window_in_seconds(files.iter().filter(|f| f.level() == 0), 1)
1519 })
1520 };
1521
1522 let files = [
1524 new_file_handle_with_sequence(FileId::random(), 0, 999, 1, 0),
1525 new_file_handle_with_sequence(FileId::random(), 2000, 2999, 0, 0),
1526 ];
1527 assert_eq!(Some(2), active_window_of(&files));
1528
1529 let files = [new_file_handle_with_sequence(
1531 FileId::random(),
1532 0,
1533 999,
1534 1,
1535 0,
1536 )];
1537 assert_eq!(None, active_window_of(&files));
1538 }
1539
1540 #[test]
1541 fn test_get_latest_window_in_seconds() {
1542 assert_eq!(
1543 Some(0),
1544 find_latest_window_in_seconds([new_file_handle(FileId::random(), 0, 999, 0)].iter(), 1)
1545 );
1546 assert_eq!(
1547 Some(1),
1548 find_latest_window_in_seconds(
1549 [new_file_handle(FileId::random(), 0, 1000, 0)].iter(),
1550 1
1551 )
1552 );
1553
1554 assert_eq!(
1555 Some(-9223372036857600),
1556 find_latest_window_in_seconds(
1557 [new_file_handle(FileId::random(), i64::MIN, i64::MIN + 1, 0)].iter(),
1558 3600,
1559 )
1560 );
1561
1562 assert_eq!(
1563 (i64::MAX / 10000000) * 10000,
1564 find_latest_window_in_seconds(
1565 [new_file_handle(FileId::random(), i64::MIN, i64::MAX, 0)].iter(),
1566 10000,
1567 )
1568 .unwrap()
1569 );
1570
1571 assert_eq!(
1572 Some((i64::MAX / 3600000) * 3600),
1573 find_latest_window_in_seconds(
1574 [
1575 new_file_handle(FileId::random(), i64::MIN, i64::MAX, 0),
1576 new_file_handle(FileId::random(), 0, 1000, 0)
1577 ]
1578 .iter(),
1579 3600
1580 )
1581 );
1582 }
1583
1584 #[test]
1585 fn test_window_assignment_and_activity_at_boundaries() {
1586 for (unit, ticks_per_second) in [
1589 (TimeUnit::Second, 1),
1590 (TimeUnit::Millisecond, 1_000),
1591 (TimeUnit::Microsecond, 1_000_000),
1592 (TimeUnit::Nanosecond, 1_000_000_000),
1593 ] {
1594 for window_size in [1, 3, 86_400] {
1595 for boundary in [-window_size, 0, window_size] {
1596 for (offset, expected_window) in [
1597 (-1, boundary - window_size),
1598 (0, boundary),
1599 (ticks_per_second - 1, boundary),
1600 (ticks_per_second, boundary + i64::from(window_size == 1)),
1601 ] {
1602 let timestamp = Timestamp::new(boundary * ticks_per_second + offset, unit);
1603 let file = FileHandle::new(
1604 FileMeta {
1605 time_range: (timestamp, timestamp),
1606 sequence: NonZeroU64::new(1),
1607 ..Default::default()
1608 },
1609 crate::test_util::new_noop_file_purger(),
1610 );
1611 let files = [file];
1612 let windows = assign_to_windows(files.iter(), window_size);
1613 assert_eq!(
1614 vec![expected_window],
1615 windows.keys().copied().collect::<Vec<_>>(),
1616 "{timestamp:?}, window size {window_size}"
1617 );
1618 assert_eq!(
1619 Some(expected_window),
1620 find_active_window_by_sequence(files.iter(), window_size)
1621 );
1622 assert_eq!(
1623 Some(expected_window),
1624 find_latest_window_in_seconds(files.iter(), window_size)
1625 );
1626 }
1627 }
1628 }
1629 }
1630 }
1631
1632 #[test]
1633 fn test_window_assignment_at_timestamp_limits() {
1634 for (seconds, expected_window) in [(i64::MIN, i64::MIN), (i64::MAX, i64::MAX - 1)] {
1635 let timestamp = Timestamp::new_second(seconds);
1636 let files = [FileHandle::new(
1637 FileMeta {
1638 time_range: (timestamp, timestamp),
1639 sequence: NonZeroU64::new(1),
1640 ..Default::default()
1641 },
1642 crate::test_util::new_noop_file_purger(),
1643 )];
1644 let windows = assign_to_windows(files.iter(), 3);
1645 assert_eq!(
1646 vec![expected_window],
1647 windows.keys().copied().collect::<Vec<_>>()
1648 );
1649 assert_eq!(
1650 Some(expected_window),
1651 find_active_window_by_sequence(files.iter(), 3)
1652 );
1653 assert_eq!(
1654 Some(expected_window),
1655 find_latest_window_in_seconds(files.iter(), 3)
1656 );
1657 assert!(time_window_intersects_range(
1658 expected_window,
1659 3,
1660 &TimestampRange::single(timestamp)
1661 ));
1662 }
1663 }
1664
1665 #[tokio::test]
1666 async fn test_midnight_file_is_not_compacted_with_previous_day() {
1667 let midnight = 86_400_000;
1669 CompactionPickerTestCase {
1670 window_size: 86_400,
1671 input_files: [
1672 (midnight - 240_000, midnight - 180_000),
1673 (midnight - 180_000, midnight - 90_000),
1674 (midnight - 90_000, midnight - 30_000),
1675 (midnight, midnight),
1676 ]
1677 .into_iter()
1678 .enumerate()
1679 .map(|(index, (start, end))| {
1680 new_file_handle_with_sequence(FileId::random(), start, end, 0, index as u64 + 1)
1681 })
1682 .collect(),
1683 expected_outputs: vec![ExpectedOutput {
1684 input_files: vec![0, 1, 2],
1685 output_level: 1,
1686 }],
1687 }
1688 .check()
1689 .await;
1690 }
1691
1692 #[test]
1693 fn test_assign_to_windows() {
1694 let windows = assign_to_windows(
1695 [
1696 new_file_handle(FileId::random(), 0, 999, 0),
1697 new_file_handle(FileId::random(), 0, 999, 0),
1698 new_file_handle(FileId::random(), 0, 999, 0),
1699 new_file_handle(FileId::random(), 0, 999, 0),
1700 new_file_handle(FileId::random(), 0, 999, 0),
1701 ]
1702 .iter(),
1703 3,
1704 );
1705 let fgs = &windows.get(&0).unwrap().files;
1706 assert_eq!(5, fgs.len());
1707
1708 let files = [FileId::random(); 3];
1709 let windows = assign_to_windows(
1710 [
1711 new_file_handle(files[0], -2000, -3, 0),
1712 new_file_handle(files[1], 0, 2999, 0),
1713 new_file_handle(files[2], 50, 10001, 0),
1714 ]
1715 .iter(),
1716 3,
1717 );
1718 assert_eq!(
1719 files[0],
1720 windows
1721 .get(&-3)
1722 .unwrap()
1723 .files()
1724 .next()
1725 .unwrap()
1726 .file_id()
1727 .file_id()
1728 );
1729 assert_eq!(
1730 files[1],
1731 windows
1732 .get(&0)
1733 .unwrap()
1734 .files()
1735 .next()
1736 .unwrap()
1737 .file_id()
1738 .file_id()
1739 );
1740 assert_eq!(
1741 files[2],
1742 windows
1743 .get(&9)
1744 .unwrap()
1745 .files()
1746 .next()
1747 .unwrap()
1748 .file_id()
1749 .file_id()
1750 );
1751 }
1752
1753 #[test]
1754 fn test_assign_files_to_windows() {
1755 let files = [
1756 FileId::random(),
1757 FileId::random(),
1758 FileId::random(),
1759 FileId::random(),
1760 ];
1761 let windows = assign_to_windows(
1762 [
1763 new_file_handle_with_sequence(files[0], 0, 999, 0, 1),
1764 new_file_handle_with_sequence(files[1], 0, 999, 0, 1),
1765 new_file_handle_with_sequence(files[2], 0, 999, 0, 2),
1766 new_file_handle_with_sequence(files[3], 0, 999, 0, 2),
1767 ]
1768 .iter(),
1769 3,
1770 );
1771 assert_eq!(windows.len(), 1);
1772 let window_files = &windows.get(&0).unwrap().files;
1773 assert_eq!(4, window_files.len());
1774 assert_eq!(
1775 window_files
1776 .iter()
1777 .map(|f| f.file_id().file_id())
1778 .collect::<HashSet<_>>(),
1779 files.into_iter().collect()
1780 );
1781 }
1782
1783 #[test]
1784 fn test_assign_compacting_to_windows() {
1785 let files = [
1786 new_file_handle(FileId::random(), 0, 999, 0),
1787 new_file_handle(FileId::random(), 0, 999, 0),
1788 new_file_handle(FileId::random(), 0, 999, 0),
1789 new_file_handle(FileId::random(), 0, 999, 0),
1790 new_file_handle(FileId::random(), 0, 999, 0),
1791 ];
1792 files[0].set_compacting(true);
1793 files[2].set_compacting(true);
1794 let mut windows = assign_to_windows(files.iter(), 3);
1795 let window0 = windows.remove(&0).unwrap();
1796 assert_eq!(3, window0.files.len());
1797 let candidates = window0
1798 .files
1799 .iter()
1800 .map(|f| f.file_id().file_id())
1801 .collect::<HashSet<_>>();
1802 assert_eq!(candidates.len(), 3);
1803 assert_eq!(
1804 candidates,
1805 [
1806 files[1].file_id().file_id(),
1807 files[3].file_id().file_id(),
1808 files[4].file_id().file_id()
1809 ]
1810 .into_iter()
1811 .collect::<HashSet<_>>()
1812 );
1813 }
1814
1815 type ExpectedWindowSpec = (i64, bool, Vec<(i64, i64)>);
1817
1818 fn check_assign_to_windows_with_overlapping(
1819 file_time_ranges: &[(i64, i64)],
1820 time_window: i64,
1821 expected_files: &[ExpectedWindowSpec],
1822 ) {
1823 let files: Vec<_> = (0..file_time_ranges.len())
1824 .map(|_| FileId::random())
1825 .collect();
1826
1827 let file_handles = files
1828 .iter()
1829 .zip(file_time_ranges.iter())
1830 .map(|(file_id, range)| new_file_handle(*file_id, range.0, range.1, 0))
1831 .collect::<Vec<_>>();
1832
1833 let windows = assign_to_windows(file_handles.iter(), time_window);
1834 assert_eq!(expected_files.len(), windows.len());
1835
1836 for (expected_window, overlapping, window_files) in expected_files {
1837 let actual_window = windows.get(expected_window).unwrap();
1838 let actual_overlapping = window_has_overlap(actual_window, &windows);
1839 assert_eq!(*overlapping, actual_overlapping);
1840 let mut file_ranges = actual_window
1841 .files
1842 .iter()
1843 .map(|f| {
1844 let (s, e) = f.time_range();
1845 (s.value(), e.value())
1846 })
1847 .collect::<Vec<_>>();
1848 file_ranges.sort_unstable_by(|l, r| l.0.cmp(&r.0).then(l.1.cmp(&r.1)));
1849 assert_eq!(window_files, &file_ranges);
1850 }
1851 }
1852
1853 #[test]
1854 fn test_assign_to_windows_with_overlapping() {
1855 check_assign_to_windows_with_overlapping(
1856 &[(0, 999), (1000, 1999), (2000, 2999)],
1857 2,
1858 &[
1859 (0, false, vec![(0, 999), (1000, 1999)]),
1860 (2, false, vec![(2000, 2999)]),
1861 ],
1862 );
1863
1864 check_assign_to_windows_with_overlapping(
1865 &[(0, 1), (0, 999), (100, 2999)],
1866 2,
1867 &[
1868 (0, true, vec![(0, 1), (0, 999)]),
1869 (2, true, vec![(100, 2999)]),
1870 ],
1871 );
1872
1873 check_assign_to_windows_with_overlapping(
1874 &[(0, 999), (1000, 1999), (2000, 2999), (3000, 3999)],
1875 2,
1876 &[
1877 (0, false, vec![(0, 999), (1000, 1999)]),
1878 (2, false, vec![(2000, 2999), (3000, 3999)]),
1879 ],
1880 );
1881
1882 check_assign_to_windows_with_overlapping(
1883 &[
1884 (0, 999),
1885 (1000, 1999),
1886 (2000, 2999),
1887 (3000, 3999),
1888 (0, 3999),
1889 ],
1890 2,
1891 &[
1892 (0, true, vec![(0, 999), (1000, 1999)]),
1893 (2, true, vec![(0, 3999), (2000, 2999), (3000, 3999)]),
1894 ],
1895 );
1896
1897 check_assign_to_windows_with_overlapping(
1898 &[
1899 (0, 999),
1900 (1000, 1999),
1901 (2000, 2999),
1902 (3000, 3999),
1903 (1999, 3999),
1904 ],
1905 2,
1906 &[
1907 (0, true, vec![(0, 999), (1000, 1999)]),
1908 (2, true, vec![(1999, 3999), (2000, 2999), (3000, 3999)]),
1909 ],
1910 );
1911
1912 check_assign_to_windows_with_overlapping(
1913 &[
1914 (0, 999), (1000, 1999), (2000, 2999), (3000, 3999), (2999, 3999), ],
1920 2,
1921 &[
1922 (0, false, vec![(0, 999), (1000, 1999)]),
1924 (2, false, vec![(2000, 2999), (2999, 3999), (3000, 3999)]),
1925 ],
1926 );
1927
1928 check_assign_to_windows_with_overlapping(
1929 &[
1930 (0, 999), (1000, 1999), (2000, 2999), (3000, 3999), (0, 2000), ],
1936 2,
1937 &[
1938 (0, true, vec![(0, 999), (1000, 1999)]),
1939 (2, true, vec![(0, 2000), (2000, 2999), (3000, 3999)]),
1940 ],
1941 );
1942 }
1943
1944 #[test]
1945 fn test_assign_to_windows_not_overlapping_when_pk_disjoint() {
1946 let files = [
1947 new_file_handle_with_size_sequence_and_primary_key_range(
1948 FileId::random(),
1949 0,
1950 1000,
1951 0,
1952 1,
1953 10,
1954 pk_range(b"a", b"f"),
1955 ),
1956 new_file_handle_with_size_sequence_and_primary_key_range(
1957 FileId::random(),
1958 500,
1959 2999,
1960 0,
1961 2,
1962 10,
1963 pk_range(b"x", b"z"),
1964 ),
1965 ];
1966
1967 let windows = assign_to_windows(files.iter(), 2);
1968
1969 assert_eq!(2, windows.len());
1970 let overlapping = window_has_overlap(windows.get(&0).unwrap(), &windows);
1971 assert!(!overlapping);
1972 }
1973
1974 #[test]
1975 fn test_assign_to_windows_pk_unknown_in_earlier_window_does_not_poison_later_windows() {
1976 let files = [
1977 new_file_handle(FileId::random(), 0, 1999, 0),
1978 new_file_handle_with_size_sequence_and_primary_key_range(
1979 FileId::random(),
1980 2000,
1981 3999,
1982 0,
1983 1,
1984 10,
1985 pk_range(b"a", b"f"),
1986 ),
1987 new_file_handle_with_size_sequence_and_primary_key_range(
1988 FileId::random(),
1989 3000,
1990 4999,
1991 0,
1992 2,
1993 10,
1994 pk_range(b"x", b"z"),
1995 ),
1996 ];
1997
1998 let windows = assign_to_windows(files.iter(), 2);
1999
2000 let overlapping = window_has_overlap(windows.get(&4).unwrap(), &windows);
2001 assert!(!overlapping);
2002 }
2003
2004 struct CompactionPickerTestCase {
2005 window_size: i64,
2006 input_files: Vec<FileHandle>,
2007 expected_outputs: Vec<ExpectedOutput>,
2008 }
2009
2010 impl CompactionPickerTestCase {
2011 async fn check(&self) {
2012 let file_id_to_idx = self
2013 .input_files
2014 .iter()
2015 .enumerate()
2016 .map(|(idx, file)| (file.file_id(), idx))
2017 .collect::<HashMap<_, _>>();
2018 let windows = assign_to_windows(self.input_files.iter(), self.window_size);
2019 let active_window =
2020 find_active_window_by_sequence(self.input_files.iter(), self.window_size).or_else(
2021 || find_latest_window_in_seconds(self.input_files.iter(), self.window_size),
2022 );
2023 let output = TwcsPicker {
2024 trigger_file_num: 2,
2025 active_window_l1_merge_trigger: 8,
2026 inactive_window_trigger_file_num: 2,
2027 inactive_window_l1_merge_trigger: 2,
2028 time_window_seconds: None,
2029 max_output_file_size: None,
2030 append_mode: false,
2031 max_background_tasks: None,
2032 time_range: None,
2033 }
2034 .build_output_with_time_range(RegionId::from_u64(0), windows, active_window, None)
2035 .await
2036 .unwrap();
2037
2038 let output = output
2039 .iter()
2040 .map(|o| {
2041 let input_file_ids = o
2042 .inputs
2043 .iter()
2044 .map(|f| file_id_to_idx.get(&f.file_id()).copied().unwrap())
2045 .collect::<HashSet<_>>();
2046 (input_file_ids, o.output_level)
2047 })
2048 .collect::<Vec<_>>();
2049
2050 let expected = self
2051 .expected_outputs
2052 .iter()
2053 .map(|o| {
2054 let input_file_ids = o.input_files.iter().copied().collect::<HashSet<_>>();
2055 (input_file_ids, o.output_level)
2056 })
2057 .collect::<Vec<_>>();
2058 assert_eq!(expected, output);
2059 }
2060 }
2061
2062 struct ExpectedOutput {
2063 input_files: Vec<usize>,
2064 output_level: Level,
2065 }
2066
2067 #[tokio::test]
2068 async fn test_newer_windows_are_placed_last_for_execution() {
2069 let file_ids = (0..4).map(|_| FileId::random()).collect::<Vec<_>>();
2070
2071 CompactionPickerTestCase {
2073 window_size: 3,
2074 input_files: [
2075 new_file_handle_with_sequence(file_ids[0], -2000, -3, 0, 1),
2076 new_file_handle_with_sequence(file_ids[1], -3000, -100, 0, 2),
2077 new_file_handle_with_sequence(file_ids[2], 0, 2999, 0, 3), new_file_handle_with_sequence(file_ids[3], 50, 2998, 0, 4), ]
2080 .to_vec(),
2081 expected_outputs: vec![
2082 ExpectedOutput {
2083 input_files: vec![0, 1],
2084 output_level: 1,
2085 },
2086 ExpectedOutput {
2087 input_files: vec![2, 3],
2088 output_level: 1,
2089 },
2090 ],
2091 }
2092 .check()
2093 .await;
2094
2095 let file_ids = (0..6).map(|_| FileId::random()).collect::<Vec<_>>();
2102 CompactionPickerTestCase {
2103 window_size: 3,
2104 input_files: [
2105 new_file_handle_with_sequence(file_ids[0], -2000, -3, 0, 1),
2106 new_file_handle_with_sequence(file_ids[1], -3000, -100, 0, 2),
2107 new_file_handle_with_sequence(file_ids[2], 0, 2999, 0, 3),
2108 new_file_handle_with_sequence(file_ids[3], 50, 2998, 0, 4),
2109 new_file_handle_with_sequence(file_ids[4], 11, 2990, 0, 5),
2110 ]
2111 .to_vec(),
2112 expected_outputs: vec![
2113 ExpectedOutput {
2114 input_files: vec![0, 1],
2115 output_level: 1,
2116 },
2117 ExpectedOutput {
2118 input_files: vec![2, 3, 4],
2119 output_level: 1,
2120 },
2121 ],
2122 }
2123 .check()
2124 .await;
2125
2126 let file_ids = (0..6).map(|_| FileId::random()).collect::<Vec<_>>();
2131 CompactionPickerTestCase {
2132 window_size: 3,
2133 input_files: [
2134 new_file_handle_with_sequence(file_ids[0], 0, 2999, 1, 1),
2135 new_file_handle_with_sequence(file_ids[1], 0, 2998, 1, 1),
2136 new_file_handle_with_sequence(file_ids[2], 3000, 5999, 1, 2),
2137 new_file_handle_with_sequence(file_ids[3], 3000, 5000, 1, 2),
2138 new_file_handle_with_sequence(file_ids[4], 11, 2990, 0, 3),
2139 ]
2140 .to_vec(),
2141 expected_outputs: vec![ExpectedOutput {
2142 input_files: vec![2, 3],
2143 output_level: 1,
2144 }],
2145 }
2146 .check()
2147 .await;
2148 }
2149
2150 #[tokio::test]
2151 async fn test_build_output_skips_pk_disjoint_files() {
2152 let files = [
2153 new_file_handle_with_size_sequence_and_primary_key_range(
2154 FileId::random(),
2155 0,
2156 2999,
2157 0,
2158 1,
2159 10,
2160 pk_range(b"a", b"f"),
2161 ),
2162 new_file_handle_with_size_sequence_and_primary_key_range(
2163 FileId::random(),
2164 50,
2165 2998,
2166 0,
2167 2,
2168 10,
2169 pk_range(b"x", b"z"),
2170 ),
2171 ];
2172 let windows = assign_to_windows(files.iter(), 3);
2173 let active_window = find_latest_window_in_seconds(files.iter(), 3);
2174 let output = TwcsPicker {
2175 trigger_file_num: 4,
2176 active_window_l1_merge_trigger: 8,
2177 inactive_window_trigger_file_num: 4,
2178 inactive_window_l1_merge_trigger: 8,
2179 time_window_seconds: None,
2180 max_output_file_size: None,
2181 append_mode: false,
2182 max_background_tasks: None,
2183 time_range: None,
2184 }
2185 .build_output_with_time_range(RegionId::from_u64(0), windows, active_window, None)
2186 .await
2187 .unwrap();
2188
2189 assert!(output.is_empty());
2190 }
2191
2192 #[test]
2193 fn test_append_mode_filter_large_files() {
2194 let file_ids = (0..4).map(|_| FileId::random()).collect::<Vec<_>>();
2195 let max_output_file_size = 1000u64;
2196
2197 let small_file_1 = new_file_handle_with_size_and_sequence(file_ids[0], 0, 999, 0, 1, 500);
2199 let large_file_1 = new_file_handle_with_size_and_sequence(file_ids[1], 0, 999, 0, 2, 1500);
2200 let small_file_2 = new_file_handle_with_size_and_sequence(file_ids[2], 0, 999, 0, 3, 800);
2201 let large_file_2 = new_file_handle_with_size_and_sequence(file_ids[3], 0, 999, 0, 4, 2000);
2202
2203 let mut files_to_merge = vec![small_file_1, large_file_1, small_file_2, large_file_2];
2204
2205 let original_count = files_to_merge.len();
2207
2208 files_to_merge.retain(|file| file.size() <= max_output_file_size);
2210
2211 assert_eq!(files_to_merge.len(), 2);
2213 assert_eq!(original_count, 4);
2214
2215 for file in &files_to_merge {
2217 assert!(
2218 file.size() <= max_output_file_size,
2219 "File size {} should be <= {}",
2220 file.size(),
2221 max_output_file_size
2222 );
2223 }
2224 }
2225
2226 #[tokio::test]
2227 async fn test_build_output_multiple_windows_with_zero_runs() {
2228 let file_ids = (0..7).map(|_| FileId::random()).collect::<Vec<_>>();
2229
2230 let files = [
2231 new_file_handle_with_sequence(file_ids[0], 0, 999, 0, 1),
2233 new_file_handle_with_sequence(file_ids[1], 0, 999, 0, 2),
2234 new_file_handle_with_sequence(file_ids[2], 0, 999, 0, 3),
2235 new_file_handle_with_sequence(file_ids[3], 3000, 3999, 0, 4),
2237 new_file_handle_with_sequence(file_ids[4], 3000, 3999, 0, 5),
2238 new_file_handle_with_sequence(file_ids[5], 3000, 3999, 0, 6),
2239 new_file_handle_with_sequence(file_ids[6], 3000, 3999, 0, 7),
2240 ];
2241
2242 let windows = assign_to_windows(files.iter(), 3);
2243
2244 let picker = TwcsPicker {
2246 trigger_file_num: 4, active_window_l1_merge_trigger: 8,
2248 inactive_window_trigger_file_num: 4,
2249 inactive_window_l1_merge_trigger: 8,
2250 time_window_seconds: Some(3),
2251 max_output_file_size: None,
2252 append_mode: false,
2253 max_background_tasks: None,
2254 time_range: None,
2255 };
2256
2257 let active_window = find_latest_window_in_seconds(files.iter(), 3);
2258 let output = picker
2259 .build_output_with_time_range(RegionId::from_u64(123), windows, active_window, None)
2260 .await
2261 .unwrap();
2262
2263 assert!(
2264 !output.is_empty(),
2265 "Should have output from windows with runs, even when one window has 0 runs"
2266 );
2267
2268 let all_output_files: Vec<_> = output
2269 .iter()
2270 .flat_map(|o| o.inputs.iter())
2271 .map(|f| f.file_id().file_id())
2272 .collect();
2273
2274 assert!(
2275 all_output_files.contains(&file_ids[3])
2276 || all_output_files.contains(&file_ids[4])
2277 || all_output_files.contains(&file_ids[5]),
2278 "Output should contain files from the window with runs"
2279 );
2280 }
2281
2282 #[tokio::test]
2283 async fn test_build_output_single_window_zero_runs() {
2284 let file_ids = (0..2).map(|_| FileId::random()).collect::<Vec<_>>();
2285
2286 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];
2290
2291 let windows = assign_to_windows(files.iter(), 3);
2292
2293 let picker = TwcsPicker {
2294 trigger_file_num: 2,
2295 active_window_l1_merge_trigger: 8,
2296 inactive_window_trigger_file_num: 2,
2297 inactive_window_l1_merge_trigger: 8,
2298 time_window_seconds: Some(3),
2299 max_output_file_size: Some(1000),
2300 append_mode: true,
2301 max_background_tasks: None,
2302 time_range: None,
2303 };
2304
2305 let active_window = find_latest_window_in_seconds(files.iter(), 3);
2306 let output = picker
2307 .build_output_with_time_range(RegionId::from_u64(456), windows, active_window, None)
2308 .await
2309 .unwrap();
2310
2311 assert!(
2313 output.is_empty(),
2314 "Should return empty output when no runs are found after filtering"
2315 );
2316 }
2317
2318 #[tokio::test]
2319 async fn test_append_mode_can_pick_remaining_single_level_files() {
2320 let files = [
2321 new_file_handle_with_size_and_sequence(FileId::random(), 0, 9, 0, 1, 100),
2322 new_file_handle_with_size_and_sequence(FileId::random(), 20, 29, 0, 2, 100),
2323 new_file_handle_with_size_and_sequence(FileId::random(), 40, 49, 0, 3, 100),
2324 new_file_handle_with_size_and_sequence(FileId::random(), 60, 69, 0, 4, 2_000),
2325 ];
2326 let windows = assign_to_windows(files.iter(), 1);
2327 let picker = TwcsPicker {
2328 trigger_file_num: 4,
2329 active_window_l1_merge_trigger: 8,
2330 inactive_window_trigger_file_num: 4,
2331 inactive_window_l1_merge_trigger: 8,
2332 time_window_seconds: Some(1),
2333 max_output_file_size: Some(1_000),
2334 append_mode: true,
2335 max_background_tasks: None,
2336 time_range: None,
2337 };
2338
2339 let output = picker
2340 .build_output_with_time_range(RegionId::from_u64(1), windows, Some(1), None)
2341 .await
2342 .unwrap();
2343
2344 assert_eq!(1, output.len());
2345 assert_eq!(3, output[0].inputs.len());
2346 }
2347
2348 #[tokio::test]
2349 async fn test_max_background_tasks_truncation() {
2350 let file_ids = (0..10).map(|_| FileId::random()).collect::<Vec<_>>();
2351 let max_background_tasks = 3;
2352
2353 let files = [
2355 new_file_handle_with_sequence(file_ids[0], 0, 999, 0, 1),
2357 new_file_handle_with_sequence(file_ids[1], 0, 999, 0, 2),
2358 new_file_handle_with_sequence(file_ids[2], 0, 999, 0, 3),
2359 new_file_handle_with_sequence(file_ids[3], 0, 999, 0, 4),
2360 new_file_handle_with_sequence(file_ids[4], 3000, 3999, 0, 5),
2362 new_file_handle_with_sequence(file_ids[5], 3000, 3999, 0, 6),
2363 new_file_handle_with_sequence(file_ids[6], 3000, 3999, 0, 7),
2364 new_file_handle_with_sequence(file_ids[7], 3000, 3999, 0, 8),
2365 new_file_handle_with_sequence(file_ids[8], 6000, 6999, 0, 9),
2367 new_file_handle_with_sequence(file_ids[9], 6000, 6999, 0, 10),
2368 ];
2369
2370 let windows = assign_to_windows(files.iter(), 3);
2371
2372 let picker = TwcsPicker {
2373 trigger_file_num: 4,
2374 active_window_l1_merge_trigger: 8,
2375 inactive_window_trigger_file_num: 4,
2376 inactive_window_l1_merge_trigger: 8,
2377 time_window_seconds: Some(3),
2378 max_output_file_size: None,
2379 append_mode: false,
2380 max_background_tasks: Some(max_background_tasks),
2381 time_range: None,
2382 };
2383
2384 let active_window = find_latest_window_in_seconds(files.iter(), 3);
2385 let output = picker
2386 .build_output_with_time_range(RegionId::from_u64(123), windows, active_window, None)
2387 .await
2388 .unwrap();
2389
2390 assert!(
2392 output.len() <= max_background_tasks,
2393 "Output should be truncated to max_background_tasks: expected <= {}, got {}",
2394 max_background_tasks,
2395 output.len()
2396 );
2397
2398 let picker_no_limit = TwcsPicker {
2400 trigger_file_num: 4,
2401 active_window_l1_merge_trigger: 8,
2402 inactive_window_trigger_file_num: 4,
2403 inactive_window_l1_merge_trigger: 8,
2404 time_window_seconds: Some(3),
2405 max_output_file_size: None,
2406 append_mode: false,
2407 max_background_tasks: None,
2408 time_range: None,
2409 };
2410
2411 let windows_no_limit = assign_to_windows(files.iter(), 3);
2412 let output_no_limit = picker_no_limit
2413 .build_output_with_time_range(
2414 RegionId::from_u64(123),
2415 windows_no_limit,
2416 active_window,
2417 None,
2418 )
2419 .await
2420 .unwrap();
2421
2422 if output_no_limit.len() > max_background_tasks {
2424 assert!(
2425 output_no_limit.len() > output.len(),
2426 "Without limit should have more outputs than with limit"
2427 );
2428 }
2429 }
2430
2431 #[tokio::test]
2432 async fn test_max_background_tasks_no_truncation_when_under_limit() {
2433 let file_ids = (0..4).map(|_| FileId::random()).collect::<Vec<_>>();
2434 let max_background_tasks = 10; let files = [
2438 new_file_handle_with_sequence(file_ids[0], 0, 999, 0, 1),
2439 new_file_handle_with_sequence(file_ids[1], 0, 999, 0, 2),
2440 new_file_handle_with_sequence(file_ids[2], 0, 999, 0, 3),
2441 new_file_handle_with_sequence(file_ids[3], 0, 999, 0, 4),
2442 ];
2443
2444 let windows = assign_to_windows(files.iter(), 3);
2445
2446 let picker = TwcsPicker {
2447 trigger_file_num: 4,
2448 active_window_l1_merge_trigger: 8,
2449 inactive_window_trigger_file_num: 4,
2450 inactive_window_l1_merge_trigger: 8,
2451 time_window_seconds: Some(3),
2452 max_output_file_size: None,
2453 append_mode: false,
2454 max_background_tasks: Some(max_background_tasks),
2455 time_range: None,
2456 };
2457
2458 let active_window = find_latest_window_in_seconds(files.iter(), 3);
2459 let output = picker
2460 .build_output_with_time_range(RegionId::from_u64(123), windows, active_window, None)
2461 .await
2462 .unwrap();
2463
2464 assert!(
2466 output.len() <= max_background_tasks,
2467 "Output should be within limit"
2468 );
2469 assert!(!output.is_empty(), "Should have at least one output");
2471 }
2472
2473 #[tokio::test]
2474 async fn test_pick_multiple_runs() {
2475 common_telemetry::init_default_ut_logging();
2476
2477 let num_files = 8;
2478 let file_ids = (0..num_files).map(|_| FileId::random()).collect::<Vec<_>>();
2479
2480 let files: Vec<_> = file_ids
2482 .iter()
2483 .enumerate()
2484 .map(|(idx, file_id)| {
2485 new_file_handle_with_size_and_sequence(
2486 *file_id,
2487 0,
2488 999,
2489 0,
2490 (idx + 1) as u64,
2491 1024 * 1024,
2492 )
2493 })
2494 .collect();
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 max_background_tasks: None,
2507 time_range: None,
2508 };
2509
2510 let active_window = find_latest_window_in_seconds(files.iter(), 3);
2511 let output = picker
2512 .build_output_with_time_range(RegionId::from_u64(123), windows, active_window, None)
2513 .await
2514 .unwrap();
2515
2516 assert_eq!(1, output.len());
2517 assert_eq!(output[0].inputs.len(), num_files);
2518 }
2519
2520 #[tokio::test]
2521 async fn test_window_trigger_can_exceed_input_limit() {
2522 common_telemetry::init_default_ut_logging();
2523
2524 let num_files = 50;
2525 let file_ids = (0..num_files).map(|_| FileId::random()).collect::<Vec<_>>();
2526
2527 let files: Vec<_> = file_ids
2529 .iter()
2530 .enumerate()
2531 .map(|(idx, file_id)| {
2532 new_file_handle_with_size_and_sequence(
2533 *file_id,
2534 (idx / 2 * 10) as i64,
2535 (idx / 2 * 10 + 5) as i64,
2536 0,
2537 (idx + 1) as u64,
2538 1024 * 1024,
2539 )
2540 })
2541 .collect();
2542
2543 let windows = assign_to_windows(files.iter(), 3);
2544
2545 let picker = TwcsPicker {
2546 trigger_file_num: num_files,
2547 active_window_l1_merge_trigger: 8,
2548 inactive_window_trigger_file_num: num_files,
2549 inactive_window_l1_merge_trigger: 8,
2550 time_window_seconds: Some(3),
2551 max_output_file_size: None,
2552 append_mode: false,
2553 max_background_tasks: None,
2554 time_range: None,
2555 };
2556
2557 let active_window = find_latest_window_in_seconds(files.iter(), 3);
2558 let output = picker
2559 .build_output_with_time_range(RegionId::from_u64(123), windows, active_window, None)
2560 .await
2561 .unwrap();
2562
2563 assert_eq!(1, output.len());
2564 assert_eq!(output[0].inputs.len(), num_files.min(*MAX_INPUT_FILES));
2565 }
2566
2567 #[tokio::test]
2568 async fn test_limit_max_input_files_keeps_deletion_markers() {
2569 common_telemetry::init_default_ut_logging();
2570
2571 let mut files = vec![new_file_handle_with_size_and_sequence(
2574 FileId::random(),
2575 0,
2576 3_000_000,
2577 0,
2578 1,
2579 1024 * 1024 * 1024,
2580 )];
2581 files.extend((0..DEFAULT_MAX_INPUT_FILES as i64).map(|idx| {
2582 new_file_handle_with_size_and_sequence(
2583 FileId::random(),
2584 (idx + 1) * 10_000,
2585 (idx + 1) * 10_000 + 1_000,
2586 0,
2587 (idx + 2) as u64,
2588 1024,
2589 )
2590 }));
2591
2592 let windows = assign_to_windows(files.iter(), 3600);
2593
2594 let picker = TwcsPicker {
2595 trigger_file_num: 4,
2596 active_window_l1_merge_trigger: 8,
2597 inactive_window_trigger_file_num: 4,
2598 inactive_window_l1_merge_trigger: 8,
2599 time_window_seconds: Some(3600),
2600 max_output_file_size: None,
2601 append_mode: false,
2602 max_background_tasks: None,
2603 time_range: None,
2604 };
2605
2606 let active_window = find_latest_window_in_seconds(files.iter(), 3600);
2607 let output = picker
2608 .build_output_with_time_range(RegionId::from_u64(123), windows, active_window, None)
2609 .await
2610 .unwrap();
2611
2612 assert_eq!(1, output.len());
2613 assert_eq!(DEFAULT_MAX_INPUT_FILES, output[0].inputs.len());
2616 assert!(
2617 !output[0].filter_deleted,
2618 "deletion markers must be kept once the file num limit drops files they may mask"
2619 );
2620 }
2621
2622 #[tokio::test]
2623 async fn test_limit_max_input_files_still_filters_without_overlap() {
2624 common_telemetry::init_default_ut_logging();
2625
2626 let files: Vec<_> = (0..DEFAULT_MAX_INPUT_FILES as i64 + 8)
2629 .map(|idx| {
2630 new_file_handle_with_size_and_sequence(
2631 FileId::random(),
2632 (idx + 1) * 10_000,
2633 (idx + 1) * 10_000 + 1_000,
2634 0,
2635 (idx + 1) as u64,
2636 1024,
2637 )
2638 })
2639 .collect();
2640
2641 let windows = assign_to_windows(files.iter(), 3600);
2642
2643 let picker = TwcsPicker {
2644 trigger_file_num: 4,
2645 active_window_l1_merge_trigger: 8,
2646 inactive_window_trigger_file_num: 4,
2647 inactive_window_l1_merge_trigger: 8,
2648 time_window_seconds: Some(3600),
2649 max_output_file_size: Some(1024 * 1024 * 1024),
2650 append_mode: false,
2651 max_background_tasks: None,
2652 time_range: None,
2653 };
2654
2655 let active_window = find_latest_window_in_seconds(files.iter(), 3600);
2656 let output = picker
2657 .build_output_with_time_range(RegionId::from_u64(123), windows, active_window, None)
2658 .await
2659 .unwrap();
2660
2661 assert_eq!(1, output.len());
2662 assert_eq!(DEFAULT_MAX_INPUT_FILES, output[0].inputs.len());
2663 assert!(output[0].filter_deleted);
2664 }
2665
2666 #[tokio::test]
2667 async fn test_newer_windows_have_priority() {
2668 let older_file_ids = [FileId::random(), FileId::random()];
2669 let newer_file_ids = [FileId::random(), FileId::random()];
2670 let files = [
2671 new_file_handle_with_sequence(older_file_ids[0], 1_000, 1_999, 0, 1),
2672 new_file_handle_with_sequence(older_file_ids[1], 1_000, 1_999, 0, 2),
2673 new_file_handle_with_sequence(newer_file_ids[0], 7_000, 7_999, 0, 3),
2674 new_file_handle_with_sequence(newer_file_ids[1], 7_000, 7_999, 0, 4),
2675 ];
2676 let windows = assign_to_windows(files.iter(), 3);
2677 let picker = TwcsPicker {
2678 trigger_file_num: 2,
2679 active_window_l1_merge_trigger: 8,
2680 inactive_window_trigger_file_num: 2,
2681 inactive_window_l1_merge_trigger: 8,
2682 time_window_seconds: Some(3),
2683 max_output_file_size: None,
2684 append_mode: false,
2685 max_background_tasks: Some(1),
2686 time_range: None,
2687 };
2688
2689 let output = picker
2690 .build_output_with_time_range(RegionId::from_u64(123), windows, Some(6), None)
2691 .await
2692 .unwrap();
2693
2694 assert_eq!(1, output.len());
2695 assert_eq!(
2696 newer_file_ids.into_iter().collect::<HashSet<_>>(),
2697 output[0]
2698 .inputs
2699 .iter()
2700 .map(|file| file.file_id().file_id())
2701 .collect::<HashSet<_>>()
2702 );
2703 }
2704
2705 #[test]
2706 fn test_filter_time_windows_by_time_range() {
2707 for (start, end, expected_windows) in [
2710 (1_200, 1_800, vec![0]),
2711 (0, 3_000, vec![0]),
2712 (3_000, 6_000, vec![3]),
2713 (2_999, 3_000, vec![0]),
2714 (2_999, 3_001, vec![0, 3]),
2715 (3_000, 3_001, vec![3]),
2716 (3_001, 3_002, vec![3]),
2717 (-3_000, 0, vec![-3]),
2718 (-3_001, -2_999, vec![-6, -3]),
2719 (0, 0, vec![]),
2720 (1_500, 1_500, vec![]),
2721 ] {
2722 let time_range = TimestampRange::with_unit(start, end, TimeUnit::Millisecond).unwrap();
2723 let selected = [-6, -3, 0, 3, 6]
2724 .into_iter()
2725 .filter(|window| time_window_intersects_range(*window, 3, &time_range))
2726 .collect::<Vec<_>>();
2727 assert_eq!(expected_windows, selected, "{time_range}");
2728 }
2729 }
2730
2731 #[test]
2732 fn test_filter_time_windows_with_unbounded_and_extreme_ranges() {
2733 for (range, expected_windows) in [
2734 (TimestampRange::min_to_max(), vec![-3, 0, 3]),
2735 (
2736 TimestampRange::from_start(Timestamp::new_second(0)),
2737 vec![0, 3],
2738 ),
2739 (
2740 TimestampRange::until_end(Timestamp::new_second(0), false),
2741 vec![-3],
2742 ),
2743 ] {
2744 let selected = [-3, 0, 3]
2745 .into_iter()
2746 .filter(|window| time_window_intersects_range(*window, 3, &range))
2747 .collect::<Vec<_>>();
2748 assert_eq!(expected_windows, selected, "{range}");
2749 }
2750
2751 let upper_range = TimestampRange::new(
2752 Timestamp::new_second(i64::MAX - 1),
2753 Timestamp::new_second(i64::MAX),
2754 )
2755 .unwrap();
2756 assert!(!time_window_intersects_range(0, 4, &upper_range));
2757 assert!(time_window_intersects_range(i64::MAX - 3, 4, &upper_range));
2758
2759 let lower_range = TimestampRange::until_end(Timestamp::new_second(i64::MIN), false);
2760 assert!(!time_window_intersects_range(i64::MIN, 3, &lower_range));
2761 }
2762
2763 #[tokio::test]
2764 async fn test_time_range_filter_precedes_background_task_limit() {
2765 let early_file_ids = [FileId::random(), FileId::random()];
2766 let selected_file_ids = [FileId::random(), FileId::random()];
2767 let files = [
2768 new_file_handle_with_sequence(early_file_ids[0], 1_000, 1_999, 0, 1),
2769 new_file_handle_with_sequence(early_file_ids[1], 1_000, 1_999, 0, 2),
2770 new_file_handle_with_sequence(selected_file_ids[0], 7_000, 7_999, 0, 3),
2771 new_file_handle_with_sequence(selected_file_ids[1], 7_000, 7_999, 0, 4),
2772 ];
2773 let windows = assign_to_windows(files.iter(), 3);
2774 let picker = TwcsPicker {
2775 trigger_file_num: 2,
2776 active_window_l1_merge_trigger: 8,
2777 inactive_window_trigger_file_num: 2,
2778 inactive_window_l1_merge_trigger: 8,
2779 time_window_seconds: Some(3),
2780 max_output_file_size: None,
2781 append_mode: false,
2782 max_background_tasks: Some(1),
2783 time_range: TimestampRange::new(
2784 Timestamp::new_millisecond(7_200),
2785 Timestamp::new_millisecond(7_800),
2786 ),
2787 };
2788
2789 let output = picker
2790 .build_output_with_time_range(RegionId::from_u64(123), windows, Some(6), Some(3))
2791 .await
2792 .unwrap();
2793
2794 assert_eq!(1, output.len());
2795 assert_eq!(
2796 selected_file_ids.into_iter().collect::<HashSet<_>>(),
2797 output[0]
2798 .inputs
2799 .iter()
2800 .map(|file| file.file_id().file_id())
2801 .collect::<HashSet<_>>()
2802 );
2803 }
2804
2805 #[tokio::test]
2806 async fn test_inactive_window_uses_its_trigger_file_num() {
2807 let files = [
2808 new_file_handle_with_size_and_sequence(FileId::random(), 0, 10, 0, 1, 10),
2809 new_file_handle_with_size_and_sequence(FileId::random(), 20, 30, 0, 2, 10),
2810 ];
2811 let windows = assign_to_windows(files.iter(), 100);
2812 let picker = TwcsPicker {
2813 trigger_file_num: 4,
2814 active_window_l1_merge_trigger: 8,
2815 inactive_window_trigger_file_num: 2,
2816 inactive_window_l1_merge_trigger: 8,
2817 time_window_seconds: Some(100),
2818 max_output_file_size: None,
2819 append_mode: false,
2820 max_background_tasks: None,
2821 time_range: None,
2822 };
2823
2824 let output = picker
2825 .build_output_with_time_range(RegionId::from_u64(1), windows, Some(100), None)
2826 .await
2827 .unwrap();
2828
2829 assert_eq!(1, output.len());
2830 assert_eq!(2, output[0].inputs.len());
2831 }
2832
2833 #[test]
2834 fn test_inactive_window_l1_trigger_is_independent_of_l0_trigger() {
2835 let l0_files = (0..6)
2836 .map(|idx| {
2837 new_file_handle_with_size_and_sequence(
2838 FileId::random(),
2839 idx * 20,
2840 idx * 20 + 10,
2841 0,
2842 idx as u64 + 1,
2843 10,
2844 )
2845 })
2846 .collect();
2847 let l1_files = (0..2)
2848 .map(|idx| {
2849 new_file_handle_with_size_and_sequence(
2850 FileId::random(),
2851 idx * 20,
2852 idx * 20 + 10,
2853 1,
2854 idx as u64 + 10,
2855 10,
2856 )
2857 })
2858 .collect();
2859
2860 let (inputs, _) = pick_inactive_window_files(
2861 l0_files,
2862 l1_files,
2863 InactiveWindowPick {
2864 l0_file_num: 8,
2865 l1_file_num: 2,
2866 phase: PickPhase::L1FileReduction,
2867 primary_key_mapper: &primary_key_mapper_for_test(),
2868 },
2869 None,
2870 );
2871
2872 assert_eq!(2, inputs.len());
2873 assert!(inputs.iter().all(|file| file.level() == 1));
2874 }
2875
2876 #[test]
2877 fn test_inactive_window_does_not_fallback_to_l1_below_trigger() {
2878 let l1_files = (0..2)
2879 .map(|idx| {
2880 new_file_handle_with_size_and_sequence(
2881 FileId::random(),
2882 idx * 20,
2883 idx * 20 + 10,
2884 1,
2885 idx as u64 + 1,
2886 10,
2887 )
2888 })
2889 .collect();
2890
2891 let (inputs, _) = pick_inactive_window_files(
2892 vec![],
2893 l1_files,
2894 InactiveWindowPick {
2895 l0_file_num: 2,
2896 l1_file_num: 8,
2897 phase: PickPhase::L1FileReduction,
2898 primary_key_mapper: &primary_key_mapper_for_test(),
2899 },
2900 None,
2901 );
2902
2903 assert!(inputs.is_empty());
2904 }
2905
2906 #[tokio::test]
2907 async fn test_inactive_window_requires_a_level_trigger_before_mixed_fallback() {
2908 let files = [
2909 new_file_handle_with_size_and_sequence(FileId::random(), 0, 10, 0, 1, 10),
2910 new_file_handle_with_size_and_sequence(FileId::random(), 0, 10, 1, 2, 10),
2911 ];
2912 let windows = assign_to_windows(files.iter(), 100);
2913 let picker = TwcsPicker {
2914 trigger_file_num: 4,
2915 active_window_l1_merge_trigger: 8,
2916 inactive_window_trigger_file_num: 8,
2917 inactive_window_l1_merge_trigger: 2,
2918 time_window_seconds: Some(100),
2919 max_output_file_size: Some(1_000),
2920 append_mode: false,
2921 max_background_tasks: None,
2922 time_range: None,
2923 };
2924
2925 let output = picker
2926 .build_output_with_time_range(RegionId::from_u64(1), windows, Some(100), None)
2927 .await
2928 .unwrap();
2929
2930 assert!(output.is_empty());
2931 }
2932
2933 #[tokio::test]
2934 async fn test_count_first_prefers_more_files_over_smaller_overlap() {
2935 let files = [
2936 new_file_handle_with_size_and_sequence(FileId::random(), 0, 10, 0, 1, 10),
2937 new_file_handle_with_size_and_sequence(FileId::random(), 20, 30, 0, 2, 10),
2938 new_file_handle_with_size_and_sequence(FileId::random(), 40, 50, 0, 3, 10),
2939 new_file_handle_with_size_and_sequence(FileId::random(), 5, 15, 0, 4, 10),
2940 ];
2941 let windows = assign_to_windows(files.iter(), 100);
2942 let picker = TwcsPicker {
2943 trigger_file_num: 2,
2944 active_window_l1_merge_trigger: 8,
2945 inactive_window_trigger_file_num: 2,
2946 inactive_window_l1_merge_trigger: 8,
2947 time_window_seconds: Some(100),
2948 max_output_file_size: None,
2949 append_mode: false,
2950 max_background_tasks: None,
2951 time_range: None,
2952 };
2953
2954 let output = picker
2955 .build_output_with_time_range(RegionId::from_u64(1), windows, Some(0), None)
2956 .await
2957 .unwrap();
2958
2959 assert_eq!(1, output.len());
2960 assert_eq!(4, output[0].inputs.len());
2961 }
2962
2963 #[tokio::test]
2964 async fn test_count_first_trigger_counts_physical_ssts() {
2965 let files = [
2966 new_file_handle_with_size_and_sequence(FileId::random(), 0, 10, 0, 1, 10),
2967 new_file_handle_with_size_and_sequence(FileId::random(), 0, 10, 0, 1, 10),
2968 new_file_handle_with_size_and_sequence(FileId::random(), 20, 30, 0, 2, 10),
2969 ];
2970 let windows = assign_to_windows(files.iter(), 100);
2971 let picker = TwcsPicker {
2972 trigger_file_num: 3,
2973 active_window_l1_merge_trigger: 8,
2974 inactive_window_trigger_file_num: 3,
2975 inactive_window_l1_merge_trigger: 8,
2976 time_window_seconds: Some(100),
2977 max_output_file_size: None,
2978 append_mode: false,
2979 max_background_tasks: None,
2980 time_range: None,
2981 };
2982
2983 let output = picker
2984 .build_output_with_time_range(RegionId::from_u64(1), windows, Some(0), None)
2985 .await
2986 .unwrap();
2987
2988 assert_eq!(1, output.len());
2989 assert_eq!(3, output[0].inputs.len());
2990 }
2991
2992 #[tokio::test]
2993 async fn test_count_first_does_not_compact_overlap_below_trigger() {
2994 let files = [
2995 new_file_handle_with_size_and_sequence(FileId::random(), 0, 20, 0, 1, 10),
2996 new_file_handle_with_size_and_sequence(FileId::random(), 10, 30, 0, 2, 10),
2997 ];
2998 let windows = assign_to_windows(files.iter(), 100);
2999 let picker = TwcsPicker {
3000 trigger_file_num: 3,
3001 active_window_l1_merge_trigger: 8,
3002 inactive_window_trigger_file_num: 3,
3003 inactive_window_l1_merge_trigger: 8,
3004 time_window_seconds: Some(100),
3005 max_output_file_size: None,
3006 append_mode: false,
3007 max_background_tasks: None,
3008 time_range: None,
3009 };
3010
3011 let output = picker
3012 .build_output_with_time_range(RegionId::from_u64(1), windows, Some(0), None)
3013 .await
3014 .unwrap();
3015
3016 assert!(output.is_empty());
3017 }
3018
3019 #[tokio::test]
3020 async fn test_filter_deleted_is_false_when_selected_files_overlap_unselected_file() {
3021 let mut files = (0..32)
3022 .map(|idx| {
3023 new_file_handle_with_size_and_sequence(
3024 FileId::random(),
3025 idx * 10,
3026 idx * 10 + 9,
3027 0,
3028 idx as u64 + 1,
3029 10,
3030 )
3031 })
3032 .collect::<Vec<_>>();
3033 files.push(new_file_handle_with_size_and_sequence(
3034 FileId::random(),
3035 0,
3036 320,
3037 0,
3038 33,
3039 10,
3040 ));
3041 let windows = assign_to_windows(files.iter(), 1000);
3042 let picker = TwcsPicker {
3043 trigger_file_num: 2,
3044 active_window_l1_merge_trigger: 8,
3045 inactive_window_trigger_file_num: 2,
3046 inactive_window_l1_merge_trigger: 8,
3047 time_window_seconds: Some(1000),
3048 max_output_file_size: None,
3049 append_mode: false,
3050 max_background_tasks: None,
3051 time_range: None,
3052 };
3053
3054 let output = picker
3055 .build_output_with_time_range(RegionId::from_u64(1), windows, Some(0), None)
3056 .await
3057 .unwrap();
3058
3059 assert_eq!(1, output.len());
3060 assert_eq!(DEFAULT_MAX_INPUT_FILES, output[0].inputs.len());
3061 assert!(!output[0].filter_deleted);
3062 }
3063
3064 fn new_file(start: i64, end: i64, sequence: u64, file_size: u64) -> FileHandle {
3065 new_file_handle_with_size_and_sequence(FileId::random(), start, end, 0, sequence, file_size)
3066 }
3067
3068 fn new_file_with_level_and_rows(
3069 start: i64,
3070 end: i64,
3071 level: Level,
3072 sequence: u64,
3073 file_size: u64,
3074 num_rows: u64,
3075 ) -> FileHandle {
3076 let file = new_file_handle_with_size_and_sequence(
3077 FileId::random(),
3078 start,
3079 end,
3080 level,
3081 sequence,
3082 file_size,
3083 );
3084 let mut meta = file.meta_ref().clone();
3085 meta.num_rows = num_rows;
3086 FileHandle::new(meta, crate::test_util::new_noop_file_purger())
3087 }
3088
3089 fn picked_ranges(files: &[FileHandle]) -> Vec<(i64, i64)> {
3090 files
3091 .iter()
3092 .map(|file| {
3093 let (start, end) = file.range();
3094 (start.value(), end.value())
3095 })
3096 .collect()
3097 }
3098
3099 #[test]
3100 fn test_count_first_rejects_dominant_historical_file() {
3101 let picked = pick_count_first(
3102 vec![SortedRun::from(vec![
3103 new_file(0, 9, 1, 400),
3104 new_file(20, 29, 2, 100),
3105 ])],
3106 None,
3107 );
3108
3109 assert!(picked.is_empty());
3110 }
3111
3112 #[test]
3113 fn test_count_first_accepts_balanced_historical_file() {
3114 let picked = pick_count_first(
3115 vec![SortedRun::from(vec![
3116 new_file(0, 9, 1, 400),
3117 new_file(20, 29, 2, 100),
3118 new_file(40, 49, 3, 100),
3119 new_file(60, 69, 4, 100),
3120 new_file(80, 89, 5, 100),
3121 ])],
3122 None,
3123 );
3124
3125 assert_eq!(
3126 vec![(0, 9), (20, 29), (40, 49), (60, 69), (80, 89)],
3127 picked_ranges(&picked)
3128 );
3129 }
3130
3131 #[test]
3132 fn test_count_first_finds_smaller_balanced_interval_when_larger_one_is_unbalanced() {
3133 let picked = pick_count_first(
3134 vec![SortedRun::from(vec![
3135 new_file(0, 9, 1, 1000),
3136 new_file(20, 29, 2, 100),
3137 new_file(40, 49, 3, 100),
3138 new_file(60, 69, 4, 100),
3139 new_file(80, 89, 5, 100),
3140 ])],
3141 None,
3142 );
3143
3144 assert_eq!(
3145 vec![(20, 29), (40, 49), (60, 69), (80, 89)],
3146 picked_ranges(&picked)
3147 );
3148 }
3149
3150 #[test]
3151 fn test_count_first_prefers_overlap_participants_when_file_counts_match() {
3152 let first_run = (0..DEFAULT_MAX_INPUT_FILES)
3153 .map(|idx| {
3154 let start = idx as i64 * 20;
3155 let end = if idx + 1 == DEFAULT_MAX_INPUT_FILES {
3156 700
3157 } else {
3158 start + 9
3159 };
3160 new_file(start, end, idx as u64 + 1, 10)
3161 })
3162 .collect::<Vec<_>>();
3163 let overlapping = new_file(690, 710, 100, 10);
3164
3165 let picked = pick_count_first(
3166 vec![
3167 SortedRun::from(first_run),
3168 SortedRun::from(vec![overlapping]),
3169 ],
3170 None,
3171 );
3172 let ranges = picked_ranges(&picked);
3173
3174 assert_eq!(DEFAULT_MAX_INPUT_FILES, ranges.len());
3175 assert!(!ranges.contains(&(0, 9)));
3176 assert!(ranges.contains(&(690, 710)));
3177 }
3178
3179 #[tokio::test]
3180 async fn test_picker_avoids_chained_l1_rewrites_by_compacting_levels_separately() {
3181 let mut enough_l0 = (0..DEFAULT_MAX_INPUT_FILES)
3182 .map(|idx| {
3183 let start = idx as i64 * 20;
3184 new_file_handle_with_size_and_sequence(
3185 FileId::random(),
3186 start,
3187 start + 9,
3188 0,
3189 idx as u64 + 1,
3190 10,
3191 )
3192 })
3193 .collect::<Vec<_>>();
3194 enough_l0.push(new_file_handle_with_size_and_sequence(
3195 FileId::random(),
3196 0,
3197 700,
3198 1,
3199 100,
3200 100,
3201 ));
3202 let mut enough_l1 = (0..4)
3203 .map(|idx| {
3204 new_file_handle_with_size_and_sequence(
3205 FileId::random(),
3206 idx * 100,
3207 idx * 100 + 99,
3208 1,
3209 idx as u64 + 1,
3210 100,
3211 )
3212 })
3213 .collect::<Vec<_>>();
3214 enough_l1.extend((0..3).map(|idx| {
3215 new_file_handle_with_size_and_sequence(
3216 FileId::random(),
3217 idx * 20,
3218 idx * 20 + 9,
3219 0,
3220 idx as u64 + 10,
3221 10,
3222 )
3223 }));
3224 let picker = TwcsPicker {
3225 trigger_file_num: 4,
3226 active_window_l1_merge_trigger: 8,
3227 inactive_window_trigger_file_num: 4,
3228 inactive_window_l1_merge_trigger: 4,
3229 time_window_seconds: Some(1),
3230 max_output_file_size: None,
3231 append_mode: false,
3232 max_background_tasks: None,
3233 time_range: None,
3234 };
3235
3236 for (case, files, expected_level, expected_len) in [
3237 ("L0 reaches trigger", enough_l0, 0, DEFAULT_MAX_INPUT_FILES),
3238 ("L0 fallback precedes triggered L1", enough_l1, 0, 3),
3239 ] {
3240 let windows = assign_to_windows(files.iter(), 1);
3241 let output = picker
3242 .build_output_with_time_range(RegionId::from_u64(1), windows, Some(1), None)
3243 .await
3244 .unwrap();
3245
3246 assert_eq!(1, output.len(), "{case}");
3247 assert_eq!(expected_len, output[0].inputs.len(), "{case}");
3248 assert!(
3249 output[0]
3250 .inputs
3251 .iter()
3252 .all(|file| file.level() == expected_level),
3253 "{case}"
3254 );
3255 }
3256 }
3257
3258 #[tokio::test]
3259 async fn test_active_window_defers_l1_below_safety_trigger() {
3260 let files = (0..7)
3261 .map(|idx| {
3262 let start = idx * 100;
3263 new_file_handle_with_size_and_sequence(
3264 FileId::random(),
3265 start,
3266 start + 99,
3267 1,
3268 idx as u64 + 1,
3269 100,
3270 )
3271 })
3272 .collect::<Vec<_>>();
3273 let windows = assign_to_windows(files.iter(), 1);
3274 let active_window = windows.keys().next().copied();
3275 let picker = TwcsPicker {
3276 trigger_file_num: 4,
3277 active_window_l1_merge_trigger: 8,
3278 inactive_window_trigger_file_num: 2,
3279 inactive_window_l1_merge_trigger: 8,
3280 time_window_seconds: Some(1),
3281 max_output_file_size: None,
3282 append_mode: false,
3283 max_background_tasks: None,
3284 time_range: None,
3285 };
3286
3287 let output = picker
3288 .build_output_with_time_range(RegionId::from_u64(1), windows, active_window, None)
3289 .await
3290 .unwrap();
3291
3292 assert!(output.is_empty());
3293 }
3294
3295 #[tokio::test]
3296 async fn test_active_window_l1_safety_trigger_is_independent_of_l0_trigger() {
3297 let files = (0..8)
3298 .map(|idx| {
3299 let start = idx * 100;
3300 new_file_handle_with_size_and_sequence(
3301 FileId::random(),
3302 start,
3303 start + 99,
3304 1,
3305 idx as u64 + 1,
3306 100,
3307 )
3308 })
3309 .collect::<Vec<_>>();
3310 let windows = assign_to_windows(files.iter(), 1);
3311 let active_window = windows.keys().next().copied();
3312 let picker = TwcsPicker {
3313 trigger_file_num: 16,
3314 active_window_l1_merge_trigger: 8,
3315 inactive_window_trigger_file_num: 2,
3316 inactive_window_l1_merge_trigger: 8,
3317 time_window_seconds: Some(1),
3318 max_output_file_size: None,
3319 append_mode: false,
3320 max_background_tasks: None,
3321 time_range: None,
3322 };
3323
3324 let output = picker
3325 .build_output_with_time_range(RegionId::from_u64(1), windows, active_window, None)
3326 .await
3327 .unwrap();
3328
3329 assert_eq!(1, output.len());
3330 assert_eq!(8, output[0].inputs.len());
3331 assert!(output[0].inputs.iter().all(|file| file.level() == 1));
3332 }
3333
3334 #[tokio::test]
3335 async fn test_picker_falls_back_to_l1_when_triggered_l0_cannot_make_progress() {
3336 let mut files = (0..4)
3337 .map(|idx| {
3338 let start = idx * 20;
3339 new_file_handle_with_size_and_sequence(
3340 FileId::random(),
3341 start,
3342 start + 9,
3343 0,
3344 idx as u64 + 1,
3345 600,
3346 )
3347 })
3348 .collect::<Vec<_>>();
3349 files.extend((0..8).map(|idx| {
3350 let start = idx * 20 + 100;
3351 new_file_handle_with_size_and_sequence(
3352 FileId::random(),
3353 start,
3354 start + 9,
3355 1,
3356 idx as u64 + 10,
3357 50,
3358 )
3359 }));
3360 let windows = assign_to_windows(files.iter(), 1);
3361 let active_window = windows.keys().next().copied();
3362 let picker = TwcsPicker {
3363 trigger_file_num: 4,
3364 active_window_l1_merge_trigger: 8,
3365 inactive_window_trigger_file_num: 4,
3366 inactive_window_l1_merge_trigger: 8,
3367 time_window_seconds: Some(1),
3368 max_output_file_size: Some(512),
3369 append_mode: false,
3370 max_background_tasks: None,
3371 time_range: None,
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_eq!(1, output.len());
3380 assert_eq!(8, output[0].inputs.len());
3381 assert!(output[0].inputs.iter().all(|file| file.level() == 1));
3382 }
3383
3384 fn priority_test_files(
3385 window: i64,
3386 level: Level,
3387 file_size: u64,
3388 overlap: bool,
3389 ) -> [FileHandle; 2] {
3390 let start = window * 1_000;
3391 let ranges = if overlap {
3392 [(start, start + 499), (start + 250, start + 749)]
3393 } else {
3394 [(start, start + 99), (start + 200, start + 299)]
3395 };
3396 ranges.map(|(start, end)| {
3397 new_file_handle_with_size_and_sequence(
3398 FileId::random(),
3399 start,
3400 end,
3401 level,
3402 window as u64 + 1,
3403 file_size,
3404 )
3405 })
3406 }
3407
3408 fn priority_test_picker(max_background_tasks: usize) -> TwcsPicker {
3409 TwcsPicker {
3410 trigger_file_num: 2,
3411 active_window_l1_merge_trigger: 2,
3412 inactive_window_trigger_file_num: 2,
3413 inactive_window_l1_merge_trigger: 2,
3414 time_window_seconds: Some(1),
3415 max_output_file_size: Some(100),
3416 append_mode: false,
3417 max_background_tasks: Some(max_background_tasks),
3418 time_range: None,
3419 }
3420 }
3421
3422 fn output_window(output: &CompactionOutput) -> i64 {
3423 output.inputs[0]
3424 .time_range()
3425 .1
3426 .convert_to(TimeUnit::Second)
3427 .unwrap()
3428 .value()
3429 }
3430
3431 #[tokio::test]
3432 async fn test_older_l0_candidate_has_priority_over_newer_pure_l1_candidate() {
3433 let files = priority_test_files(0, 0, 40, false)
3434 .into_iter()
3435 .chain(priority_test_files(1, 1, 40, false))
3436 .collect::<Vec<_>>();
3437 let windows = assign_to_windows(files.iter(), 1);
3438
3439 let output = priority_test_picker(1)
3440 .build_output_with_time_range(RegionId::from_u64(1), windows, Some(1), None)
3441 .await
3442 .unwrap();
3443
3444 assert_eq!(1, output.len());
3445 assert_eq!(0, output_window(&output[0]));
3446 assert!(output[0].inputs.iter().all(|file| file.level() == 0));
3447 }
3448
3449 #[tokio::test]
3450 async fn test_older_l1_file_reduction_has_priority_over_newer_overlap_only_l1() {
3451 let files = priority_test_files(0, 1, 40, false)
3452 .into_iter()
3453 .chain(priority_test_files(1, 1, 100, true))
3454 .collect::<Vec<_>>();
3455 let windows = assign_to_windows(files.iter(), 1);
3456
3457 let output = priority_test_picker(1)
3458 .build_output_with_time_range(RegionId::from_u64(1), windows, Some(1), None)
3459 .await
3460 .unwrap();
3461
3462 assert_eq!(1, output.len());
3463 assert_eq!(0, output_window(&output[0]));
3464 }
3465
3466 #[tokio::test]
3467 async fn test_multiple_slots_follow_class_priority_and_newer_first_within_class() {
3468 let files = [
3469 priority_test_files(0, 0, 40, false),
3470 priority_test_files(1, 0, 40, false),
3471 priority_test_files(2, 1, 40, false),
3472 priority_test_files(3, 1, 40, false),
3473 priority_test_files(4, 1, 100, true),
3474 priority_test_files(5, 1, 100, true),
3475 ]
3476 .into_iter()
3477 .flatten()
3478 .collect::<Vec<_>>();
3479 let windows = assign_to_windows(files.iter(), 1);
3480
3481 let output = priority_test_picker(5)
3482 .build_output_with_time_range(RegionId::from_u64(1), windows, Some(5), None)
3483 .await
3484 .unwrap();
3485 let pop_priority = output
3486 .iter()
3487 .rev()
3488 .map(|output| (output.inputs[0].level(), output_window(output)))
3489 .collect::<Vec<_>>();
3490
3491 assert_eq!(vec![(0, 1), (0, 0), (1, 3), (1, 2), (1, 5)], pop_priority);
3492 }
3493
3494 #[tokio::test]
3495 async fn test_inactive_window_mixed_fallback_merges_within_rewrite_budget() {
3496 let files = [
3497 new_file_with_level_and_rows(0, 99, 1, 1, 1_000, 1_000_000),
3498 new_file_with_level_and_rows(0, 9, 0, 2, 10, 1),
3499 ];
3500 let windows = assign_to_windows(files.iter(), 100);
3501 let picker = TwcsPicker {
3502 trigger_file_num: 4,
3503 active_window_l1_merge_trigger: 8,
3504 inactive_window_trigger_file_num: 2,
3505 inactive_window_l1_merge_trigger: 8,
3506 time_window_seconds: Some(100),
3507 max_output_file_size: Some(2_000),
3508 append_mode: false,
3509 max_background_tasks: None,
3510 time_range: None,
3511 };
3512
3513 let output = picker
3514 .build_output_with_time_range(RegionId::from_u64(1), windows, Some(100), None)
3515 .await
3516 .unwrap();
3517
3518 assert_eq!(1, output.len());
3519 assert_eq!(2, output[0].inputs.len());
3520 }
3521
3522 #[tokio::test]
3523 async fn test_inactive_window_over_budget_merges_l0_only() {
3524 let files = [
3527 new_file_handle_with_size_and_sequence(FileId::random(), 0, 9, 0, 1, 10_000),
3528 new_file_handle_with_size_and_sequence(FileId::random(), 20, 29, 0, 2, 10),
3529 new_file_handle_with_size_and_sequence(FileId::random(), 0, 99, 1, 3, 1_000_000),
3530 ];
3531 let windows = assign_to_windows(files.iter(), 100);
3532 let picker = TwcsPicker {
3533 trigger_file_num: 4,
3534 active_window_l1_merge_trigger: 8,
3535 inactive_window_trigger_file_num: 2,
3536 inactive_window_l1_merge_trigger: 8,
3537 time_window_seconds: Some(100),
3538 max_output_file_size: Some(100_000),
3539 append_mode: false,
3540 max_background_tasks: None,
3541 time_range: None,
3542 };
3543
3544 let output = picker
3545 .build_output_with_time_range(RegionId::from_u64(1), windows, Some(100), None)
3546 .await
3547 .unwrap();
3548
3549 assert_eq!(1, output.len());
3550 assert_eq!(2, output[0].inputs.len());
3551 assert!(output[0].inputs.iter().all(|file| file.level() == 0));
3552 }
3553
3554 #[tokio::test]
3555 async fn test_inactive_window_over_budget_without_l0_pair_does_not_merge() {
3556 let files = [
3559 new_file_handle_with_size_and_sequence(FileId::random(), 0, 9, 0, 1, 10),
3560 new_file_handle_with_size_and_sequence(FileId::random(), 0, 99, 1, 2, 1_000_000),
3561 ];
3562 let windows = assign_to_windows(files.iter(), 100);
3563 let picker = TwcsPicker {
3564 trigger_file_num: 4,
3565 active_window_l1_merge_trigger: 8,
3566 inactive_window_trigger_file_num: 2,
3567 inactive_window_l1_merge_trigger: 8,
3568 time_window_seconds: Some(100),
3569 max_output_file_size: Some(100_000),
3570 append_mode: false,
3571 max_background_tasks: None,
3572 time_range: None,
3573 };
3574
3575 let output = picker
3576 .build_output_with_time_range(RegionId::from_u64(1), windows, Some(100), None)
3577 .await
3578 .unwrap();
3579
3580 assert!(output.is_empty());
3581 }
3582
3583 #[tokio::test]
3584 async fn test_inactive_window_falls_through_when_triggered_l0_pick_fails() {
3585 let files = [
3589 new_file_handle_with_size_and_sequence(FileId::random(), 0, 9, 0, 1, 100_000),
3590 new_file_handle_with_size_and_sequence(FileId::random(), 20, 29, 0, 2, 10),
3591 new_file_handle_with_size_and_sequence(FileId::random(), 0, 99, 1, 3, 50_000),
3592 ];
3593 let windows = assign_to_windows(files.iter(), 100);
3594 let picker = TwcsPicker {
3595 trigger_file_num: 4,
3596 active_window_l1_merge_trigger: 8,
3597 inactive_window_trigger_file_num: 2,
3598 inactive_window_l1_merge_trigger: 8,
3599 time_window_seconds: Some(100),
3600 max_output_file_size: Some(1_000_000),
3601 append_mode: false,
3602 max_background_tasks: None,
3603 time_range: None,
3604 };
3605
3606 let output = picker
3607 .build_output_with_time_range(RegionId::from_u64(1), windows, Some(100), None)
3608 .await
3609 .unwrap();
3610
3611 assert_eq!(1, output.len());
3612 assert_eq!(3, output[0].inputs.len());
3613 }
3614
3615 #[test]
3616 fn test_mixed_candidate_row_balance() {
3617 for (case, l1_rows, l0_rows, expected_len) in [
3618 ("L1 rows dominate", 1_000_000, 10_000, 0),
3619 ("L1 rows meet ratio", 60_000, 10_000, 4),
3620 ("L0 rows are unknown", 1_000_000, 0, 4),
3621 ] {
3622 let files = vec![
3623 new_file_with_level_and_rows(0, 99, 1, 1, 100, l1_rows),
3624 new_file_with_level_and_rows(0, 9, 0, 2, 100, l0_rows),
3625 new_file_with_level_and_rows(20, 29, 0, 3, 100, l0_rows),
3626 new_file_with_level_and_rows(40, 49, 0, 4, 100, l0_rows),
3627 ];
3628
3629 let picked = pick_mixed_count_first(vec![SortedRun::from(files)], None);
3630
3631 assert_eq!(expected_len, picked.len(), "{case}");
3632 }
3633 }
3634
3635 #[test]
3636 fn test_count_first_prefers_smaller_bytes_when_file_counts_match() {
3637 let files = (0..=DEFAULT_MAX_INPUT_FILES)
3638 .map(|idx| {
3639 let start = idx as i64 * 20;
3640 let size = if idx == 0 {
3641 100
3642 } else if idx == DEFAULT_MAX_INPUT_FILES {
3643 1
3644 } else {
3645 10
3646 };
3647 new_file(start, start + 9, idx as u64 + 1, size)
3648 })
3649 .collect::<Vec<_>>();
3650
3651 let picked = pick_count_first(vec![SortedRun::from(files)], None);
3652 let ranges = picked_ranges(&picked);
3653
3654 assert_eq!(DEFAULT_MAX_INPUT_FILES, ranges.len());
3655 assert_eq!(Some(&(20, 29)), ranges.first());
3656 let last_start = DEFAULT_MAX_INPUT_FILES as i64 * 20;
3657 assert_eq!(Some(&(last_start, last_start + 9)), ranges.last());
3658 }
3659
3660 #[test]
3661 fn test_count_first_skips_pure_rewrite_without_progress() {
3662 let picked = pick_count_first(
3666 vec![SortedRun::from(vec![
3667 new_file(0, 9, 1, 600),
3668 new_file(20, 29, 2, 600),
3669 new_file(40, 49, 3, 600),
3670 new_file(60, 69, 4, 600),
3671 ])],
3672 Some(512),
3673 );
3674
3675 assert!(picked.is_empty());
3676 }
3677
3678 #[test]
3679 fn test_count_first_allows_overlap_resolution_without_file_reduction() {
3680 let picked = pick_count_first(
3684 vec![
3685 SortedRun::from(vec![new_file(0, 19, 1, 600)]),
3686 SortedRun::from(vec![new_file(10, 29, 2, 600)]),
3687 ],
3688 Some(512),
3689 );
3690
3691 assert_eq!(vec![(0, 19), (10, 29)], picked_ranges(&picked));
3692 }
3693
3694 #[test]
3695 fn test_count_first_prefers_guaranteed_reduction_over_pure_rewrite() {
3696 let picked = pick_count_first(
3700 vec![SortedRun::from(vec![
3701 new_file(0, 9, 1, 600),
3702 new_file(10, 19, 2, 600),
3703 new_file(20, 29, 3, 600),
3704 new_file(30, 39, 4, 10),
3705 new_file(40, 49, 5, 10),
3706 new_file(50, 59, 6, 10),
3707 ])],
3708 Some(512),
3709 );
3710
3711 assert_eq!(vec![(30, 39), (40, 49), (50, 59)], picked_ranges(&picked));
3712 }
3713
3714 #[test]
3715 fn test_count_first_prefers_earlier_time_on_exact_tie() {
3716 let files = (0..=DEFAULT_MAX_INPUT_FILES)
3717 .map(|idx| {
3718 let start = idx as i64 * 20;
3719 new_file(start, start + 9, idx as u64 + 1, 10)
3720 })
3721 .collect::<Vec<_>>();
3722
3723 let picked = pick_count_first(vec![SortedRun::from(files)], None);
3724 let ranges = picked_ranges(&picked);
3725
3726 assert_eq!(DEFAULT_MAX_INPUT_FILES, ranges.len());
3727 assert_eq!(Some(&(0, 9)), ranges.first());
3728 let last_start = (DEFAULT_MAX_INPUT_FILES as i64 - 1) * 20;
3729 assert_eq!(Some(&(last_start, last_start + 9)), ranges.last());
3730 }
3731
3732 #[test]
3733 fn test_count_first_keeps_each_interleaved_run_contiguous() {
3734 let picked = pick_count_first(
3735 vec![
3736 SortedRun::from(vec![
3737 new_file(0, 9, 1, 1),
3738 new_file(20, 29, 2, 1),
3739 new_file(40, 49, 3, 1),
3740 ]),
3741 SortedRun::from(vec![new_file(10, 19, 4, 1), new_file(30, 39, 5, 1)]),
3742 ],
3743 None,
3744 );
3745
3746 assert_eq!(
3747 vec![(0, 9), (10, 19), (20, 29), (30, 39), (40, 49)],
3748 picked_ranges(&picked)
3749 );
3750 }
3751
3752 #[test]
3759 fn test_count_first_candidate_spans_same_sequence_files() {
3760 let picked = pick_count_first(
3761 vec![SortedRun::from(vec![
3762 new_file(0, 9, 1, 10),
3763 new_file(10, 19, 2, 10),
3765 new_file(10, 19, 2, 10),
3766 new_file(10, 19, 2, 10),
3767 new_file(20, 29, 3, 10),
3768 ])],
3769 None,
3770 );
3771
3772 assert_eq!(
3773 vec![(0, 9), (10, 19), (10, 19), (10, 19), (20, 29)],
3774 picked_ranges(&picked)
3775 );
3776 }
3777
3778 #[tokio::test]
3784 async fn test_count_first_merges_window_with_interleaved_flush_groups() {
3785 let files = [
3786 new_file_handle_with_size_and_sequence(FileId::random(), 0, 9, 0, 1, 10),
3788 new_file_handle_with_size_and_sequence(FileId::random(), 10, 19, 0, 2, 10),
3790 new_file_handle_with_size_and_sequence(FileId::random(), 10, 19, 0, 2, 10),
3791 new_file_handle_with_size_and_sequence(FileId::random(), 10, 19, 0, 2, 10),
3792 new_file_handle_with_size_and_sequence(FileId::random(), 20, 29, 0, 3, 10),
3794 ];
3795 let windows = assign_to_windows(files.iter(), 100);
3796 let picker = TwcsPicker {
3797 trigger_file_num: 3,
3798 active_window_l1_merge_trigger: 8,
3799 inactive_window_trigger_file_num: 3,
3800 inactive_window_l1_merge_trigger: 8,
3801 time_window_seconds: Some(100),
3802 max_output_file_size: None,
3803 append_mode: false,
3804 max_background_tasks: None,
3805 time_range: None,
3806 };
3807
3808 let output = picker
3809 .build_output_with_time_range(RegionId::from_u64(1), windows, Some(0), None)
3810 .await
3811 .unwrap();
3812
3813 assert_eq!(1, output.len());
3814 assert_eq!(5, output[0].inputs.len());
3816 }
3817}