1use std::collections::{HashMap, HashSet};
16
17use snafu::ResultExt;
18use store_api::metadata::RegionMetadataRef;
19use store_api::storage::SequenceNumber;
20
21use crate::compaction::CompactionOutput;
22use crate::compaction::compactor::CompactionRegion;
23use crate::compaction::overlap::FileOverlapIndex;
24use crate::compaction::picker::{Picker, PickerOutput};
25use crate::compaction::twcs::TwcsPicker;
26use crate::error::{JoinSnafu, Result};
27use crate::sst::file::{FileHandle, RegionFileId};
28
29#[derive(Debug)]
45pub(super) struct LastNonNullPicker {
46 base_picker: TwcsPicker,
47}
48
49impl LastNonNullPicker {
50 pub(super) fn new(base_picker: TwcsPicker) -> Self {
51 Self { base_picker }
52 }
53}
54
55#[async_trait::async_trait]
56impl Picker for LastNonNullPicker {
57 async fn pick(&self, region: &CompactionRegion) -> Result<Option<PickerOutput>> {
58 let max_outputs = self.base_picker.max_background_tasks;
59
60 let Some(all_seeds) = self
62 .base_picker
63 .pick_with_output_limit(region, None)
64 .await?
65 else {
66 return Ok(None);
67 };
68 let (output, _) = close_outputs(region, all_seeds, max_outputs).await?;
69 Ok(output)
70 }
71}
72
73pub(crate) async fn close_outputs(
77 region: &CompactionRegion,
78 mut all_seeds: PickerOutput,
79 max_outputs: Option<usize>,
80) -> Result<(Option<PickerOutput>, Vec<usize>)> {
81 let version = region.current_version.clone();
82 common_runtime::spawn_blocking_compact(move || {
83 let expired: HashSet<_> = all_seeds
84 .expired_ssts
85 .iter()
86 .map(FileHandle::file_id)
87 .collect();
88 let all_files = version
91 .ssts
92 .levels()
93 .iter()
94 .flat_map(|level| level.files())
95 .filter(|file| !expired.contains(&file.file_id()))
96 .cloned()
97 .collect();
98 let (outputs, attempted_seeds) = build_closed_outputs(
99 all_seeds.outputs,
100 all_files,
101 &version.metadata,
102 ClosureLimits {
103 max_outputs,
104 memtable_min_sequence: version.memtable_min_sequence,
105 },
106 );
107 all_seeds.outputs = outputs;
108 let output = (!all_seeds.outputs.is_empty() || !all_seeds.expired_ssts.is_empty())
109 .then_some(all_seeds);
110 (output, attempted_seeds)
111 })
112 .await
113 .context(JoinSnafu)
114}
115
116pub(super) fn inputs_precede_memtables(
119 inputs: &[FileHandle],
120 memtable_min_sequence: Option<SequenceNumber>,
121) -> bool {
122 memtable_min_sequence.is_none_or(|min_sequence| {
123 inputs.iter().all(|file| {
124 file.meta_ref()
125 .sequence
126 .is_some_and(|max_sequence| max_sequence.get() < min_sequence)
127 })
128 })
129}
130
131#[derive(Default)]
133struct ClosureLimits {
134 max_outputs: Option<usize>,
135 memtable_min_sequence: Option<SequenceNumber>,
136}
137
138fn build_closed_outputs(
142 seeds: Vec<CompactionOutput>,
143 files: Vec<FileHandle>,
144 metadata: &RegionMetadataRef,
145 limits: ClosureLimits,
146) -> (Vec<CompactionOutput>, Vec<usize>) {
147 let seed_by_file: HashMap<_, _> = seeds
150 .iter()
151 .enumerate()
152 .flat_map(|(i, seed)| seed.inputs.iter().map(move |file| (file.file_id(), i)))
153 .collect();
154 let mut seeds: Vec<_> = seeds.into_iter().map(Some).collect();
156 let mut remaining = FileOverlapIndex::new(files, metadata);
157 let mut outputs = Vec::new();
158 for seed_index in (0..seeds.len()).rev() {
160 if outputs.len() >= limits.max_outputs.unwrap_or(usize::MAX) {
163 break;
164 }
165 let Some(output) = seeds[seed_index].take() else {
166 continue;
167 };
168 let output = expand_closure(output, &mut seeds, &seed_by_file, &mut remaining);
169 if !output.inputs.iter().any(FileHandle::compacting)
171 && inputs_precede_memtables(&output.inputs, limits.memtable_min_sequence)
172 {
173 outputs.push(output);
174 }
175 }
176 outputs.reverse();
177 let attempted_seeds = seeds
178 .iter()
179 .enumerate()
180 .filter_map(|(index, seed)| seed.is_none().then_some(index))
181 .collect();
182 (outputs, attempted_seeds)
183}
184
185fn expand_closure(
189 mut output: CompactionOutput,
190 seeds: &mut [Option<CompactionOutput>],
191 seed_by_file: &HashMap<RegionFileId, usize>,
192 remaining_files: &mut FileOverlapIndex<'_>,
193) -> CompactionOutput {
194 let mut selected = HashSet::new();
195 let inputs = std::mem::take(&mut output.inputs);
196 extend_inputs(&mut output, inputs, &mut selected, remaining_files);
197
198 let mut cursor = 0;
199 while cursor < output.inputs.len() {
200 let input = output.inputs[cursor].clone();
201 if let Some(&seed_index) = seed_by_file.get(&input.file_id())
202 && let Some(seed) = seeds[seed_index].take()
203 {
204 output.filter_deleted &= seed.filter_deleted;
206 extend_inputs(&mut output, seed.inputs, &mut selected, remaining_files);
207 }
208 let dependencies = remaining_files.drain_overlaps(&input);
209 extend_inputs(&mut output, dependencies, &mut selected, remaining_files);
210 cursor += 1;
211 }
212 output
213}
214
215fn extend_inputs(
218 output: &mut CompactionOutput,
219 inputs: Vec<FileHandle>,
220 selected: &mut HashSet<RegionFileId>,
221 remaining: &mut FileOverlapIndex<'_>,
222) {
223 for input in inputs {
224 if selected.insert(input.file_id()) {
225 remaining.remove(input.file_id());
226 output.inputs.push(input);
227 }
228 }
229}
230
231#[cfg(test)]
232mod tests {
233 use std::time::{Duration, Instant};
234
235 use api::v1::region::compact_request;
236 use common_time::Timestamp;
237 use common_time::range::TimestampRange;
238 use store_api::storage::FileId;
239
240 use super::*;
241 use crate::compaction::picker::new_picker;
242 use crate::compaction::scheduler::history::{PickHistory, PickKey};
243 use crate::compaction::scheduler::pick_automatic_for_test;
244 use crate::compaction::test_util::{
245 compaction_region_with_ssts, new_file_handle, new_file_handle_with_size_and_sequence,
246 };
247 use crate::compaction::twcs::PickClass;
248 use crate::region::options::{CompactionOptions, MergeMode};
249 use crate::sst::file::{FileMeta, RegionFileId};
250 use crate::test_util::memtable_util::metadata_for_test;
251
252 fn seed(inputs: Vec<FileHandle>) -> CompactionOutput {
253 CompactionOutput {
254 output_level: 1,
255 inputs,
256 filter_deleted: false,
257 output_time_range: None,
258 }
259 }
260
261 fn file_ids(files: &[FileHandle]) -> HashSet<RegionFileId> {
262 files.iter().map(FileHandle::file_id).collect()
263 }
264
265 #[rstest::rstest]
266 #[case::memtable_barrier(2, Some(20), false, Some(1), vec![vec![1]])]
267 #[case::busy_dependency(2, None, true, Some(1), vec![vec![1]])]
268 #[case::highest_priority(2, None, false, Some(1), vec![vec![2]])]
269 #[case::priority_order(2, None, false, Some(2), vec![vec![2], vec![1]])]
270 #[case::unlimited(2, None, false, None, vec![vec![2], vec![1], vec![0]])]
271 #[case::fewer_eligible_outputs(2, Some(20), false, Some(3), vec![vec![1], vec![0]])]
272 #[case::absorbed_seeds(1, None, false, Some(2), vec![vec![2, 1], vec![0]])]
273 #[case::busy_bridge_defers_both_seeds(1, None, true, Some(2), vec![vec![0]])]
274 #[case::memtable_barrier_defers_both_seeds(1, Some(20), false, Some(2), vec![vec![0]])]
275 #[case::all_closures_rejected(0, None, true, Some(1), vec![])]
276 #[case::zero_budget(2, None, false, Some(0), vec![])]
277 #[tokio::test]
278 async fn test_output_limit_counts_eligible_closures(
279 #[case] dependency_start_window: usize,
280 #[case] memtable_min: Option<u64>,
281 #[case] busy: bool,
282 #[case] limit: Option<usize>,
283 #[case] expected_windows: Vec<Vec<usize>>,
284 #[values(false, true)] fair: bool,
285 ) {
286 let windows: Vec<Vec<_>> = (0..3)
291 .map(|window| {
292 (0..4)
293 .map(|i| FileMeta {
294 file_id: FileId::random(),
295 time_range: (
296 Timestamp::new_second(window * 3600),
297 Timestamp::new_second(window * 3600 + 10),
298 ),
299 level: 0,
300 file_size: 100,
301 sequence: std::num::NonZeroU64::new((window * 4 + i + 1) as u64),
302 ..Default::default()
303 })
304 .collect()
305 })
306 .collect();
307 let dependency = FileMeta {
308 file_id: FileId::random(),
309 time_range: (
310 Timestamp::new_second(dependency_start_window as i64 * 3600),
311 Timestamp::new_second(7210),
312 ),
313 level: 1,
314 file_size: 1_000_000,
315 sequence: std::num::NonZeroU64::new(20),
316 ..Default::default()
317 };
318 let dependency_id = dependency.file_id();
319 let files = windows.iter().flatten().cloned().chain([dependency]);
320 let mut region = compaction_region_with_ssts(files, Duration::from_secs(60)).await;
321 region.ttl = None;
322 region.region_options.merge_mode = Some(MergeMode::LastNonNull);
323 region.current_version.memtable_min_sequence = memtable_min;
324 let CompactionOptions::Twcs(opts) = &mut region.region_options.compaction;
325 opts.time_window = Some(Duration::from_secs(3600));
326 opts.active_window_trigger_file_num = 4;
327 opts.inactive_window_trigger_file_num = 4;
328 region.current_version.ssts.levels()[1]
329 .files()
330 .next()
331 .unwrap()
332 .set_compacting(busy);
333 let request = compact_request::Options::Regular(Default::default());
334 let picked = if fair {
335 let selected_metrics = prometheus::IntCounterVec::new(
336 prometheus::Opts::new("test_twcs_selected", "selected seeds"),
337 &["class", "priority"],
338 )
339 .unwrap();
340 let mut history = PickHistory::with_selected_metrics_for_test(selected_metrics.clone());
341 let now = Instant::now();
342 let waiting_since = now - Duration::from_secs(120);
343 let keys: Vec<_> = (0..3)
344 .map(|window| PickKey {
345 window: window * 3600,
346 class: PickClass::L0Merge,
347 })
348 .collect();
349 history.observe_for_test(keys.iter().copied(), waiting_since);
350 let promoted = selected_metrics.with_label_values(&["l0_merge", "promoted"]);
351 let promoted_before = promoted.get();
352 let picked = pick_automatic_for_test(®ion, &mut history, limit)
353 .await
354 .unwrap();
355 let mut attempted_windows: HashSet<_> =
356 expected_windows.iter().flatten().copied().collect();
357 if busy || memtable_min.is_some() {
358 attempted_windows.extend(dependency_start_window..3);
361 }
362 assert_eq!(
363 promoted_before + attempted_windows.len() as u64,
364 promoted.get()
365 );
366 for (window, key) in keys.into_iter().enumerate() {
367 assert_eq!(
368 (!attempted_windows.contains(&window)).then_some(waiting_since),
369 history.promoted_since_for_test(key, now),
370 "window {window}"
371 );
372 }
373 picked
374 } else {
375 new_picker(&request, ®ion.region_options, limit, None, false)
376 .pick(®ion)
377 .await
378 .unwrap()
379 };
380 let outputs = picked.map_or_else(Vec::new, |picked| {
381 assert!(picked.expired_ssts.is_empty());
382 picked.outputs
383 });
384 assert_eq!(expected_windows.len(), outputs.len());
385 for (output, window_indices) in outputs.iter().rev().zip(expected_windows) {
387 let mut expected: HashSet<_> = window_indices
388 .iter()
389 .flat_map(|&i| windows[i].iter().map(FileMeta::file_id))
390 .collect();
391 if window_indices.contains(&2) {
392 expected.insert(dependency_id);
393 }
394 assert_eq!(expected, file_ids(&output.inputs));
395 }
396
397 region.region_options.merge_mode = Some(MergeMode::LastRow);
399 let picker = new_picker(&request, ®ion.region_options, limit, None, false);
400 let outputs = picker
401 .pick(®ion)
402 .await
403 .unwrap()
404 .map_or_else(Vec::new, |picked| picked.outputs);
405 assert_eq!(limit.unwrap_or(3).min(3), outputs.len());
406 for (output, window) in outputs.iter().rev().zip(windows.iter().rev()) {
407 let expected: HashSet<_> = window.iter().map(FileMeta::file_id).collect();
408 assert_eq!(expected, file_ids(&output.inputs));
409 }
410 }
411
412 #[tokio::test]
413 async fn test_budget_excluded_l1_keeps_waiting_across_l0_picks() {
414 let files = [(0, 1), (7200, 0)].into_iter().flat_map(|(start, level)| {
417 (0..4).map(move |i| FileMeta {
418 file_id: FileId::random(),
419 time_range: (
420 Timestamp::new_second(start),
421 Timestamp::new_second(start + 10),
422 ),
423 level,
424 file_size: 100,
425 sequence: std::num::NonZeroU64::new((start + i + 1) as u64),
426 ..Default::default()
427 })
428 });
429 let mut region = compaction_region_with_ssts(files, Duration::from_secs(60)).await;
430 region.ttl = None;
431 region.region_options.merge_mode = Some(MergeMode::LastNonNull);
432 let CompactionOptions::Twcs(opts) = &mut region.region_options.compaction;
433 opts.time_window = Some(Duration::from_secs(3600));
434 opts.inactive_window_l1_merge_trigger = 4;
435
436 let old = PickKey {
437 window: 0,
438 class: PickClass::FileReduction,
439 };
440 let mut history = PickHistory::default();
441 let start = Instant::now();
442 history.observe_for_test([old].into_iter(), start);
443 for _ in 0..3 {
444 let picked = pick_automatic_for_test(®ion, &mut history, Some(1))
445 .await
446 .unwrap()
447 .unwrap();
448 assert_eq!(1, picked.outputs.len());
449 assert_eq!(4, picked.outputs[0].inputs.len());
450 assert!(
451 picked.outputs[0]
452 .inputs
453 .iter()
454 .all(|file| file.level() == 0)
455 );
456 assert_eq!(
457 Some(start),
458 history.promoted_since_for_test(old, start + Duration::from_secs(120))
459 );
460 }
461
462 history.seed_for_test(old, start - Duration::from_secs(120));
465 let picked = pick_automatic_for_test(®ion, &mut history, Some(1))
466 .await
467 .unwrap()
468 .unwrap();
469 assert_eq!(1, picked.outputs.len());
470 assert_eq!(4, picked.outputs[0].inputs.len());
471 assert!(
472 picked.outputs[0]
473 .inputs
474 .iter()
475 .all(|file| file.level() == 1)
476 );
477 assert!(
478 history
479 .promoted_since_for_test(old, Instant::now())
480 .is_none()
481 );
482 }
483
484 #[rstest::rstest]
487 #[case(0, None, true)]
488 #[case(0, Some(2), false)]
489 #[case(1, Some(0), false)]
490 #[case(1, Some(2), true)]
491 #[case(2, Some(2), false)]
492 #[case(3, Some(2), false)]
493 #[case(u64::MAX - 1, Some(u64::MAX), true)]
494 #[case(u64::MAX, Some(u64::MAX), false)]
495 fn test_inputs_must_precede_pending_memtables(
496 #[case] file_sequence: u64,
497 #[case] memtable_min: Option<u64>,
498 #[case] expected: bool,
499 ) {
500 let file =
501 new_file_handle_with_size_and_sequence(FileId::random(), 0, 10, 0, file_sequence, 100);
502 assert_eq!(expected, inputs_precede_memtables(&[file], memtable_min));
503 }
504
505 #[test]
506 fn test_transitive_closure_exceeds_seed_limit_across_levels_and_windows() {
507 let metadata = metadata_for_test();
508 let files: Vec<_> = (0..32)
511 .map(|i| {
512 new_file_handle(
513 FileId::random(),
514 i * 3_600_000,
515 (i + 1) * 3_600_000,
516 (i % 2) as u8,
517 )
518 })
519 .collect();
520 let (outputs, _) = build_closed_outputs(
521 vec![seed(vec![files[0].clone()])],
522 files.clone(),
523 &metadata,
524 ClosureLimits::default(),
525 );
526 assert_eq!(1, outputs.len());
527 assert_eq!(files.len(), outputs[0].inputs.len());
528 assert_eq!(file_ids(&files), file_ids(&outputs[0].inputs));
529 }
530
531 #[rstest::rstest]
532 #[case::unlimited(false, None, vec![vec![0, 1, 2, 3], vec![4]], vec![0, 1, 2])]
533 #[case::busy_unlimited(true, None, vec![vec![4]], vec![0, 1, 2])]
534 #[case::one_slot(false, Some(1), vec![vec![0, 1, 2, 3]], vec![1, 2])]
535 #[case::busy_one_slot(true, Some(1), vec![vec![4]], vec![0, 1, 2])]
536 #[case::zero_budget(false, Some(0), vec![], vec![])]
537 #[case::busy_zero_budget(true, Some(0), vec![], vec![])]
538 fn test_absorbed_seed_closes_disconnected_members_and_defers_busy_group(
539 #[case] busy: bool,
540 #[case] max_outputs: Option<usize>,
541 #[case] expected_groups: Vec<Vec<usize>>,
542 #[case] expected_attempted: Vec<usize>,
543 ) {
544 let metadata = metadata_for_test();
545 let files: Vec<_> = [(0, 10), (10, 20), (100, 110), (110, 120), (200, 210)]
546 .into_iter()
547 .map(|(start, end)| new_file_handle(FileId::random(), start, end, 0))
548 .collect();
549 files[3].set_compacting(busy);
550 let mut priority_seed = seed(vec![files[0].clone()]);
551 priority_seed.filter_deleted = true;
552 let seeds = vec![
553 seed(vec![files[4].clone()]),
554 seed(vec![files[1].clone(), files[2].clone()]),
555 priority_seed,
556 ];
557
558 let (outputs, attempted) = build_closed_outputs(
559 seeds,
560 files.clone(),
561 &metadata,
562 ClosureLimits {
563 max_outputs,
564 ..Default::default()
565 },
566 );
567 assert_eq!(expected_attempted, attempted);
568 assert_eq!(expected_groups.len(), outputs.len());
569 for (output, indices) in outputs.iter().rev().zip(expected_groups) {
571 let expected: HashSet<_> = indices
572 .iter()
573 .map(|&index| files[index].file_id())
574 .collect();
575 assert_eq!(indices.len(), output.inputs.len());
576 assert_eq!(expected, file_ids(&output.inputs));
577 assert!(!output.filter_deleted);
579 }
580 }
581
582 #[rstest::rstest]
583 #[case("chain", 2048)]
584 #[case("dense_with_external", 1024)]
585 #[case("time_dense_pk_disjoint", 1)]
586 fn test_large_snapshot_closure(#[case] shape: &str, #[case] expected_count: usize) {
587 use crate::compaction::test_util::{
588 new_file_handle_with_size_sequence_and_primary_key_range, pk_range,
589 primary_key_metadata_for_test,
590 };
591
592 let metadata = primary_key_metadata_for_test();
593 let files = (0..2048)
594 .map(|i| {
595 let (start, end, pk) = match shape {
596 "chain" => (i, i + 1, None),
597 "dense_with_external" if i < 1024 => (0, 1, None),
598 "dense_with_external" => (i, i, None),
599 "time_dense_pk_disjoint" => {
600 let key = i.to_string();
601 (0, 1, pk_range(key.as_bytes(), key.as_bytes()))
602 }
603 _ => unreachable!(),
604 };
605 new_file_handle_with_size_sequence_and_primary_key_range(
606 FileId::random(),
607 start,
608 end,
609 0,
610 i as u64 + 1,
611 100,
612 pk,
613 )
614 })
615 .collect::<Vec<_>>();
616 let (outputs, _) = build_closed_outputs(
617 vec![seed(vec![files[0].clone()])],
618 files.clone(),
619 &metadata,
620 ClosureLimits::default(),
621 );
622 assert_eq!(1, outputs.len());
623 assert_eq!(expected_count, outputs[0].inputs.len());
624 assert_eq!(
625 file_ids(&files[..expected_count]),
626 file_ids(&outputs[0].inputs)
627 );
628 }
629
630 #[rstest::rstest]
631 #[case(MergeMode::LastRow, false, None, 4)]
632 #[case(MergeMode::LastRow, true, None, 4)]
633 #[case(MergeMode::LastNonNull, false, None, 5)]
634 #[case(MergeMode::LastNonNull, true, None, 0)]
635 #[case(MergeMode::LastRow, false, Some(5), 4)]
636 #[case(MergeMode::LastNonNull, false, Some(5), 0)]
637 #[case(MergeMode::LastNonNull, false, Some(6), 5)]
638 #[tokio::test]
639 async fn test_picker_dependencies_outside_request_window(
640 #[case] merge_mode: MergeMode,
641 #[case] busy: bool,
642 #[case] memtable_min: Option<u64>,
643 #[case] expected_count: usize,
644 ) {
645 let files = (0..5).map(|i| FileMeta {
646 file_id: FileId::random(),
647 time_range: (
648 Timestamp::new_second(0),
649 Timestamp::new_second(if i == 4 { 3601 } else { 10 }),
650 ),
651 level: if i == 4 { 1 } else { 0 },
652 file_size: if i == 4 { 1_000_000 } else { 100 },
653 sequence: std::num::NonZeroU64::new(i + 1),
654 ..Default::default()
655 });
656 let mut region = compaction_region_with_ssts(files, Duration::from_secs(60)).await;
657 region.ttl = None;
658 region.region_options.merge_mode = Some(merge_mode);
659 region.current_version.memtable_min_sequence = memtable_min;
660 let CompactionOptions::Twcs(opts) = &mut region.region_options.compaction;
661 opts.time_window = Some(Duration::from_secs(3600));
662 let dependency = region.current_version.ssts.levels()[1]
663 .files()
664 .next()
665 .unwrap();
666 dependency.set_compacting(busy);
667 let picker = new_picker(
668 &compact_request::Options::Regular(Default::default()),
669 ®ion.region_options,
670 Some(1),
671 TimestampRange::new(Timestamp::new_second(0), Timestamp::new_second(3600)),
672 false,
673 );
674
675 let picked = picker.pick(®ion).await.unwrap();
676 if expected_count == 0 {
677 assert!(picked.is_none());
678 } else {
679 let picked = picked.unwrap();
680 assert_eq!(1, picked.outputs.len());
681 assert_eq!(expected_count, picked.outputs[0].inputs.len());
682 assert_eq!(
683 merge_mode == MergeMode::LastNonNull,
684 picked.outputs[0]
685 .inputs
686 .iter()
687 .any(|file| file.file_id() == dependency.file_id())
688 );
689 }
690 }
691
692 #[rstest::rstest]
693 #[case(MergeMode::LastNonNull, None, 3)]
694 #[case(MergeMode::LastNonNull, Some(2), 0)]
695 #[case(MergeMode::LastRow, Some(2), 3)]
696 #[tokio::test]
697 async fn test_strict_window_keeps_all_versions_in_disjoint_output_slices(
698 #[case] merge_mode: MergeMode,
699 #[case] memtable_min: Option<u64>,
700 #[case] expected_outputs: usize,
701 ) {
702 let files: Vec<_> = [(0, 10), (10, 10), (10, 20)]
703 .into_iter()
704 .enumerate()
705 .map(|(i, (start, end))| {
706 new_file_handle_with_size_and_sequence(
707 FileId::random(),
708 start * 1000,
709 end * 1000,
710 0,
711 i as u64 + 1,
712 100,
713 )
714 .meta_ref()
715 .clone()
716 })
717 .collect();
718 let mut region = compaction_region_with_ssts(files, Duration::from_secs(60)).await;
719 region.region_options.merge_mode = Some(merge_mode);
720 region.current_version.options.merge_mode = Some(merge_mode);
721 region.current_version.memtable_min_sequence = memtable_min;
722 let picker = new_picker(
723 &compact_request::Options::StrictWindow(api::v1::region::StrictWindow {
724 window_seconds: 10,
725 }),
726 ®ion.region_options,
727 Some(1),
728 TimestampRange::new(Timestamp::new_second(10), Timestamp::new_second(20)),
729 false,
730 );
731
732 let picked = picker.pick(®ion).await.unwrap().unwrap();
733 assert_eq!(expected_outputs, picked.outputs.len());
738 for (i, output) in picked.outputs.iter().enumerate() {
739 assert_eq!(
740 TimestampRange::new(
741 Timestamp::new_second(i as i64 * 10),
742 Timestamp::new_second((i as i64 + 1) * 10),
743 ),
744 output.output_time_range
745 );
746 assert_eq!(if i == 1 { 3 } else { 1 }, output.inputs.len());
747 assert!(!output.filter_deleted);
748 }
749 }
750
751 #[tokio::test]
752 async fn test_busy_closure_keeps_expired_files_without_rewriting_them() {
753 let now = Timestamp::current_millis().value();
754 let files = (0..6).map(|i| FileMeta {
755 file_id: FileId::random(),
756 time_range: (
757 Timestamp::new_millisecond(if i == 5 { 0 } else { now - 10_000 }),
758 Timestamp::new_millisecond(if i == 5 { 10 } else { now }),
759 ),
760 level: if i == 4 { 1 } else { 0 },
761 file_size: if i == 4 { 1_000_000 } else { 100 },
762 sequence: std::num::NonZeroU64::new(i + 1),
763 ..Default::default()
764 });
765 let mut region = compaction_region_with_ssts(files, Duration::from_secs(3600)).await;
766 region.region_options.merge_mode = Some(MergeMode::LastNonNull);
767 let dependency = region.current_version.ssts.levels()[1]
768 .files()
769 .next()
770 .unwrap();
771 dependency.set_compacting(true);
772 let picker = new_picker(
773 &compact_request::Options::Regular(Default::default()),
774 ®ion.region_options,
775 Some(1),
776 None,
777 false,
778 );
779 let picked = picker.pick(®ion).await.unwrap().unwrap();
780 assert!(picked.outputs.is_empty());
781 assert_eq!(1, picked.expired_ssts.len());
782 assert_eq!(
783 Timestamp::new_millisecond(10),
784 picked.expired_ssts[0].time_range().1
785 );
786 assert!(!picked.expired_ssts[0].compacting());
787
788 dependency.set_compacting(false);
789 let picked = picker.pick(®ion).await.unwrap().unwrap();
790 assert_eq!(1, picked.outputs.len());
791 assert_eq!(5, picked.outputs[0].inputs.len());
792 assert_eq!(1, picked.expired_ssts.len());
793 assert!(!file_ids(&picked.outputs[0].inputs).contains(&picked.expired_ssts[0].file_id()));
794 }
795}