1use std::num::NonZeroU64;
16use std::sync::Arc;
17use std::time::Duration;
18
19use common_base::Plugins;
20use common_base::cancellation::{CancellableFuture, CancellationHandle};
21use common_meta::key::SchemaMetadataManagerRef;
22use common_telemetry::{debug, info, warn};
23use common_time::TimeToLive;
24use either::Either;
25use itertools::Itertools;
26use object_store::manager::ObjectStoreManagerRef;
27use partition::expr::PartitionExpr;
28use serde::{Deserialize, Serialize};
29use snafu::{OptionExt, ResultExt};
30use store_api::ManifestVersion;
31use store_api::metadata::RegionMetadataRef;
32use store_api::region_request::PathType;
33use store_api::storage::{RegionId, SequenceNumber};
34
35use crate::access_layer::{
36 AccessLayer, AccessLayerRef, Metrics, OperationType, SstWriteRequest, WriteType,
37};
38use crate::cache::{CacheManager, CacheManagerRef};
39use crate::compaction::picker::PickerOutput;
40use crate::compaction::reader::CompactionSstReaderBuilder;
41use crate::compaction::unit::CompactionUnit;
42use crate::compaction::{CompactionOutput, find_dynamic_options};
43use crate::config::MitoConfig;
44use crate::engine::region_hook::{RegionHookRef, SstFileInfo};
45use crate::error;
46use crate::error::{
47 EmptyRegionDirSnafu, InvalidPartitionExprSnafu, ObjectStoreNotFoundSnafu, Result,
48};
49use crate::manifest::action::{RegionEdit, RegionMetaAction, RegionMetaActionList};
50use crate::manifest::manager::{RegionManifestManager, RegionManifestOptions};
51use crate::region::options::{MergeMode, RegionOptions};
52use crate::region::version::VersionRef;
53use crate::region::{ManifestContext, RegionLeaderState, RegionRoleState};
54use crate::schedule::scheduler::LocalScheduler;
55use crate::sst::FormatType;
56use crate::sst::file::{FileHandle, FileMeta, UncommittedSsts};
57use crate::sst::file_purger::LocalFilePurger;
58use crate::sst::index::intermediate::IntermediateManager;
59use crate::sst::index::puffin_manager::PuffinManagerFactory;
60use crate::sst::location::region_dir_from_table_dir;
61use crate::sst::parquet::metadata::extract_primary_key_range;
62use crate::sst::parquet::{SstInfo, WriteOptions};
63use crate::sst::version::{SstVersion, SstVersionRef};
64
65#[derive(Clone)]
67pub struct CompactionVersion {
68 pub(crate) metadata: RegionMetadataRef,
73 pub(crate) options: RegionOptions,
75 pub(crate) ssts: SstVersionRef,
77 pub(crate) memtable_min_sequence: Option<SequenceNumber>,
81 pub(crate) compaction_time_window: Option<Duration>,
83}
84
85impl From<VersionRef> for CompactionVersion {
86 fn from(value: VersionRef) -> Self {
87 Self {
88 metadata: value.metadata.clone(),
89 options: value.options.clone(),
90 ssts: value.ssts.clone(),
91 memtable_min_sequence: if value.options.merge_mode() == MergeMode::LastNonNull {
92 value.memtables.min_sequence()
93 } else {
94 None
95 },
96 compaction_time_window: value.compaction_time_window,
97 }
98 }
99}
100
101#[derive(Clone)]
104pub struct CompactionRegion {
105 pub region_id: RegionId,
106 pub region_options: RegionOptions,
107
108 pub(crate) engine_config: Arc<MitoConfig>,
109 pub(crate) region_metadata: RegionMetadataRef,
110 pub(crate) cache_manager: CacheManagerRef,
111 pub access_layer: AccessLayerRef,
113 pub(crate) manifest_ctx: Arc<ManifestContext>,
114 pub(crate) current_version: CompactionVersion,
115 pub(crate) file_purger: Option<Arc<LocalFilePurger>>,
116 pub(crate) ttl: Option<TimeToLive>,
117
118 pub max_parallelism: usize,
123
124 pub(crate) plugins: Plugins,
125}
126
127#[derive(Clone)]
132pub struct CompactionContext {
133 pub region_id: RegionId,
135 pub region_options: RegionOptions,
137 version_options: RegionOptions,
139 pub(crate) engine_config: Arc<MitoConfig>,
140 region_metadata: RegionMetadataRef,
141 pub(crate) cache_manager: CacheManagerRef,
142 pub(crate) access_layer: AccessLayerRef,
143 manifest_ctx: Arc<ManifestContext>,
144 plugins: Plugins,
145}
146
147impl From<&CompactionRegion> for CompactionContext {
148 fn from(region: &CompactionRegion) -> Self {
149 Self {
150 region_id: region.region_id,
151 region_options: region.region_options.clone(),
152 version_options: region.current_version.options.clone(),
153 engine_config: region.engine_config.clone(),
154 region_metadata: region.region_metadata.clone(),
155 cache_manager: region.cache_manager.clone(),
156 access_layer: region.access_layer.clone(),
157 manifest_ctx: region.manifest_ctx.clone(),
158 plugins: region.plugins.clone(),
159 }
160 }
161}
162
163#[cfg(test)]
165pub(crate) async fn new_test_compaction_region() -> CompactionRegion {
166 let env = crate::test_util::scheduler_util::SchedulerEnv::new().await;
167 let metadata = crate::test_util::memtable_util::metadata_for_test();
168 let manifest_ctx = env.mock_manifest_context(metadata.clone()).await;
169 CompactionRegion {
170 region_id: RegionId::new(1, 1),
171 region_options: RegionOptions::default(),
172 engine_config: Arc::new(MitoConfig::default()),
173 region_metadata: metadata.clone(),
174 cache_manager: Arc::new(CacheManager::default()),
175 access_layer: env.access_layer.clone(),
176 manifest_ctx,
177 current_version: CompactionVersion {
178 metadata: metadata.clone(),
179 options: RegionOptions::default(),
180 ssts: Arc::new(SstVersion::new(metadata)),
181 memtable_min_sequence: None,
182 compaction_time_window: None,
183 },
184 file_purger: None,
185 ttl: None,
186 max_parallelism: 1,
187 plugins: Plugins::new(),
188 }
189}
190
191#[derive(Clone)]
193pub struct OpenCompactionRegionRequest {
194 pub region_id: RegionId,
195 pub table_dir: String,
196 pub path_type: PathType,
197 pub region_options: RegionOptions,
198 pub max_parallelism: usize,
199 pub plugins: Plugins,
201}
202
203pub async fn open_compaction_region(
206 req: &OpenCompactionRegionRequest,
207 mito_config: &MitoConfig,
208 object_store_manager: ObjectStoreManagerRef,
209 ttl_provider: Either<TimeToLive, SchemaMetadataManagerRef>,
210) -> Result<CompactionRegion> {
211 let object_store = {
212 let name = &req.region_options.storage;
213 if let Some(name) = name {
214 object_store_manager
215 .find(name)
216 .with_context(|| ObjectStoreNotFoundSnafu {
217 object_store: name.clone(),
218 })?
219 } else {
220 object_store_manager.default_object_store()
221 }
222 };
223
224 let access_layer = {
225 let puffin_manager_factory = PuffinManagerFactory::new(
226 &mito_config.index.aux_path,
227 mito_config.index.staging_size.as_bytes(),
228 Some(mito_config.index.write_buffer_size.as_bytes() as _),
229 mito_config.index.staging_ttl,
230 )
231 .await?;
232 let intermediate_manager =
233 IntermediateManager::init_fs(mito_config.index.aux_path.clone()).await?;
234
235 Arc::new(AccessLayer::new(
236 &req.table_dir,
237 req.path_type,
238 object_store.clone(),
239 puffin_manager_factory,
240 intermediate_manager,
241 ))
242 };
243
244 let manifest_manager = {
245 let region_dir = region_dir_from_table_dir(&req.table_dir, req.region_id, req.path_type);
246 let region_manifest_options =
247 RegionManifestOptions::new(mito_config, ®ion_dir, object_store);
248
249 RegionManifestManager::open(region_manifest_options, &Default::default())
250 .await?
251 .with_context(|| EmptyRegionDirSnafu {
252 region_id: req.region_id,
253 region_dir: region_dir_from_table_dir(&req.table_dir, req.region_id, req.path_type),
254 })?
255 };
256
257 let manifest = manifest_manager.manifest();
258 let region_metadata = manifest.metadata.clone();
259 let hook: Option<RegionHookRef> = req.plugins.get();
260 let manifest_ctx = Arc::new(ManifestContext::new(
261 manifest_manager,
262 RegionRoleState::Leader(RegionLeaderState::Writable),
263 hook,
264 ));
265
266 let file_purger = {
267 let purge_scheduler = Arc::new(LocalScheduler::new(mito_config.max_background_purges));
268 Arc::new(LocalFilePurger::new(
269 purge_scheduler.clone(),
270 access_layer.clone(),
271 None,
272 ))
273 };
274
275 let current_version = {
276 let mut ssts = SstVersion::new(region_metadata.clone());
277 ssts.add_files(file_purger.clone(), manifest.files.values().cloned());
278 CompactionVersion {
279 metadata: region_metadata.clone(),
280 options: req.region_options.clone(),
281 ssts: Arc::new(ssts),
282 memtable_min_sequence: Some(0),
285 compaction_time_window: manifest.compaction_time_window,
286 }
287 };
288
289 let ttl = match ttl_provider {
290 Either::Left(ttl) => ttl,
292 Either::Right(schema_metadata_manager) => {
294 let (_, ttl) =
295 find_dynamic_options(req.region_id, &req.region_options, &schema_metadata_manager)
296 .await
297 .unwrap_or_else(|e| {
298 warn!(e; "Failed to get ttl for region: {}", region_metadata.region_id);
299 (
300 crate::region::options::CompactionOptions::default(),
301 TimeToLive::default(),
302 )
303 });
304 ttl
305 }
306 };
307
308 Ok(CompactionRegion {
309 region_id: req.region_id,
310 region_options: req.region_options.clone(),
311 engine_config: Arc::new(mito_config.clone()),
312 region_metadata: region_metadata.clone(),
313 cache_manager: Arc::new(CacheManager::default()),
314 access_layer,
315 manifest_ctx,
316 current_version,
317 file_purger: Some(file_purger),
318 ttl: Some(ttl),
319 max_parallelism: req.max_parallelism,
320 plugins: req.plugins.clone(),
321 })
322}
323
324impl CompactionRegion {
325 pub fn file_purger(&self) -> Option<Arc<LocalFilePurger>> {
327 self.file_purger.clone()
328 }
329
330 pub async fn stop_purger_scheduler(&self) -> Result<()> {
332 if let Some(file_purger) = &self.file_purger {
333 file_purger.stop_scheduler().await
334 } else {
335 Ok(())
336 }
337 }
338
339 pub async fn invoke_sst_hook(&self, merge_output: &MergeOutput) {
344 CompactionContext::from(self)
345 .invoke_sst_hook(merge_output)
346 .await;
347 }
348}
349
350impl CompactionContext {
351 pub(crate) async fn invoke_sst_hook(&self, merge_output: &MergeOutput) {
353 let Some(hook) = self.plugins.get::<RegionHookRef>() else {
354 return;
355 };
356
357 let synthesized: Vec<SstInfo>;
362 let infos: &[SstInfo] = if merge_output.sst_infos.is_empty() {
363 synthesized = merge_output
364 .files_to_add
365 .iter()
366 .map(sst_info_from_file_meta)
367 .collect();
368 &synthesized
369 } else {
370 if merge_output.sst_infos.len() != merge_output.files_to_add.len() {
374 warn!(
375 "sst_infos length ({}) does not match files_to_add length ({}) for region {}",
376 merge_output.sst_infos.len(),
377 merge_output.files_to_add.len(),
378 self.region_id
379 );
380 }
381 &merge_output.sst_infos
382 };
383
384 let files: Vec<SstFileInfo<'_>> = merge_output
385 .files_to_add
386 .iter()
387 .zip(infos)
388 .map(|(meta, info)| SstFileInfo {
389 sst_info_ref: info,
390 file_meta: meta,
391 })
392 .collect();
393 hook.on_sst_files_written(self.region_id, &self.region_metadata, &files)
394 .await;
395 }
396
397 pub(crate) async fn update_manifest(
399 &self,
400 merge_output: MergeOutput,
401 ) -> Result<(RegionEdit, ManifestVersion)> {
402 let edit = RegionEdit {
403 files_to_add: merge_output.files_to_add,
404 files_to_remove: merge_output.files_to_remove,
405 timestamp_ms: Some(chrono::Utc::now().timestamp_millis()),
406 compaction_time_window: merge_output
407 .compaction_time_window
408 .map(|seconds| Duration::from_secs(seconds as u64)),
409 flushed_entry_id: None,
410 flushed_sequence: None,
411 committed_sequence: None,
412 };
413
414 let action_list = RegionMetaActionList::with_action(RegionMetaAction::Edit(edit.clone()));
415 let manifest_version = self
416 .manifest_ctx
417 .update_manifest_for_compaction(action_list)
418 .await?;
419
420 Ok((edit, manifest_version))
421 }
422}
423
424fn sst_info_from_file_meta(meta: &FileMeta) -> SstInfo {
447 SstInfo {
448 file_id: meta.file_id,
449 time_range: meta.time_range,
450 file_size: meta.file_size,
451 max_row_group_uncompressed_size: meta.max_row_group_uncompressed_size,
452 num_rows: meta.num_rows as usize,
453 num_row_groups: meta.num_row_groups,
454 num_series: meta.num_series,
455 ..Default::default()
456 }
457}
458
459#[derive(Default, Debug, Serialize, Deserialize)]
461pub struct MergeOutput {
462 pub files_to_add: Vec<FileMeta>,
463 pub files_to_remove: Vec<FileMeta>,
464 pub compaction_time_window: Option<i64>,
465 #[serde(skip)]
466 pub sst_infos: Vec<SstInfo>,
467}
468
469impl MergeOutput {
470 pub fn is_empty(&self) -> bool {
471 self.files_to_add.is_empty() && self.files_to_remove.is_empty()
472 }
473
474 pub fn input_file_size(&self) -> u64 {
475 self.files_to_remove.iter().map(|f| f.file_size).sum()
476 }
477
478 pub fn output_file_size(&self) -> u64 {
479 self.files_to_add.iter().map(|f| f.file_size).sum()
480 }
481}
482
483#[async_trait::async_trait]
485pub trait Compactor: Send + Sync + 'static {
486 async fn merge_ssts(
488 &self,
489 compaction_region: &CompactionRegion,
490 picker_output: PickerOutput,
491 ) -> Result<MergeOutput>;
492
493 async fn update_manifest(
495 &self,
496 compaction_region: &CompactionRegion,
497 merge_output: MergeOutput,
498 ) -> Result<(RegionEdit, ManifestVersion)>;
499}
500
501#[async_trait::async_trait]
506pub trait SstMerger: Send + Sync + 'static {
507 async fn merge_single_output(
508 &self,
509 context: CompactionContext,
510 output: CompactionOutput,
511 write_opts: WriteOptions,
512 ) -> Result<(Vec<FileMeta>, Vec<SstInfo>)>;
513}
514
515#[derive(Clone)]
517pub struct DefaultSstMerger;
518
519struct OutputSequenceMetadata {
521 file_sequence: Option<NonZeroU64>,
522 exact_sequence_trusted: bool,
523}
524
525fn output_sequence_metadata(
529 context: &CompactionContext,
530 inputs: &[FileHandle],
531) -> OutputSequenceMetadata {
532 let exact_sequence_trusted = context.region_options.preserve_row_sequence
533 && inputs
534 .iter()
535 .all(|f| f.is_effective_target_sequence_trusted(context.region_id));
536 OutputSequenceMetadata {
537 file_sequence: known_max_input_sequence(inputs),
538 exact_sequence_trusted,
539 }
540}
541
542fn known_max_input_sequence(inputs: &[FileHandle]) -> Option<NonZeroU64> {
547 let mut max: Option<NonZeroU64> = None;
548 for input in inputs {
549 let sequence = input.meta_ref().sequence?;
550 max = Some(max.map_or(sequence, |current| current.max(sequence)));
551 }
552 max
553}
554
555#[async_trait::async_trait]
556impl SstMerger for DefaultSstMerger {
557 async fn merge_single_output(
558 &self,
559 context: CompactionContext,
560 output: CompactionOutput,
561 write_opts: WriteOptions,
562 ) -> Result<(Vec<FileMeta>, Vec<SstInfo>)> {
563 let region_id = context.region_id;
564 let index_options = context.version_options.index_options.clone();
565 let append_mode = context.version_options.append_mode;
566 let merge_mode = context.version_options.merge_mode();
567 let flat_format = context
568 .region_options
569 .sst_format
570 .map(|format| format == FormatType::Flat)
571 .unwrap_or(context.engine_config.default_flat_format);
572
573 let index_config = context.engine_config.index.clone();
574 let inverted_index_config = context.engine_config.inverted_index.clone();
575 let fulltext_index_config = context.engine_config.fulltext_index.clone();
576 let bloom_filter_index_config = context.engine_config.bloom_filter_index.clone();
577
578 let input_file_names = output
579 .inputs
580 .iter()
581 .map(|f| f.file_id().to_string())
582 .join(",");
583 let sequence_metadata = output_sequence_metadata(&context, &output.inputs);
584 let builder = CompactionSstReaderBuilder {
585 metadata: context.region_metadata.clone(),
586 sst_layer: context.access_layer.clone(),
587 cache: context.cache_manager.clone(),
588 inputs: &output.inputs,
589 append_mode,
590 filter_deleted: output.filter_deleted,
591 time_range: output.output_time_range,
592 merge_mode,
593 };
594 let source = builder.build_flat_sst_reader().await?;
595
596 let mut metrics = Metrics::new(WriteType::Compaction);
597 let region_metadata = context.region_metadata.clone();
598 let sst_infos = context
599 .access_layer
600 .write_sst(
601 SstWriteRequest {
602 op_type: OperationType::Compact,
603 metadata: region_metadata.clone(),
604 source,
605 cache_manager: context.cache_manager.clone(),
606 max_sequence: None,
610 sst_write_format: if flat_format {
611 FormatType::Flat
612 } else {
613 FormatType::PrimaryKey
614 },
615 preserve_row_sequence: sequence_metadata.exact_sequence_trusted,
616 index_options,
617 index_config,
618 inverted_index_config,
619 fulltext_index_config,
620 bloom_filter_index_config,
621 },
622 &write_opts,
623 &mut metrics,
624 )
625 .await?;
626 let partition_expr = match ®ion_metadata.partition_expr {
628 None => None,
629 Some(json_str) if json_str.is_empty() => None,
630 Some(json_str) => PartitionExpr::from_json_str(json_str).with_context(|_| {
631 InvalidPartitionExprSnafu {
632 expr: json_str.clone(),
633 }
634 })?,
635 };
636
637 let output_files = sst_infos
638 .iter()
639 .map(|sst_info| {
640 let pk_range = sst_info
641 .file_metadata
642 .as_ref()
643 .and_then(|meta| extract_primary_key_range(meta, ®ion_metadata));
644 let (primary_key_min, primary_key_max) = match pk_range {
645 Some((min, max)) => (Some(min), Some(max)),
646 None => (None, None),
647 };
648
649 FileMeta {
650 region_id,
651 file_id: sst_info.file_id,
652 time_range: sst_info.time_range,
653 level: output.output_level,
654 file_size: sst_info.file_size,
655 max_row_group_uncompressed_size: sst_info.max_row_group_uncompressed_size,
656 available_indexes: sst_info.index_metadata.build_available_indexes(),
657 indexes: sst_info.index_metadata.build_indexes(),
658 index_file_size: sst_info.index_metadata.file_size,
659 index_version: 0,
660 num_rows: sst_info.num_rows as u64,
661 num_row_groups: sst_info.num_row_groups,
662 sequence: sequence_metadata.file_sequence,
663 partition_expr: partition_expr.clone(),
664 num_series: sst_info.num_series,
665 primary_key_min,
666 primary_key_max,
667 preserve_row_sequence: sequence_metadata.exact_sequence_trusted,
668 }
669 })
670 .collect::<Vec<_>>();
671 let output_file_names = output_files.iter().map(|f| f.file_id.to_string()).join(",");
672 info!(
673 "Region {} compaction inputs: [{}], outputs: [{}], flat_format: {}, metrics: {:?}",
674 region_id, input_file_names, output_file_names, flat_format, metrics
675 );
676 metrics.observe();
677 Ok((output_files, sst_infos.into_iter().collect()))
678 }
679}
680
681pub struct DefaultCompactor<M = DefaultSstMerger> {
686 merger: M,
687 cancel_handle: Arc<CancellationHandle>,
688 uncommitted: Option<UncommittedSsts>,
689}
690
691#[cfg(test)]
692impl<M: SstMerger> DefaultCompactor<M> {
693 pub fn with_merger(merger: M) -> Self {
694 Self {
695 merger,
696 cancel_handle: Arc::new(CancellationHandle::default()),
697 uncommitted: None,
698 }
699 }
700}
701
702impl DefaultCompactor {
703 pub fn new_with_cancel_handle(cancel_handle: Arc<CancellationHandle>) -> Self {
709 Self {
710 merger: DefaultSstMerger,
711 cancel_handle,
712 uncommitted: None,
713 }
714 }
715
716 pub(crate) fn with_cancel_handle(
717 cancel_handle: Arc<CancellationHandle>,
718 uncommitted: UncommittedSsts,
719 ) -> Self {
720 Self {
721 merger: DefaultSstMerger,
722 cancel_handle,
723 uncommitted: Some(uncommitted),
724 }
725 }
726}
727
728impl<M: SstMerger> DefaultCompactor<M> {
729 pub(crate) async fn merge_unit(
731 &self,
732 context: &CompactionContext,
733 unit: &CompactionUnit,
734 ) -> Result<MergeOutput> {
735 let mut result = MergeOutput {
736 files_to_remove: unit.inputs.iter().map(|f| f.meta_ref().clone()).collect(),
737 compaction_time_window: Some(unit.time_window_size),
738 ..Default::default()
739 };
740 for output in &unit.outputs {
741 if self.cancel_handle.is_cancelled() {
742 return error::CompactionCancelledSnafu.fail();
743 }
744 let (files, infos) = self
747 .merger
748 .merge_single_output(
749 context.clone(),
750 output.clone(),
751 WriteOptions {
752 write_buffer_size: context.engine_config.sst_write_buffer_size,
753 max_file_size: unit.max_file_size,
754 row_group_size: context.region_options.row_group_size(),
755 float_field_encoding: context.region_options.float_field_encoding,
756 },
757 )
758 .await?;
759 if let Some(uncommitted) = &self.uncommitted {
760 uncommitted.track(&infos);
761 }
762 result.files_to_add.extend(files);
763 result.sst_infos.extend(infos);
764 }
765 if self.cancel_handle.is_cancelled() {
766 return error::CompactionCancelledSnafu.fail();
767 }
768 Ok(result)
769 }
770}
771
772#[async_trait::async_trait]
773impl<M: SstMerger> Compactor for DefaultCompactor<M>
774where
775 M: Clone,
776{
777 async fn merge_ssts(
778 &self,
779 compaction_region: &CompactionRegion,
780 mut picker_output: PickerOutput,
781 ) -> Result<MergeOutput> {
782 let internal_parallelism = compaction_region.max_parallelism.max(1);
783 let compaction_time_window = picker_output.time_window_size;
784 let region_id = compaction_region.region_id;
785 let context = CompactionContext::from(compaction_region);
786
787 let mut tasks: Vec<(Vec<FileMeta>, _)> = Vec::with_capacity(picker_output.outputs.len());
790
791 for output in picker_output.outputs.drain(..) {
792 let inputs_to_remove: Vec<_> =
793 output.inputs.iter().map(|f| f.meta_ref().clone()).collect();
794 let write_opts = WriteOptions {
795 write_buffer_size: compaction_region.engine_config.sst_write_buffer_size,
796 max_file_size: picker_output.max_file_size,
797 row_group_size: compaction_region.region_options.row_group_size(),
798 float_field_encoding: compaction_region.region_options.float_field_encoding,
799 };
800 let merger = self.merger.clone();
801 let context = context.clone();
802 let uncommitted = self.uncommitted.clone();
803 let fut = async move {
804 let result = merger
805 .merge_single_output(context, output, write_opts)
806 .await;
807 if let (Some(uncommitted), Ok((_, infos))) = (&uncommitted, &result) {
808 uncommitted.track(infos);
809 }
810 result
811 };
812 tasks.push((inputs_to_remove, fut));
813 }
814
815 let hook: Option<RegionHookRef> = compaction_region.plugins.get();
816 let mut output_files = Vec::with_capacity(tasks.len());
817 let mut all_sst_infos: Vec<SstInfo> = Vec::new();
818 let mut compacted_inputs = Vec::with_capacity(
819 tasks.iter().map(|(inputs, _)| inputs.len()).sum::<usize>()
820 + picker_output.expired_ssts.len(),
821 );
822
823 while !tasks.is_empty() {
824 let mut chunk: Vec<(Vec<FileMeta>, _)> = Vec::with_capacity(internal_parallelism);
825 for _ in 0..internal_parallelism {
826 if let Some(task) = tasks.pop() {
827 chunk.push(task);
828 }
829 }
830 let mut spawned: Vec<_> = chunk
831 .into_iter()
832 .map(|(inputs, fut)| {
833 let handle = common_runtime::spawn_compact(fut);
834 (inputs, handle)
835 })
836 .collect();
837
838 while let Some((inputs, mut handle)) = spawned.pop() {
839 let abort_handle = handle.abort_handle();
840 match CancellableFuture::new(&mut handle, self.cancel_handle.clone()).await {
841 Ok(Ok(Ok((files, infos)))) => {
842 output_files.extend(files);
843 if hook.is_some() {
844 all_sst_infos.extend(infos);
845 }
846 compacted_inputs.extend(inputs);
847 }
848 Ok(Ok(Err(e))) => {
849 warn!(
850 e; "Failed to merge compaction output for region: {}, inputs: [{}]",
851 region_id,
852 inputs.iter().map(|f| f.file_id.to_string()).join(",")
853 );
854 }
855 Ok(Err(e)) => {
856 warn!(
857 "Region {} compaction task join error for inputs: [{}], skipping: {}",
858 region_id,
859 inputs.iter().map(|f| f.file_id.to_string()).join(","),
860 e
861 );
862 if self.cancel_handle.is_cancelled() {
865 abort_handle.abort();
866 for (_, handle) in &spawned {
867 handle.abort();
868 }
869 for (_, handle) in spawned {
870 let _ = handle.await;
871 }
872 }
873 return Err(e).context(error::JoinSnafu);
874 }
875 Err(_) => {
876 debug!(
877 "Compaction merge cancelled for region: {}, aborting remaining {} spawned tasks",
878 region_id,
879 spawned.len(),
880 );
881 abort_handle.abort();
882 for (_, handle) in &spawned {
883 handle.abort();
884 }
885 let _ = handle.await;
886 for (_, handle) in spawned {
887 let _ = handle.await;
888 }
889 break;
890 }
891 }
892 }
893
894 if self.cancel_handle.is_cancelled() {
895 info!("Compaction merge cancelled for region: {}", region_id);
896 break;
897 }
898 }
899
900 compacted_inputs.extend(
902 picker_output
903 .expired_ssts
904 .iter()
905 .map(|f| f.meta_ref().clone()),
906 );
907
908 Ok(MergeOutput {
909 files_to_add: output_files,
910 files_to_remove: compacted_inputs,
911 compaction_time_window: Some(compaction_time_window),
912 sst_infos: all_sst_infos,
913 })
914 }
915
916 async fn update_manifest(
917 &self,
918 compaction_region: &CompactionRegion,
919 merge_output: MergeOutput,
920 ) -> Result<(RegionEdit, ManifestVersion)> {
921 CompactionContext::from(compaction_region)
922 .update_manifest(merge_output)
923 .await
924 }
925}
926
927#[cfg(test)]
928mod tests {
929 use std::sync::atomic::{AtomicUsize, Ordering};
930 use std::sync::{Arc, Mutex};
931 use std::time::Duration;
932
933 use store_api::storage::{FileId, RegionId};
934 use tokio::time::sleep;
935
936 use super::{DefaultCompactor, *};
937 use crate::compaction::picker::PickerOutput;
938 use crate::error::Result;
939 use crate::sst::file::FileHandle;
940 use crate::sst::file_purger::NoopFilePurger;
941
942 fn dummy_file_meta() -> FileMeta {
943 FileMeta {
944 region_id: RegionId::new(1, 1),
945 file_id: FileId::random(),
946 file_size: 100,
947 ..Default::default()
948 }
949 }
950
951 fn new_file_handle(meta: FileMeta) -> FileHandle {
952 FileHandle::new(meta, Arc::new(NoopFilePurger))
953 }
954
955 #[test]
956 fn test_effective_target_trust_and_max_sequence() {
957 let target = RegionId::new(1, 1);
958 let local_trusted = new_file_handle(FileMeta {
959 region_id: target,
960 sequence: NonZeroU64::new(3),
961 preserve_row_sequence: true,
962 ..dummy_file_meta()
963 });
964 let foreign_untrusted_marker = new_file_handle(FileMeta {
965 region_id: RegionId::new(1, 2),
966 sequence: NonZeroU64::new(9),
967 preserve_row_sequence: false,
968 ..dummy_file_meta()
969 });
970 let local_untrusted = new_file_handle(FileMeta {
971 region_id: target,
972 sequence: NonZeroU64::new(11),
973 preserve_row_sequence: false,
974 ..dummy_file_meta()
975 });
976 assert!(local_trusted.is_effective_target_sequence_trusted(target));
977 assert!(foreign_untrusted_marker.is_effective_target_sequence_trusted(target));
978 assert!(!local_untrusted.is_effective_target_sequence_trusted(target));
979 assert_eq!(
980 NonZeroU64::new(9),
981 known_max_input_sequence(&[local_trusted, foreign_untrusted_marker])
982 );
983 }
984
985 #[test]
986 fn test_known_max_input_sequence() {
987 let meta_with = |sequence| FileMeta {
988 sequence,
989 ..dummy_file_meta()
990 };
991
992 let inputs = vec![
993 new_file_handle(meta_with(NonZeroU64::new(3))),
994 new_file_handle(meta_with(NonZeroU64::new(9))),
995 new_file_handle(meta_with(NonZeroU64::new(5))),
996 ];
997 assert_eq!(NonZeroU64::new(9), known_max_input_sequence(&inputs));
998
999 let inputs = vec![
1000 new_file_handle(meta_with(NonZeroU64::new(9))),
1001 new_file_handle(meta_with(None)),
1002 ];
1003 assert_eq!(None, known_max_input_sequence(&inputs));
1004
1005 assert_eq!(None, known_max_input_sequence(&[]));
1006 }
1007
1008 #[tokio::test]
1009 async fn test_output_sequence_metadata_preserves_trust_and_unknown_bounds() {
1010 let mut region = new_test_compaction_region().await;
1011 let local = region.region_id;
1012 let foreign = RegionId::new(1, 2);
1013 let file = |region_id, sequence: Option<u64>, preserve_row_sequence| {
1014 new_file_handle(FileMeta {
1015 region_id,
1016 sequence: sequence.and_then(NonZeroU64::new),
1017 preserve_row_sequence,
1018 ..dummy_file_meta()
1019 })
1020 };
1021 let cases = [
1024 (
1025 vec![file(local, Some(3), true), file(foreign, Some(9), false)],
1026 Some(9),
1027 true,
1028 ),
1029 (
1030 vec![file(local, Some(3), true), file(local, Some(6), false)],
1031 Some(6),
1032 false,
1033 ),
1034 (
1035 vec![file(foreign, Some(9), false), file(foreign, Some(11), true)],
1036 Some(11),
1037 true,
1038 ),
1039 (
1040 vec![file(local, Some(3), true), file(local, None, true)],
1041 None,
1042 true,
1043 ),
1044 (
1045 vec![file(local, None, false), file(local, Some(6), true)],
1046 None,
1047 false,
1048 ),
1049 (
1050 vec![file(local, Some(3), true), file(foreign, None, true)],
1051 None,
1052 false,
1053 ),
1054 (vec![], None, true),
1055 (
1056 vec![file(local, Some(u64::MAX), false)],
1057 Some(u64::MAX),
1058 false,
1059 ),
1060 ];
1061 for committed_sequence in [100, 101] {
1062 region
1063 .manifest_ctx
1064 .manifest_manager
1065 .write()
1066 .await
1067 .update(
1068 RegionMetaActionList::with_action(RegionMetaAction::Edit(RegionEdit {
1069 committed_sequence: Some(committed_sequence),
1070 files_to_add: vec![],
1071 files_to_remove: vec![],
1072 timestamp_ms: None,
1073 compaction_time_window: None,
1074 flushed_entry_id: None,
1075 flushed_sequence: None,
1076 })),
1077 false,
1078 )
1079 .await
1080 .unwrap();
1081 for preserve in [false, true] {
1082 region.region_options.preserve_row_sequence = preserve;
1083 for (inputs, known_bound, inputs_trusted) in &cases {
1084 let metadata =
1085 output_sequence_metadata(&CompactionContext::from(®ion), inputs);
1086 let trusted = preserve && *inputs_trusted;
1087 assert_eq!(trusted, metadata.exact_sequence_trusted);
1088 assert_eq!(
1089 known_bound.and_then(NonZeroU64::new),
1090 metadata.file_sequence
1091 );
1092 }
1093 }
1094 }
1095 }
1096
1097 #[rstest::rstest]
1101 #[case(true)]
1102 #[case(false)]
1103 #[tokio::test]
1104 async fn test_independent_outputs_preserve_delete_in_every_visibility_state(
1105 #[case] flat_format: bool,
1106 #[values(false, true)] unknown_put_bound: bool,
1107 ) {
1108 use datatypes::arrow::array::AsArray;
1109 use datatypes::arrow::datatypes::TimestampMillisecondType;
1110 use store_api::region_engine::RegionEngine;
1111 use store_api::region_request::RegionRequest;
1112
1113 use crate::compaction::CompactionOutput;
1114 use crate::compaction::compactor::{CompactionRegion, Compactor, DefaultCompactor};
1115 use crate::compaction::picker::PickerOutput;
1116 use crate::compaction::reader::CompactionSstReaderBuilder;
1117 use crate::engine::compaction_test::{delete_and_flush, put_and_flush};
1118 use crate::region::options::MergeMode;
1119 use crate::sst::file::FileHandle;
1120 use crate::sst::file_purger::NoopFilePurger;
1121 use crate::test_util::{CreateRequestBuilder, TestEnv, rows_schema};
1122
1123 let mut env = TestEnv::new().await;
1124 let region_id = RegionId::new(1, 1);
1125 let config = MitoConfig {
1126 default_flat_format: flat_format,
1127 min_compaction_interval: Duration::from_secs(3600),
1128 ..Default::default()
1129 };
1130 let engine = env.create_engine(config).await;
1131 let request = CreateRequestBuilder::new().build();
1132 let columns = rows_schema(&request);
1133 engine
1134 .handle_request(region_id, RegionRequest::Create(request))
1135 .await
1136 .unwrap();
1137 put_and_flush(&engine, region_id, &columns, 0..1).await;
1138 delete_and_flush(&engine, region_id, &columns, 0..1).await;
1139 put_and_flush(&engine, region_id, &columns, 10..11).await;
1140 let region = engine.get_region(region_id).unwrap();
1141 let version = region.version();
1142 let mut files: Vec<_> = version
1143 .ssts
1144 .levels()
1145 .iter()
1146 .flat_map(|level| level.files())
1147 .cloned()
1148 .collect();
1149 assert_eq!(3, files.len());
1150 files.sort_unstable_by_key(|file| file.meta_ref().sequence);
1151 if unknown_put_bound {
1152 let mut meta = files[0].meta_ref().clone();
1155 meta.sequence = None;
1156 files[0] = FileHandle::new(meta, Arc::new(NoopFilePurger));
1157 }
1158 let compaction_region = CompactionRegion {
1159 region_id,
1160 region_options: version.options.clone(),
1161 engine_config: Arc::new(MitoConfig {
1162 default_flat_format: flat_format,
1163 ..Default::default()
1164 }),
1165 region_metadata: version.metadata.clone(),
1166 cache_manager: engine.cache_manager(),
1167 access_layer: region.access_layer.clone(),
1168 manifest_ctx: region.manifest_ctx.clone(),
1169 current_version: version.into(),
1170 file_purger: None,
1171 ttl: None,
1172 max_parallelism: 2,
1173 plugins: common_base::Plugins::new(),
1174 };
1175 let puts = vec![files[0].clone(), files[2].clone()];
1176 let deletes = vec![files[1].clone()];
1177 let picker_output = PickerOutput {
1178 outputs: [puts.clone(), deletes.clone()]
1179 .into_iter()
1180 .map(|inputs| CompactionOutput {
1181 output_level: 1,
1182 inputs,
1183 filter_deleted: false,
1184 output_time_range: None,
1185 })
1186 .collect(),
1187 expired_ssts: vec![],
1188 time_window_size: 3600,
1189 max_file_size: None,
1190 };
1191 let merged = DefaultCompactor::with_merger(DefaultSstMerger)
1192 .merge_ssts(&compaction_region, picker_output)
1193 .await
1194 .unwrap();
1195 assert_eq!(2, merged.files_to_add.len());
1196 let outputs: Vec<_> = merged
1197 .files_to_add
1198 .into_iter()
1199 .map(|meta| FileHandle::new(meta, Arc::new(NoopFilePurger)))
1200 .collect();
1201 let put_output = outputs
1202 .iter()
1203 .find(|file| file.meta_ref().num_rows == 2)
1204 .unwrap();
1205 let delete_output = outputs
1206 .iter()
1207 .find(|file| file.meta_ref().num_rows == 1)
1208 .unwrap();
1209 assert_eq!(
1210 if unknown_put_bound {
1211 None
1212 } else {
1213 NonZeroU64::new(3)
1214 },
1215 put_output.meta_ref().sequence,
1216 );
1217 assert_eq!(NonZeroU64::new(2), delete_output.meta_ref().sequence);
1218 assert!(
1219 outputs
1220 .iter()
1221 .all(|file| !file.meta_ref().preserve_row_sequence)
1222 );
1223
1224 for (put_done, delete_done) in [(false, false), (true, false), (false, true), (true, true)]
1227 {
1228 let mut visible = if put_done {
1229 vec![put_output.clone()]
1230 } else {
1231 puts.clone()
1232 };
1233 visible.extend(if delete_done {
1234 vec![delete_output.clone()]
1235 } else {
1236 deletes.clone()
1237 });
1238 let mut reader = CompactionSstReaderBuilder {
1239 metadata: compaction_region.region_metadata.clone(),
1240 sst_layer: region.access_layer.clone(),
1241 cache: engine.cache_manager(),
1242 inputs: &visible,
1243 append_mode: false,
1244 filter_deleted: true,
1245 time_range: None,
1246 merge_mode: MergeMode::LastRow,
1247 }
1248 .build_flat_sst_reader()
1249 .await
1250 .unwrap();
1251 let mut timestamps = Vec::new();
1252 while let Some(batch) = reader.next_batch().await.unwrap() {
1253 timestamps.extend(
1254 batch
1255 .column_by_name("ts")
1256 .unwrap()
1257 .as_primitive::<TimestampMillisecondType>()
1258 .values()
1259 .iter()
1260 .copied(),
1261 );
1262 }
1263 assert_eq!(
1264 vec![10_000],
1265 timestamps,
1266 "put_done={put_done}, delete_done={delete_done}"
1267 );
1268 }
1269 }
1270
1271 #[derive(Clone)]
1275 struct MockMerger {
1276 results: Arc<Mutex<Vec<Result<Vec<FileMeta>>>>>,
1277 call_idx: Arc<AtomicUsize>,
1278 }
1279
1280 impl MockMerger {
1281 fn new(results: Vec<Result<Vec<FileMeta>>>) -> Self {
1282 Self {
1283 results: Arc::new(Mutex::new(results)),
1284 call_idx: Arc::new(AtomicUsize::new(0)),
1285 }
1286 }
1287 }
1288
1289 #[async_trait::async_trait]
1290 impl SstMerger for MockMerger {
1291 async fn merge_single_output(
1292 &self,
1293 _context: CompactionContext,
1294 _output: CompactionOutput,
1295 _write_opts: WriteOptions,
1296 ) -> Result<(Vec<FileMeta>, Vec<SstInfo>)> {
1297 let idx = self.call_idx.fetch_add(1, Ordering::SeqCst);
1298 match self.results.lock().unwrap().get(idx) {
1299 Some(Ok(files)) => Ok((files.clone(), Vec::new())),
1300 Some(Err(_)) => error::InvalidMetaSnafu {
1301 reason: format!("simulated failure at index {idx}"),
1302 }
1303 .fail(),
1304 None => panic!("MockMerger: no result configured for call index {idx}"),
1305 }
1306 }
1307 }
1308
1309 #[tokio::test]
1310 async fn test_compaction_unit_group_failure_is_atomic() {
1311 let context = CompactionContext::from(&new_test_compaction_region().await);
1312 let input = new_file_handle(dummy_file_meta());
1313 let unit = CompactionUnit::from_picker(PickerOutput {
1314 outputs: (0..2)
1315 .map(|_| CompactionOutput {
1316 output_level: 1,
1317 inputs: vec![input.clone()],
1318 filter_deleted: false,
1319 output_time_range: None,
1320 })
1321 .collect(),
1322 ..Default::default()
1323 })
1324 .unwrap()
1325 .pop()
1326 .unwrap();
1327 for fail_second in [false, true] {
1328 let second = if fail_second {
1329 error::InvalidMetaSnafu {
1330 reason: "second output failed",
1331 }
1332 .fail()
1333 } else {
1334 Ok(vec![dummy_file_meta(), dummy_file_meta()])
1335 };
1336 let compactor =
1337 DefaultCompactor::with_merger(MockMerger::new(vec![Ok(Vec::new()), second]));
1338 let result = compactor.merge_unit(&context, &unit).await;
1339 if fail_second {
1340 assert!(
1341 result.is_err(),
1342 "cannot publish a successful subset of a dependency group"
1343 );
1344 } else {
1345 let result = result.unwrap();
1346 assert_eq!(2, result.files_to_add.len());
1347 assert_eq!(
1348 vec![input.meta_ref().file_id],
1349 result
1350 .files_to_remove
1351 .iter()
1352 .map(|f| f.file_id)
1353 .collect::<Vec<_>>()
1354 );
1355 }
1356 }
1357 }
1358
1359 #[tokio::test]
1360 async fn test_partial_merge_failure_collects_only_successful_outputs() {
1361 common_telemetry::init_default_ut_logging();
1362
1363 let compaction_region = new_test_compaction_region().await;
1364
1365 let input_meta_0 = dummy_file_meta();
1367 let input_meta_1 = dummy_file_meta();
1368 let input_meta_2 = dummy_file_meta();
1369
1370 let output_meta_0 = vec![dummy_file_meta()];
1371 let output_meta_2 = vec![dummy_file_meta(), dummy_file_meta()];
1372
1373 let merger = MockMerger::new(vec![
1374 Ok(output_meta_0.clone()),
1375 Err(error::InvalidMetaSnafu {
1376 reason: "boom".to_string(),
1377 }
1378 .build()),
1379 Ok(output_meta_2.clone()),
1380 ]);
1381 let compactor = DefaultCompactor::with_merger(merger);
1382
1383 let picker_output = PickerOutput {
1384 outputs: vec![
1385 CompactionOutput {
1386 output_level: 1,
1387 inputs: vec![new_file_handle(input_meta_0.clone())],
1388 filter_deleted: false,
1389 output_time_range: None,
1390 },
1391 CompactionOutput {
1392 output_level: 1,
1393 inputs: vec![new_file_handle(input_meta_1.clone())],
1394 filter_deleted: false,
1395 output_time_range: None,
1396 },
1397 CompactionOutput {
1398 output_level: 1,
1399 inputs: vec![new_file_handle(input_meta_2.clone())],
1400 filter_deleted: false,
1401 output_time_range: None,
1402 },
1403 ],
1404 expired_ssts: vec![],
1405 time_window_size: 3600,
1406 max_file_size: None,
1407 };
1408
1409 let merge_output = compactor
1410 .merge_ssts(&compaction_region, picker_output)
1411 .await
1412 .unwrap();
1413
1414 assert_eq!(merge_output.files_to_add.len(), 3);
1416 assert_eq!(merge_output.files_to_remove.len(), 2);
1418
1419 let removed_ids: Vec<_> = merge_output
1420 .files_to_remove
1421 .iter()
1422 .map(|f| f.file_id)
1423 .collect();
1424 assert!(removed_ids.contains(&input_meta_0.file_id));
1425 assert!(removed_ids.contains(&input_meta_2.file_id));
1426 assert!(!removed_ids.contains(&input_meta_1.file_id));
1428 }
1429
1430 #[tokio::test]
1431 async fn test_all_outputs_succeed() {
1432 common_telemetry::init_default_ut_logging();
1433
1434 let compaction_region = new_test_compaction_region().await;
1435 let input_meta = dummy_file_meta();
1436 let output_meta = vec![dummy_file_meta()];
1437
1438 let merger = MockMerger::new(vec![Ok(output_meta.clone())]);
1439 let compactor = DefaultCompactor::with_merger(merger);
1440
1441 let picker_output = PickerOutput {
1442 outputs: vec![CompactionOutput {
1443 output_level: 1,
1444 inputs: vec![new_file_handle(input_meta.clone())],
1445 filter_deleted: false,
1446 output_time_range: None,
1447 }],
1448 expired_ssts: vec![],
1449 time_window_size: 3600,
1450 max_file_size: None,
1451 };
1452
1453 let merge_output = compactor
1454 .merge_ssts(&compaction_region, picker_output)
1455 .await
1456 .unwrap();
1457
1458 assert_eq!(merge_output.files_to_add.len(), 1);
1459 assert_eq!(merge_output.files_to_add[0].file_id, output_meta[0].file_id);
1460 assert_eq!(merge_output.files_to_remove.len(), 1);
1461 assert_eq!(merge_output.files_to_remove[0].file_id, input_meta.file_id);
1462 }
1463
1464 #[tokio::test]
1465 async fn test_expired_ssts_always_removed() {
1466 common_telemetry::init_default_ut_logging();
1467
1468 let compaction_region = new_test_compaction_region().await;
1469 let input_meta = dummy_file_meta();
1470 let expired_meta = dummy_file_meta();
1471
1472 let merger = MockMerger::new(vec![Err(error::InvalidMetaSnafu {
1474 reason: "fail".to_string(),
1475 }
1476 .build())]);
1477 let compactor = DefaultCompactor::with_merger(merger);
1478
1479 let picker_output = PickerOutput {
1480 outputs: vec![CompactionOutput {
1481 output_level: 1,
1482 inputs: vec![new_file_handle(input_meta.clone())],
1483 filter_deleted: false,
1484 output_time_range: None,
1485 }],
1486 expired_ssts: vec![new_file_handle(expired_meta.clone())],
1487 time_window_size: 3600,
1488 max_file_size: None,
1489 };
1490
1491 let merge_output = compactor
1492 .merge_ssts(&compaction_region, picker_output)
1493 .await
1494 .unwrap();
1495
1496 assert!(merge_output.files_to_add.is_empty());
1498 assert_eq!(merge_output.files_to_remove.len(), 1);
1500 assert_eq!(
1501 merge_output.files_to_remove[0].file_id,
1502 expired_meta.file_id
1503 );
1504 }
1505
1506 #[derive(Clone)]
1507 struct BlockingMerger {
1508 call_idx: Arc<AtomicUsize>,
1509 }
1510
1511 #[async_trait::async_trait]
1512 impl SstMerger for BlockingMerger {
1513 async fn merge_single_output(
1514 &self,
1515 _context: CompactionContext,
1516 _output: CompactionOutput,
1517 _write_opts: WriteOptions,
1518 ) -> Result<(Vec<FileMeta>, Vec<SstInfo>)> {
1519 self.call_idx.fetch_add(1, Ordering::SeqCst);
1520 std::future::pending().await
1521 }
1522 }
1523
1524 #[tokio::test(flavor = "multi_thread")]
1525 async fn test_merge_ssts_cancels_spawned_tasks() {
1526 common_telemetry::init_default_ut_logging();
1527
1528 let mut compaction_region = new_test_compaction_region().await;
1529 compaction_region.max_parallelism = 2;
1530
1531 let cancel_handle = Arc::new(CancellationHandle::default());
1532 let call_idx = Arc::new(AtomicUsize::new(0));
1533 let compactor = DefaultCompactor {
1534 merger: BlockingMerger {
1535 call_idx: call_idx.clone(),
1536 },
1537 cancel_handle: cancel_handle.clone(),
1538 uncommitted: None,
1539 };
1540
1541 let picker_output = PickerOutput {
1542 outputs: vec![
1543 CompactionOutput {
1544 output_level: 1,
1545 inputs: vec![new_file_handle(dummy_file_meta())],
1546 filter_deleted: false,
1547 output_time_range: None,
1548 },
1549 CompactionOutput {
1550 output_level: 1,
1551 inputs: vec![new_file_handle(dummy_file_meta())],
1552 filter_deleted: false,
1553 output_time_range: None,
1554 },
1555 CompactionOutput {
1556 output_level: 1,
1557 inputs: vec![new_file_handle(dummy_file_meta())],
1558 filter_deleted: false,
1559 output_time_range: None,
1560 },
1561 ],
1562 expired_ssts: vec![],
1563 time_window_size: 3600,
1564 max_file_size: None,
1565 };
1566
1567 let task = tokio::spawn(async move {
1568 compactor
1569 .merge_ssts(&compaction_region, picker_output)
1570 .await
1571 });
1572
1573 sleep(Duration::from_millis(100)).await;
1574 cancel_handle.cancel();
1575
1576 let merge_output = task
1577 .await
1578 .expect("merge_ssts should stop after cancellation")
1579 .unwrap();
1580
1581 let started = call_idx.load(Ordering::SeqCst);
1582
1583 assert!(merge_output.files_to_add.is_empty());
1584 assert!(merge_output.files_to_remove.is_empty());
1585 assert_eq!(started, 2);
1586 }
1587}