1use std::collections::hash_map::Entry;
16use std::collections::{BTreeMap, HashMap, HashSet};
17use std::fmt::Debug;
18use std::num::NonZeroU64;
19use std::sync::Arc;
20
21use common_base::readable_size::ReadableSize;
22use common_telemetry::{debug, info};
23use common_time::Timestamp;
24use common_time::range::TimestampRange;
25use common_time::timestamp::TimeUnit;
26use common_time::timestamp_millis::BucketAligned;
27use snafu::ResultExt;
28use store_api::storage::RegionId;
29
30use crate::compaction::CompactionOutput;
31use crate::compaction::buckets::infer_time_bucket;
32use crate::compaction::compactor::CompactionRegion;
33use crate::compaction::picker::{Picker, PickerOutput, get_expired_ssts};
34use crate::compaction::run::{
35 FileGroup, Item, Ranged, find_sorted_runs, find_sorted_runs_by_time_range,
36 merge_primary_key_ranges, merge_seq_files, primary_key_ranges_overlap, reduce_runs,
37};
38use crate::error::{JoinSnafu, Result};
39use crate::sst::file::{FileHandle, Level, overlaps};
40use crate::sst::version::LevelMeta;
41
42const LEVEL_COMPACTED: Level = 1;
43
44const DEFAULT_MAX_INPUT_FILE_NUM: usize = 32;
46
47#[derive(Clone, Debug)]
50pub struct TwcsPicker {
51 pub trigger_file_num: usize,
53 pub time_window_seconds: Option<i64>,
55 pub max_output_file_size: Option<u64>,
57 pub append_mode: bool,
59 pub max_background_tasks: Option<usize>,
61 pub(crate) time_range: Option<TimestampRange>,
63}
64
65impl TwcsPicker {
66 async fn build_output_with_time_range(
67 &self,
68 region_id: RegionId,
69 time_windows: BTreeMap<i64, Window>,
70 active_window: Option<i64>,
71 time_window_size: Option<i64>,
72 ) -> Result<Vec<CompactionOutput>> {
73 let mut output = vec![];
74 let windows = time_windows
75 .values()
76 .rev()
77 .filter(|window| {
78 !window.files.is_empty()
79 && self.time_range.as_ref().is_none_or(|time_range| {
80 time_window_size.is_none_or(|time_window_size| {
81 time_window_intersects_range(
82 window.time_window,
83 time_window_size,
84 time_range,
85 )
86 })
87 })
88 })
89 .map(|window| window.time_window)
90 .collect::<Vec<_>>();
91 let time_windows = Arc::new(time_windows);
92 let chunk_size = self.max_background_tasks.unwrap_or(windows.len()).max(1);
93 'chunks: for chunk in windows.chunks(chunk_size) {
94 let mut handles = Vec::with_capacity(chunk.len());
95 for window in chunk {
96 let picker = self.clone();
97 let time_windows = time_windows.clone();
98 let window = *window;
99 handles.push(common_runtime::spawn_blocking_compact(move || {
100 time_windows.get(&window).map(|window| {
101 picker.find_inputs(region_id, active_window, window, &time_windows)
102 })
103 }));
104 tokio::task::yield_now().await;
105 }
106 for result in futures::future::join_all(handles).await {
107 let Some((inputs, filter_deleted)) = result.context(JoinSnafu)? else {
108 continue;
109 };
110 if inputs.is_empty() {
111 continue;
112 }
113
114 output.push(CompactionOutput {
115 output_level: LEVEL_COMPACTED, inputs: inputs.into_iter().flat_map(|fg| fg.into_files()).collect(),
117 filter_deleted,
118 output_time_range: None, });
120
121 if let Some(max_background_tasks) = self.max_background_tasks
122 && output.len() >= max_background_tasks
123 {
124 debug!(
125 "Region ({:?}) compaction task size larger than max background tasks({}), remaining tasks discarded",
126 region_id, max_background_tasks
127 );
128 break 'chunks;
129 }
130 }
131 }
132 Ok(output)
133 }
134
135 fn find_inputs(
136 &self,
137 region_id: RegionId,
138 active_window: Option<i64>,
139 files: &Window,
140 windows: &BTreeMap<i64, Window>,
141 ) -> (Vec<FileGroup>, bool) {
142 let window = &files.time_window;
143 let mut files_to_merge: Vec<_> = files.files().cloned().collect();
144
145 if self.append_mode
147 && let Some(max_size) = self.max_output_file_size
148 {
149 let (kept_files, ignored_files) = files_to_merge
150 .into_iter()
151 .partition(|fg| fg.size() <= max_size as usize);
152 files_to_merge = kept_files;
153 if !ignored_files.is_empty() {
154 info!(
155 "Skipped {} large files in append mode for region {}, window {}, max_size: {}",
156 ignored_files.len(),
157 region_id,
158 window,
159 max_size
160 );
161 }
162 }
163
164 let sorted_runs = if files_to_merge.len() < 1024 {
165 find_sorted_runs(&mut files_to_merge)
166 } else {
167 find_sorted_runs_by_time_range(&mut files_to_merge)
168 };
169 let found_runs = sorted_runs.len();
170 let filter_deleted =
173 found_runs <= 2 && !self.append_mode && !window_has_overlap(files, windows);
174 if found_runs == 0 {
175 return (vec![], filter_deleted);
176 }
177
178 let mut inputs = if found_runs > 1 {
179 reduce_runs(sorted_runs)
180 } else {
181 let run = sorted_runs.last().unwrap();
182 if run.items().len() < self.trigger_file_num {
183 return (vec![], filter_deleted);
184 }
185 merge_seq_files(run.items(), self.max_output_file_size)
187 };
188
189 let total_input_files: usize = inputs.iter().map(|fg| fg.num_files()).sum();
191 if total_input_files > DEFAULT_MAX_INPUT_FILE_NUM {
192 inputs.sort_unstable_by_key(|fg| fg.size());
194 let mut num_picked_files = 0;
195 inputs = inputs
196 .into_iter()
197 .take_while(|fg| {
198 let current_group_file_num = fg.num_files();
199 if current_group_file_num + num_picked_files <= DEFAULT_MAX_INPUT_FILE_NUM {
200 num_picked_files += current_group_file_num;
201 true
202 } else {
203 false
204 }
205 })
206 .collect::<Vec<_>>();
207 info!(
208 "Compaction for region {} enforces max input file num limit: {}, current total: {}, input: {:?}",
209 region_id, DEFAULT_MAX_INPUT_FILE_NUM, total_input_files, inputs
210 );
211 }
212
213 if inputs.len() > 1 {
214 log_pick_result(
216 region_id,
217 *window,
218 active_window,
219 found_runs,
220 files.files.len(),
221 self.max_output_file_size,
222 filter_deleted,
223 &inputs,
224 );
225 }
226 (inputs, filter_deleted)
227 }
228}
229
230#[allow(clippy::too_many_arguments)]
231fn log_pick_result(
232 region_id: RegionId,
233 window: i64,
234 active_window: Option<i64>,
235 found_runs: usize,
236 file_num: usize,
237 max_output_file_size: Option<u64>,
238 filter_deleted: bool,
239 inputs: &[FileGroup],
240) {
241 let input_file_str: Vec<String> = inputs
242 .iter()
243 .map(|f| {
244 let range = f.range();
245 let start = range.0.to_iso8601_string();
246 let end = range.1.to_iso8601_string();
247 let num_rows = f.num_rows();
248 format!(
249 "FileGroup{{id: {:?}, range: ({}, {}), size: {}, num rows: {} }}",
250 f.file_ids(),
251 start,
252 end,
253 ReadableSize(f.size() as u64),
254 num_rows
255 )
256 })
257 .collect();
258 let window_str = Timestamp::new_second(window).to_iso8601_string();
259 let active_window_str = active_window.map(|s| Timestamp::new_second(s).to_iso8601_string());
260 let max_output_file_size = max_output_file_size.map(|size| ReadableSize(size).to_string());
261 info!(
262 "Region ({:?}) compaction pick result: current window: {}, active window: {:?}, \
263 found runs: {}, file num: {}, max output file size: {:?}, filter deleted: {}, \
264 input files: {:?}",
265 region_id,
266 window_str,
267 active_window_str,
268 found_runs,
269 file_num,
270 max_output_file_size,
271 filter_deleted,
272 input_file_str
273 );
274}
275
276#[async_trait::async_trait]
277impl Picker for TwcsPicker {
278 async fn pick(&self, compaction_region: &CompactionRegion) -> Result<Option<PickerOutput>> {
279 let region_id = compaction_region.region_id;
280 let picker = self.clone();
281 let compaction_region = compaction_region.clone();
282 let (expired_ssts, time_window_size, active_window, windows) =
283 common_runtime::spawn_blocking_compact(move || {
284 let levels = compaction_region.current_version.ssts.levels();
285 let expired_ssts = get_expired_ssts(
286 levels,
287 compaction_region.ttl,
288 Timestamp::current_millis(),
289 );
290 if !expired_ssts.is_empty() {
291 info!("Expired SSTs in region {}: {:?}", region_id, expired_ssts);
292 }
293 let expired_file_ids = expired_ssts
294 .iter()
295 .map(|file| file.file_id())
296 .collect::<HashSet<_>>();
297
298 let compaction_time_window = compaction_region
299 .current_version
300 .compaction_time_window
301 .map(|window| window.as_secs() as i64);
302 let time_window_size = compaction_time_window
303 .or(picker.time_window_seconds)
304 .unwrap_or_else(|| {
305 let inferred = infer_time_bucket(levels[0].files());
306 info!(
307 "Compaction window for region {} is not present, inferring from files: {:?}",
308 region_id, inferred
309 );
310 inferred
311 });
312
313 let active_window =
314 find_latest_window_in_seconds(levels[0].files(), time_window_size);
315 let windows = assign_to_windows(
316 levels
317 .iter()
318 .flat_map(LevelMeta::files)
319 .filter(|file| !expired_file_ids.contains(&file.file_id())),
320 time_window_size,
321 );
322
323 (expired_ssts, time_window_size, active_window, windows)
324 })
325 .await
326 .context(JoinSnafu)?;
327
328 let outputs = self
329 .build_output_with_time_range(region_id, windows, active_window, Some(time_window_size))
330 .await?;
331
332 if outputs.is_empty() && expired_ssts.is_empty() {
333 return Ok(None);
334 }
335
336 let max_file_size = self.max_output_file_size.map(|v| v as usize);
337 Ok(Some(PickerOutput {
338 outputs,
339 expired_ssts,
340 time_window_size,
341 max_file_size,
342 }))
343 }
344}
345
346#[derive(Clone)]
347struct Window {
348 start: Timestamp,
349 end: Timestamp,
350 files: HashMap<Option<NonZeroU64>, FileGroup>,
353 time_window: i64,
354 primary_key_range: Option<(bytes::Bytes, bytes::Bytes)>,
355}
356
357impl Window {
358 fn new_with_file(file: FileHandle) -> Self {
360 let (start, end) = file.time_range();
361 let primary_key_range = file.primary_key_range();
362 let files = HashMap::from([(file.meta_ref().sequence, FileGroup::new_with_file(file))]);
363 Self {
364 start,
365 end,
366 files,
367 time_window: 0,
368 primary_key_range,
369 }
370 }
371
372 fn range(&self) -> (Timestamp, Timestamp) {
374 (self.start, self.end)
375 }
376
377 fn add_file(&mut self, file: FileHandle) {
379 let (start, end) = file.time_range();
380 self.start = self.start.min(start);
381 self.end = self.end.max(end);
382 self.primary_key_range =
383 merge_primary_key_ranges(self.primary_key_range.take(), file.primary_key_range());
384
385 match self.files.entry(file.meta_ref().sequence) {
386 Entry::Occupied(mut o) => {
387 o.get_mut().add_file(file);
388 }
389 Entry::Vacant(v) => {
390 v.insert(FileGroup::new_with_file(file));
391 }
392 }
393 }
394
395 fn files(&self) -> impl Iterator<Item = &FileGroup> {
396 self.files.values()
397 }
398}
399
400fn assign_to_windows<'a>(
402 files: impl Iterator<Item = &'a FileHandle>,
403 time_window_size: i64,
404) -> BTreeMap<i64, Window> {
405 let mut windows: HashMap<i64, Window> = HashMap::new();
406 for f in files {
408 if f.compacting() {
409 continue;
410 }
411 let (_, end) = f.time_range();
412 let time_window = end
413 .convert_to(TimeUnit::Second)
414 .unwrap()
415 .value()
416 .align_to_ceil_by_bucket(time_window_size)
417 .unwrap_or(i64::MIN);
418
419 match windows.entry(time_window) {
420 Entry::Occupied(mut e) => {
421 e.get_mut().add_file(f.clone());
422 }
423 Entry::Vacant(e) => {
424 let mut window = Window::new_with_file(f.clone());
425 window.time_window = time_window;
426 e.insert(window);
427 }
428 }
429 }
430 windows.into_iter().collect()
431}
432
433fn time_window_intersects_range(
434 window_end: i64,
435 time_window_size: i64,
436 time_range: &TimestampRange,
437) -> bool {
438 let first_window = match time_range.start() {
439 None => i64::MIN,
440 Some(start) => {
441 let Some(first_window) = start
442 .convert_to(TimeUnit::Second)
443 .and_then(|timestamp| timestamp.value().align_to_ceil_by_bucket(time_window_size))
444 else {
445 return false;
446 };
447 first_window
448 }
449 };
450 let last_window = match time_range.end() {
451 None => i64::MAX,
452 Some(end) => {
453 let Some(last_window) = end
454 .convert_to_ceil(TimeUnit::Second)
455 .and_then(|timestamp| timestamp.value().checked_sub(1))
456 .and_then(|timestamp| timestamp.align_to_ceil_by_bucket(time_window_size))
457 else {
458 return false;
459 };
460 last_window
461 }
462 };
463 (first_window..=last_window).contains(&window_end)
464}
465
466fn window_has_overlap(this: &Window, windows: &BTreeMap<i64, Window>) -> bool {
467 windows
468 .values()
469 .filter(|that| this.time_window != that.time_window)
470 .any(|that| {
471 overlaps(&this.range(), &that.range()) && {
472 match (&this.primary_key_range, &that.primary_key_range) {
473 (Some(l), Some(r)) => primary_key_ranges_overlap(l, r),
474 _ => true,
475 }
476 }
477 })
478}
479
480fn find_latest_window_in_seconds<'a>(
483 files: impl Iterator<Item = &'a FileHandle>,
484 time_window_size: i64,
485) -> Option<i64> {
486 let mut latest_timestamp = None;
487 for f in files {
488 let (_, end) = f.time_range();
489 if let Some(latest) = latest_timestamp {
490 if end > latest {
491 latest_timestamp = Some(end);
492 }
493 } else {
494 latest_timestamp = Some(end);
495 }
496 }
497 latest_timestamp
498 .and_then(|ts| ts.convert_to_ceil(TimeUnit::Second))
499 .and_then(|ts| ts.value().align_to_ceil_by_bucket(time_window_size))
500}
501
502#[cfg(test)]
503mod tests {
504 use std::collections::HashSet;
505 use std::num::NonZeroU64;
506 use std::sync::Arc;
507 use std::time::Duration;
508
509 use bytes::Bytes;
510 use common_base::Plugins;
511 use common_time::range::TimestampRange;
512 use store_api::storage::FileId;
513
514 use super::*;
515 use crate::cache::CacheManager;
516 use crate::compaction::compactor::CompactionVersion;
517 use crate::compaction::test_util::{
518 new_file_handle, new_file_handle_with_sequence, new_file_handle_with_size_and_sequence,
519 new_file_handle_with_size_sequence_and_primary_key_range,
520 };
521 use crate::config::MitoConfig;
522 use crate::region::options::RegionOptions;
523 use crate::sst::file::{FileMeta, Level};
524 use crate::sst::version::SstVersion;
525 use crate::test_util::memtable_util::metadata_for_test;
526 use crate::test_util::scheduler_util::SchedulerEnv;
527
528 async fn compaction_region_with_expired_sst() -> CompactionRegion {
529 let env = SchedulerEnv::new().await;
530 let metadata = metadata_for_test();
531 let manifest_ctx = env.mock_manifest_context(metadata.clone()).await;
532 let mut ssts = SstVersion::new();
533 ssts.add_files(
534 Arc::new(crate::sst::file_purger::NoopFilePurger),
535 (1..=4).map(|sequence| FileMeta {
536 file_id: FileId::random(),
537 time_range: (
538 Timestamp::new_millisecond(0),
539 Timestamp::new_millisecond(10),
540 ),
541 level: 0,
542 sequence: NonZeroU64::new(sequence),
543 ..Default::default()
544 }),
545 );
546
547 CompactionRegion {
548 region_id: metadata.region_id,
549 region_options: RegionOptions::default(),
550 engine_config: Arc::new(MitoConfig::default()),
551 region_metadata: metadata.clone(),
552 cache_manager: Arc::new(CacheManager::default()),
553 access_layer: env.access_layer,
554 manifest_ctx,
555 current_version: CompactionVersion {
556 metadata,
557 options: RegionOptions::default(),
558 ssts: Arc::new(ssts),
559 compaction_time_window: None,
560 },
561 file_purger: None,
562 ttl: Some(Duration::from_millis(1).into()),
563 max_parallelism: 1,
564 plugins: Plugins::new(),
565 }
566 }
567
568 #[tokio::test]
569 async fn test_pick_expired_ssts_without_marking_compacting() {
570 let picker = TwcsPicker {
571 trigger_file_num: 4,
572 time_window_seconds: Some(3),
573 max_output_file_size: None,
574 append_mode: false,
575 max_background_tasks: None,
576 time_range: None,
577 };
578 let compaction_region = compaction_region_with_expired_sst().await;
579
580 let output = picker.pick(&compaction_region).await.unwrap().unwrap();
581
582 assert!(output.outputs.is_empty());
583 assert!(!output.expired_ssts.is_empty());
584 assert!(output.expired_ssts.iter().all(|file| !file.compacting()));
585 }
586
587 #[test]
588 fn test_get_latest_window_in_seconds() {
589 assert_eq!(
590 Some(1),
591 find_latest_window_in_seconds([new_file_handle(FileId::random(), 0, 999, 0)].iter(), 1)
592 );
593 assert_eq!(
594 Some(1),
595 find_latest_window_in_seconds(
596 [new_file_handle(FileId::random(), 0, 1000, 0)].iter(),
597 1
598 )
599 );
600
601 assert_eq!(
602 Some(-9223372036854000),
603 find_latest_window_in_seconds(
604 [new_file_handle(FileId::random(), i64::MIN, i64::MIN + 1, 0)].iter(),
605 3600,
606 )
607 );
608
609 assert_eq!(
610 (i64::MAX / 10000000 + 1) * 10000,
611 find_latest_window_in_seconds(
612 [new_file_handle(FileId::random(), i64::MIN, i64::MAX, 0)].iter(),
613 10000,
614 )
615 .unwrap()
616 );
617
618 assert_eq!(
619 Some((i64::MAX / 3600000 + 1) * 3600),
620 find_latest_window_in_seconds(
621 [
622 new_file_handle(FileId::random(), i64::MIN, i64::MAX, 0),
623 new_file_handle(FileId::random(), 0, 1000, 0)
624 ]
625 .iter(),
626 3600
627 )
628 );
629 }
630
631 #[test]
632 fn test_assign_to_windows() {
633 let windows = assign_to_windows(
634 [
635 new_file_handle(FileId::random(), 0, 999, 0),
636 new_file_handle(FileId::random(), 0, 999, 0),
637 new_file_handle(FileId::random(), 0, 999, 0),
638 new_file_handle(FileId::random(), 0, 999, 0),
639 new_file_handle(FileId::random(), 0, 999, 0),
640 ]
641 .iter(),
642 3,
643 );
644 let fgs = &windows.get(&0).unwrap().files;
645 assert_eq!(1, fgs.len());
646 assert_eq!(fgs.values().map(|f| f.files().len()).sum::<usize>(), 5);
647
648 let files = [FileId::random(); 3];
649 let windows = assign_to_windows(
650 [
651 new_file_handle(files[0], -2000, -3, 0),
652 new_file_handle(files[1], 0, 2999, 0),
653 new_file_handle(files[2], 50, 10001, 0),
654 ]
655 .iter(),
656 3,
657 );
658 assert_eq!(
659 files[0],
660 windows.get(&0).unwrap().files().next().unwrap().files()[0]
661 .file_id()
662 .file_id()
663 );
664 assert_eq!(
665 files[1],
666 windows.get(&3).unwrap().files().next().unwrap().files()[0]
667 .file_id()
668 .file_id()
669 );
670 assert_eq!(
671 files[2],
672 windows.get(&12).unwrap().files().next().unwrap().files()[0]
673 .file_id()
674 .file_id()
675 );
676 }
677
678 #[test]
679 fn test_assign_file_groups_to_windows() {
680 let files = [
681 FileId::random(),
682 FileId::random(),
683 FileId::random(),
684 FileId::random(),
685 ];
686 let windows = assign_to_windows(
687 [
688 new_file_handle_with_sequence(files[0], 0, 999, 0, 1),
689 new_file_handle_with_sequence(files[1], 0, 999, 0, 1),
690 new_file_handle_with_sequence(files[2], 0, 999, 0, 2),
691 new_file_handle_with_sequence(files[3], 0, 999, 0, 2),
692 ]
693 .iter(),
694 3,
695 );
696 assert_eq!(windows.len(), 1);
697 let fgs = &windows.get(&0).unwrap().files;
698 assert_eq!(2, fgs.len());
699 assert_eq!(
700 fgs.get(&NonZeroU64::new(1))
701 .unwrap()
702 .files()
703 .iter()
704 .map(|f| f.file_id().file_id())
705 .collect::<HashSet<_>>(),
706 [files[0], files[1]].into_iter().collect()
707 );
708 assert_eq!(
709 fgs.get(&NonZeroU64::new(2))
710 .unwrap()
711 .files()
712 .iter()
713 .map(|f| f.file_id().file_id())
714 .collect::<HashSet<_>>(),
715 [files[2], files[3]].into_iter().collect()
716 );
717 }
718
719 #[test]
720 fn test_assign_compacting_to_windows() {
721 let files = [
722 new_file_handle(FileId::random(), 0, 999, 0),
723 new_file_handle(FileId::random(), 0, 999, 0),
724 new_file_handle(FileId::random(), 0, 999, 0),
725 new_file_handle(FileId::random(), 0, 999, 0),
726 new_file_handle(FileId::random(), 0, 999, 0),
727 ];
728 files[0].set_compacting(true);
729 files[2].set_compacting(true);
730 let mut windows = assign_to_windows(files.iter(), 3);
731 let window0 = windows.remove(&0).unwrap();
732 assert_eq!(1, window0.files.len());
733 let candidates = window0
734 .files
735 .into_values()
736 .flat_map(|fg| fg.into_files())
737 .map(|f| f.file_id().file_id())
738 .collect::<HashSet<_>>();
739 assert_eq!(candidates.len(), 3);
740 assert_eq!(
741 candidates,
742 [
743 files[1].file_id().file_id(),
744 files[3].file_id().file_id(),
745 files[4].file_id().file_id()
746 ]
747 .into_iter()
748 .collect::<HashSet<_>>()
749 );
750 }
751
752 type ExpectedWindowSpec = (i64, bool, Vec<(i64, i64)>);
754
755 fn pk_range(min: &'static [u8], max: &'static [u8]) -> Option<(Bytes, Bytes)> {
756 Some((Bytes::from_static(min), Bytes::from_static(max)))
757 }
758
759 fn check_assign_to_windows_with_overlapping(
760 file_time_ranges: &[(i64, i64)],
761 time_window: i64,
762 expected_files: &[ExpectedWindowSpec],
763 ) {
764 let files: Vec<_> = (0..file_time_ranges.len())
765 .map(|_| FileId::random())
766 .collect();
767
768 let file_handles = files
769 .iter()
770 .zip(file_time_ranges.iter())
771 .map(|(file_id, range)| new_file_handle(*file_id, range.0, range.1, 0))
772 .collect::<Vec<_>>();
773
774 let windows = assign_to_windows(file_handles.iter(), time_window);
775
776 for (expected_window, overlapping, window_files) in expected_files {
777 let actual_window = windows.get(expected_window).unwrap();
778 let actual_overlapping = window_has_overlap(actual_window, &windows);
779 assert_eq!(*overlapping, actual_overlapping);
780 let mut file_ranges = actual_window
781 .files
782 .values()
783 .flat_map(|f| {
784 f.files().iter().map(|f| {
785 let (s, e) = f.time_range();
786 (s.value(), e.value())
787 })
788 })
789 .collect::<Vec<_>>();
790 file_ranges.sort_unstable_by(|l, r| l.0.cmp(&r.0).then(l.1.cmp(&r.1)));
791 assert_eq!(window_files, &file_ranges);
792 }
793 }
794
795 #[test]
796 fn test_assign_to_windows_with_overlapping() {
797 check_assign_to_windows_with_overlapping(
798 &[(0, 999), (1000, 1999), (2000, 2999)],
799 2,
800 &[
801 (0, false, vec![(0, 999)]),
802 (2, false, vec![(1000, 1999), (2000, 2999)]),
803 ],
804 );
805
806 check_assign_to_windows_with_overlapping(
807 &[(0, 1), (0, 999), (100, 2999)],
808 2,
809 &[
810 (0, true, vec![(0, 1), (0, 999)]),
811 (2, true, vec![(100, 2999)]),
812 ],
813 );
814
815 check_assign_to_windows_with_overlapping(
816 &[(0, 999), (1000, 1999), (2000, 2999), (3000, 3999)],
817 2,
818 &[
819 (0, false, vec![(0, 999)]),
820 (2, false, vec![(1000, 1999), (2000, 2999)]),
821 (4, false, vec![(3000, 3999)]),
822 ],
823 );
824
825 check_assign_to_windows_with_overlapping(
826 &[
827 (0, 999),
828 (1000, 1999),
829 (2000, 2999),
830 (3000, 3999),
831 (0, 3999),
832 ],
833 2,
834 &[
835 (0, true, vec![(0, 999)]),
836 (2, true, vec![(1000, 1999), (2000, 2999)]),
837 (4, true, vec![(0, 3999), (3000, 3999)]),
838 ],
839 );
840
841 check_assign_to_windows_with_overlapping(
842 &[
843 (0, 999),
844 (1000, 1999),
845 (2000, 2999),
846 (3000, 3999),
847 (1999, 3999),
848 ],
849 2,
850 &[
851 (0, false, vec![(0, 999)]),
852 (2, true, vec![(1000, 1999), (2000, 2999)]),
853 (4, true, vec![(1999, 3999), (3000, 3999)]),
854 ],
855 );
856
857 check_assign_to_windows_with_overlapping(
858 &[
859 (0, 999), (1000, 1999), (2000, 2999), (3000, 3999), (2999, 3999), ],
865 2,
866 &[
867 (0, false, vec![(0, 999)]),
869 (2, true, vec![(1000, 1999), (2000, 2999)]),
870 (4, true, vec![(2999, 3999), (3000, 3999)]),
871 ],
872 );
873
874 check_assign_to_windows_with_overlapping(
875 &[
876 (0, 999), (1000, 1999), (2000, 2999), (3000, 3999), (0, 1000), ],
882 2,
883 &[
884 (0, true, vec![(0, 999)]),
886 (2, true, vec![(0, 1000), (1000, 1999), (2000, 2999)]),
887 (4, false, vec![(3000, 3999)]),
888 ],
889 );
890 }
891
892 #[test]
893 fn test_assign_to_windows_not_overlapping_when_pk_disjoint() {
894 let files = [
895 new_file_handle_with_size_sequence_and_primary_key_range(
896 FileId::random(),
897 0,
898 1000,
899 0,
900 1,
901 10,
902 pk_range(b"a", b"f"),
903 ),
904 new_file_handle_with_size_sequence_and_primary_key_range(
905 FileId::random(),
906 500,
907 1999,
908 0,
909 2,
910 10,
911 pk_range(b"x", b"z"),
912 ),
913 ];
914
915 let windows = assign_to_windows(files.iter(), 2);
916
917 let overlapping = window_has_overlap(windows.get(&2).unwrap(), &windows);
918 assert!(!overlapping);
919 }
920
921 #[test]
922 fn test_assign_to_windows_pk_unknown_in_earlier_window_does_not_poison_later_windows() {
923 let files = [
924 new_file_handle(FileId::random(), 0, 1999, 0),
925 new_file_handle_with_size_sequence_and_primary_key_range(
926 FileId::random(),
927 2000,
928 3999,
929 0,
930 1,
931 10,
932 pk_range(b"a", b"f"),
933 ),
934 new_file_handle_with_size_sequence_and_primary_key_range(
935 FileId::random(),
936 3000,
937 4999,
938 0,
939 2,
940 10,
941 pk_range(b"x", b"z"),
942 ),
943 ];
944
945 let windows = assign_to_windows(files.iter(), 2);
946
947 let overlapping = window_has_overlap(windows.get(&4).unwrap(), &windows);
948 assert!(!overlapping);
949 }
950
951 struct CompactionPickerTestCase {
952 window_size: i64,
953 input_files: Vec<FileHandle>,
954 expected_outputs: Vec<ExpectedOutput>,
955 }
956
957 impl CompactionPickerTestCase {
958 async fn check(&self) {
959 let file_id_to_idx = self
960 .input_files
961 .iter()
962 .enumerate()
963 .map(|(idx, file)| (file.file_id(), idx))
964 .collect::<HashMap<_, _>>();
965 let windows = assign_to_windows(self.input_files.iter(), self.window_size);
966 let active_window =
967 find_latest_window_in_seconds(self.input_files.iter(), self.window_size);
968 let output = TwcsPicker {
969 trigger_file_num: 4,
970 time_window_seconds: None,
971 max_output_file_size: None,
972 append_mode: false,
973 max_background_tasks: None,
974 time_range: None,
975 }
976 .build_output_with_time_range(RegionId::from_u64(0), windows, active_window, None)
977 .await
978 .unwrap();
979
980 let output = output
981 .iter()
982 .map(|o| {
983 let input_file_ids = o
984 .inputs
985 .iter()
986 .map(|f| file_id_to_idx.get(&f.file_id()).copied().unwrap())
987 .collect::<HashSet<_>>();
988 (input_file_ids, o.output_level)
989 })
990 .collect::<Vec<_>>();
991
992 let expected = self
993 .expected_outputs
994 .iter()
995 .map(|o| {
996 let input_file_ids = o.input_files.iter().copied().collect::<HashSet<_>>();
997 (input_file_ids, o.output_level)
998 })
999 .collect::<Vec<_>>();
1000 assert_eq!(expected, output);
1001 }
1002 }
1003
1004 struct ExpectedOutput {
1005 input_files: Vec<usize>,
1006 output_level: Level,
1007 }
1008
1009 #[tokio::test]
1010 async fn test_build_twcs_output() {
1011 let file_ids = (0..4).map(|_| FileId::random()).collect::<Vec<_>>();
1012
1013 CompactionPickerTestCase {
1015 window_size: 3,
1016 input_files: [
1017 new_file_handle_with_sequence(file_ids[0], -2000, -3, 0, 1),
1018 new_file_handle_with_sequence(file_ids[1], -3000, -100, 0, 2),
1019 new_file_handle_with_sequence(file_ids[2], 0, 2999, 0, 3), new_file_handle_with_sequence(file_ids[3], 50, 2998, 0, 4), ]
1022 .to_vec(),
1023 expected_outputs: vec![
1024 ExpectedOutput {
1025 input_files: vec![2, 3],
1026 output_level: 1,
1027 },
1028 ExpectedOutput {
1029 input_files: vec![0, 1],
1030 output_level: 1,
1031 },
1032 ],
1033 }
1034 .check()
1035 .await;
1036
1037 let file_ids = (0..6).map(|_| FileId::random()).collect::<Vec<_>>();
1044 CompactionPickerTestCase {
1045 window_size: 3,
1046 input_files: [
1047 new_file_handle_with_sequence(file_ids[0], -2000, -3, 0, 1),
1048 new_file_handle_with_sequence(file_ids[1], -3000, -100, 0, 2),
1049 new_file_handle_with_sequence(file_ids[2], 0, 2999, 0, 3),
1050 new_file_handle_with_sequence(file_ids[3], 50, 2998, 0, 4),
1051 new_file_handle_with_sequence(file_ids[4], 11, 2990, 0, 5),
1052 ]
1053 .to_vec(),
1054 expected_outputs: vec![
1055 ExpectedOutput {
1056 input_files: vec![2, 4],
1057 output_level: 1,
1058 },
1059 ExpectedOutput {
1060 input_files: vec![0, 1],
1061 output_level: 1,
1062 },
1063 ],
1064 }
1065 .check()
1066 .await;
1067
1068 let file_ids = (0..6).map(|_| FileId::random()).collect::<Vec<_>>();
1072 CompactionPickerTestCase {
1073 window_size: 3,
1074 input_files: [
1075 new_file_handle_with_sequence(file_ids[0], 0, 2999, 1, 1),
1076 new_file_handle_with_sequence(file_ids[1], 0, 2998, 1, 1),
1077 new_file_handle_with_sequence(file_ids[2], 3000, 5999, 1, 2),
1078 new_file_handle_with_sequence(file_ids[3], 3000, 5000, 1, 2),
1079 new_file_handle_with_sequence(file_ids[4], 11, 2990, 0, 3),
1080 ]
1081 .to_vec(),
1082 expected_outputs: vec![ExpectedOutput {
1083 input_files: vec![0, 1, 4],
1084 output_level: 1,
1085 }],
1086 }
1087 .check()
1088 .await;
1089 }
1090
1091 #[tokio::test]
1092 async fn test_build_output_skips_pk_disjoint_files() {
1093 let files = [
1094 new_file_handle_with_size_sequence_and_primary_key_range(
1095 FileId::random(),
1096 0,
1097 2999,
1098 0,
1099 1,
1100 10,
1101 pk_range(b"a", b"f"),
1102 ),
1103 new_file_handle_with_size_sequence_and_primary_key_range(
1104 FileId::random(),
1105 50,
1106 2998,
1107 0,
1108 2,
1109 10,
1110 pk_range(b"x", b"z"),
1111 ),
1112 ];
1113 let windows = assign_to_windows(files.iter(), 3);
1114 let active_window = find_latest_window_in_seconds(files.iter(), 3);
1115 let output = TwcsPicker {
1116 trigger_file_num: 4,
1117 time_window_seconds: None,
1118 max_output_file_size: None,
1119 append_mode: false,
1120 max_background_tasks: None,
1121 time_range: None,
1122 }
1123 .build_output_with_time_range(RegionId::from_u64(0), windows, active_window, None)
1124 .await
1125 .unwrap();
1126
1127 assert!(output.is_empty());
1128 }
1129
1130 #[test]
1131 fn test_append_mode_filter_large_files() {
1132 let file_ids = (0..4).map(|_| FileId::random()).collect::<Vec<_>>();
1133 let max_output_file_size = 1000u64;
1134
1135 let small_file_1 = new_file_handle_with_size_and_sequence(file_ids[0], 0, 999, 0, 1, 500);
1137 let large_file_1 = new_file_handle_with_size_and_sequence(file_ids[1], 0, 999, 0, 2, 1500);
1138 let small_file_2 = new_file_handle_with_size_and_sequence(file_ids[2], 0, 999, 0, 3, 800);
1139 let large_file_2 = new_file_handle_with_size_and_sequence(file_ids[3], 0, 999, 0, 4, 2000);
1140
1141 let mut files_to_merge = vec![
1143 FileGroup::new_with_file(small_file_1),
1144 FileGroup::new_with_file(large_file_1),
1145 FileGroup::new_with_file(small_file_2),
1146 FileGroup::new_with_file(large_file_2),
1147 ];
1148
1149 let original_count = files_to_merge.len();
1151
1152 files_to_merge.retain(|fg| fg.size() <= max_output_file_size as usize);
1154
1155 assert_eq!(files_to_merge.len(), 2);
1157 assert_eq!(original_count, 4);
1158
1159 for fg in &files_to_merge {
1161 assert!(
1162 fg.size() <= max_output_file_size as usize,
1163 "File size {} should be <= {}",
1164 fg.size(),
1165 max_output_file_size
1166 );
1167 }
1168 }
1169
1170 #[tokio::test]
1171 async fn test_build_output_multiple_windows_with_zero_runs() {
1172 let file_ids = (0..6).map(|_| FileId::random()).collect::<Vec<_>>();
1173
1174 let files = [
1175 new_file_handle_with_sequence(file_ids[0], 0, 999, 0, 1),
1177 new_file_handle_with_sequence(file_ids[1], 0, 999, 0, 2),
1178 new_file_handle_with_sequence(file_ids[2], 0, 999, 0, 3),
1179 new_file_handle_with_sequence(file_ids[3], 3000, 3999, 0, 4),
1181 new_file_handle_with_sequence(file_ids[4], 3000, 3999, 0, 5),
1182 new_file_handle_with_sequence(file_ids[5], 3000, 3999, 0, 6),
1183 ];
1184
1185 let windows = assign_to_windows(files.iter(), 3);
1186
1187 let picker = TwcsPicker {
1189 trigger_file_num: 4, time_window_seconds: Some(3),
1191 max_output_file_size: None,
1192 append_mode: false,
1193 max_background_tasks: None,
1194 time_range: None,
1195 };
1196
1197 let active_window = find_latest_window_in_seconds(files.iter(), 3);
1198 let output = picker
1199 .build_output_with_time_range(RegionId::from_u64(123), windows, active_window, None)
1200 .await
1201 .unwrap();
1202
1203 assert!(
1204 !output.is_empty(),
1205 "Should have output from windows with runs, even when one window has 0 runs"
1206 );
1207
1208 let all_output_files: Vec<_> = output
1209 .iter()
1210 .flat_map(|o| o.inputs.iter())
1211 .map(|f| f.file_id().file_id())
1212 .collect();
1213
1214 assert!(
1215 all_output_files.contains(&file_ids[3])
1216 || all_output_files.contains(&file_ids[4])
1217 || all_output_files.contains(&file_ids[5]),
1218 "Output should contain files from the window with runs"
1219 );
1220 }
1221
1222 #[tokio::test]
1223 async fn test_build_output_single_window_zero_runs() {
1224 let file_ids = (0..2).map(|_| FileId::random()).collect::<Vec<_>>();
1225
1226 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];
1230
1231 let windows = assign_to_windows(files.iter(), 3);
1232
1233 let picker = TwcsPicker {
1234 trigger_file_num: 2,
1235 time_window_seconds: Some(3),
1236 max_output_file_size: Some(1000),
1237 append_mode: true,
1238 max_background_tasks: None,
1239 time_range: None,
1240 };
1241
1242 let active_window = find_latest_window_in_seconds(files.iter(), 3);
1243 let output = picker
1244 .build_output_with_time_range(RegionId::from_u64(456), windows, active_window, None)
1245 .await
1246 .unwrap();
1247
1248 assert!(
1250 output.is_empty(),
1251 "Should return empty output when no runs are found after filtering"
1252 );
1253 }
1254
1255 #[tokio::test]
1256 async fn test_max_background_tasks_truncation() {
1257 let file_ids = (0..10).map(|_| FileId::random()).collect::<Vec<_>>();
1258 let max_background_tasks = 3;
1259
1260 let files = [
1262 new_file_handle_with_sequence(file_ids[0], 0, 999, 0, 1),
1264 new_file_handle_with_sequence(file_ids[1], 0, 999, 0, 2),
1265 new_file_handle_with_sequence(file_ids[2], 0, 999, 0, 3),
1266 new_file_handle_with_sequence(file_ids[3], 0, 999, 0, 4),
1267 new_file_handle_with_sequence(file_ids[4], 3000, 3999, 0, 5),
1269 new_file_handle_with_sequence(file_ids[5], 3000, 3999, 0, 6),
1270 new_file_handle_with_sequence(file_ids[6], 3000, 3999, 0, 7),
1271 new_file_handle_with_sequence(file_ids[7], 3000, 3999, 0, 8),
1272 new_file_handle_with_sequence(file_ids[8], 6000, 6999, 0, 9),
1274 new_file_handle_with_sequence(file_ids[9], 6000, 6999, 0, 10),
1275 ];
1276
1277 let windows = assign_to_windows(files.iter(), 3);
1278
1279 let picker = TwcsPicker {
1280 trigger_file_num: 4,
1281 time_window_seconds: Some(3),
1282 max_output_file_size: None,
1283 append_mode: false,
1284 max_background_tasks: Some(max_background_tasks),
1285 time_range: None,
1286 };
1287
1288 let active_window = find_latest_window_in_seconds(files.iter(), 3);
1289 let output = picker
1290 .build_output_with_time_range(RegionId::from_u64(123), windows, active_window, None)
1291 .await
1292 .unwrap();
1293
1294 assert!(
1296 output.len() <= max_background_tasks,
1297 "Output should be truncated to max_background_tasks: expected <= {}, got {}",
1298 max_background_tasks,
1299 output.len()
1300 );
1301
1302 let picker_no_limit = TwcsPicker {
1304 trigger_file_num: 4,
1305 time_window_seconds: Some(3),
1306 max_output_file_size: None,
1307 append_mode: false,
1308 max_background_tasks: None,
1309 time_range: None,
1310 };
1311
1312 let windows_no_limit = assign_to_windows(files.iter(), 3);
1313 let output_no_limit = picker_no_limit
1314 .build_output_with_time_range(
1315 RegionId::from_u64(123),
1316 windows_no_limit,
1317 active_window,
1318 None,
1319 )
1320 .await
1321 .unwrap();
1322
1323 if output_no_limit.len() > max_background_tasks {
1325 assert!(
1326 output_no_limit.len() > output.len(),
1327 "Without limit should have more outputs than with limit"
1328 );
1329 }
1330 }
1331
1332 #[tokio::test]
1333 async fn test_max_background_tasks_no_truncation_when_under_limit() {
1334 let file_ids = (0..4).map(|_| FileId::random()).collect::<Vec<_>>();
1335 let max_background_tasks = 10; let files = [
1339 new_file_handle_with_sequence(file_ids[0], 0, 999, 0, 1),
1340 new_file_handle_with_sequence(file_ids[1], 0, 999, 0, 2),
1341 new_file_handle_with_sequence(file_ids[2], 0, 999, 0, 3),
1342 new_file_handle_with_sequence(file_ids[3], 0, 999, 0, 4),
1343 ];
1344
1345 let windows = assign_to_windows(files.iter(), 3);
1346
1347 let picker = TwcsPicker {
1348 trigger_file_num: 4,
1349 time_window_seconds: Some(3),
1350 max_output_file_size: None,
1351 append_mode: false,
1352 max_background_tasks: Some(max_background_tasks),
1353 time_range: None,
1354 };
1355
1356 let active_window = find_latest_window_in_seconds(files.iter(), 3);
1357 let output = picker
1358 .build_output_with_time_range(RegionId::from_u64(123), windows, active_window, None)
1359 .await
1360 .unwrap();
1361
1362 assert!(
1364 output.len() <= max_background_tasks,
1365 "Output should be within limit"
1366 );
1367 assert!(!output.is_empty(), "Should have at least one output");
1369 }
1370
1371 #[tokio::test]
1372 async fn test_pick_multiple_runs() {
1373 common_telemetry::init_default_ut_logging();
1374
1375 let num_files = 8;
1376 let file_ids = (0..num_files).map(|_| FileId::random()).collect::<Vec<_>>();
1377
1378 let files: Vec<_> = file_ids
1380 .iter()
1381 .enumerate()
1382 .map(|(idx, file_id)| {
1383 new_file_handle_with_size_and_sequence(
1384 *file_id,
1385 0,
1386 999,
1387 0,
1388 (idx + 1) as u64,
1389 1024 * 1024,
1390 )
1391 })
1392 .collect();
1393
1394 let windows = assign_to_windows(files.iter(), 3);
1395
1396 let picker = TwcsPicker {
1397 trigger_file_num: 4,
1398 time_window_seconds: Some(3),
1399 max_output_file_size: None,
1400 append_mode: false,
1401 max_background_tasks: None,
1402 time_range: None,
1403 };
1404
1405 let active_window = find_latest_window_in_seconds(files.iter(), 3);
1406 let output = picker
1407 .build_output_with_time_range(RegionId::from_u64(123), windows, active_window, None)
1408 .await
1409 .unwrap();
1410
1411 assert_eq!(1, output.len());
1412 assert_eq!(output[0].inputs.len(), 2);
1413 }
1414
1415 #[tokio::test]
1416 async fn test_limit_max_input_files() {
1417 common_telemetry::init_default_ut_logging();
1418
1419 let num_files = 50;
1420 let file_ids = (0..num_files).map(|_| FileId::random()).collect::<Vec<_>>();
1421
1422 let files: Vec<_> = file_ids
1424 .iter()
1425 .enumerate()
1426 .map(|(idx, file_id)| {
1427 new_file_handle_with_size_and_sequence(
1428 *file_id,
1429 (idx / 2 * 10) as i64,
1430 (idx / 2 * 10 + 5) as i64,
1431 0,
1432 (idx + 1) as u64,
1433 1024 * 1024,
1434 )
1435 })
1436 .collect();
1437
1438 let windows = assign_to_windows(files.iter(), 3);
1439
1440 let picker = TwcsPicker {
1441 trigger_file_num: 4,
1442 time_window_seconds: Some(3),
1443 max_output_file_size: None,
1444 append_mode: false,
1445 max_background_tasks: None,
1446 time_range: None,
1447 };
1448
1449 let active_window = find_latest_window_in_seconds(files.iter(), 3);
1450 let output = picker
1451 .build_output_with_time_range(RegionId::from_u64(123), windows, active_window, None)
1452 .await
1453 .unwrap();
1454
1455 assert_eq!(1, output.len());
1456 assert_eq!(output[0].inputs.len(), 32);
1457 }
1458
1459 #[tokio::test]
1460 async fn test_newer_windows_have_priority() {
1461 let older_file_ids = [FileId::random(), FileId::random()];
1462 let newer_file_ids = [FileId::random(), FileId::random()];
1463 let files = [
1464 new_file_handle_with_sequence(older_file_ids[0], 1_000, 1_999, 0, 1),
1465 new_file_handle_with_sequence(older_file_ids[1], 1_000, 1_999, 0, 2),
1466 new_file_handle_with_sequence(newer_file_ids[0], 7_000, 7_999, 0, 3),
1467 new_file_handle_with_sequence(newer_file_ids[1], 7_000, 7_999, 0, 4),
1468 ];
1469 let windows = assign_to_windows(files.iter(), 3);
1470 let picker = TwcsPicker {
1471 trigger_file_num: 2,
1472 time_window_seconds: Some(3),
1473 max_output_file_size: None,
1474 append_mode: false,
1475 max_background_tasks: Some(1),
1476 time_range: None,
1477 };
1478
1479 let output = picker
1480 .build_output_with_time_range(RegionId::from_u64(123), windows, Some(9), None)
1481 .await
1482 .unwrap();
1483
1484 assert_eq!(1, output.len());
1485 assert_eq!(
1486 newer_file_ids.into_iter().collect::<HashSet<_>>(),
1487 output[0]
1488 .inputs
1489 .iter()
1490 .map(|file| file.file_id().file_id())
1491 .collect::<HashSet<_>>()
1492 );
1493 }
1494
1495 #[test]
1496 fn test_filter_time_windows_by_time_range() {
1497 let time_range = TimestampRange::new(
1498 Timestamp::new_millisecond(1_200),
1499 Timestamp::new_millisecond(1_800),
1500 )
1501 .unwrap();
1502
1503 assert!(time_window_intersects_range(3, 3, &time_range));
1504 assert!(!time_window_intersects_range(9, 3, &time_range));
1505
1506 let boundary_range =
1507 TimestampRange::new(Timestamp::new_second(0), Timestamp::new_second(3)).unwrap();
1508 assert!(time_window_intersects_range(0, 3, &boundary_range));
1509 assert!(time_window_intersects_range(3, 3, &boundary_range));
1510 assert!(!time_window_intersects_range(6, 3, &boundary_range));
1511
1512 let overflowing_range = TimestampRange::new(
1513 Timestamp::new_second(i64::MAX - 1),
1514 Timestamp::new_second(i64::MAX),
1515 )
1516 .unwrap();
1517 assert!(!time_window_intersects_range(0, 4, &overflowing_range));
1518 }
1519
1520 #[tokio::test]
1521 async fn test_time_range_filter_precedes_background_task_limit() {
1522 let early_file_ids = [FileId::random(), FileId::random()];
1523 let selected_file_ids = [FileId::random(), FileId::random()];
1524 let files = [
1525 new_file_handle_with_sequence(early_file_ids[0], 1_000, 1_999, 0, 1),
1526 new_file_handle_with_sequence(early_file_ids[1], 1_000, 1_999, 0, 2),
1527 new_file_handle_with_sequence(selected_file_ids[0], 7_000, 7_999, 0, 3),
1528 new_file_handle_with_sequence(selected_file_ids[1], 7_000, 7_999, 0, 4),
1529 ];
1530 let windows = assign_to_windows(files.iter(), 3);
1531 let picker = TwcsPicker {
1532 trigger_file_num: 2,
1533 time_window_seconds: Some(3),
1534 max_output_file_size: None,
1535 append_mode: false,
1536 max_background_tasks: Some(1),
1537 time_range: TimestampRange::new(
1538 Timestamp::new_millisecond(7_200),
1539 Timestamp::new_millisecond(7_800),
1540 ),
1541 };
1542
1543 let output = picker
1544 .build_output_with_time_range(RegionId::from_u64(123), windows, Some(9), Some(3))
1545 .await
1546 .unwrap();
1547
1548 assert_eq!(1, output.len());
1549 assert_eq!(
1550 selected_file_ids.into_iter().collect::<HashSet<_>>(),
1551 output[0]
1552 .inputs
1553 .iter()
1554 .map(|file| file.file_id().file_id())
1555 .collect::<HashSet<_>>()
1556 );
1557 }
1558
1559 }