1use std::num::NonZeroU64;
16use std::sync::Arc;
17use std::time::Duration;
18
19use common_base::Plugins;
20use common_base::cancellation::CancellationHandle;
21use common_meta::key::SchemaMetadataManagerRef;
22use common_telemetry::{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, spawn_merge};
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::parallel_writer::EncodeRuntime;
63use crate::sst::parquet::{SstInfo, WriteOptions};
64use crate::sst::version::{SstVersion, SstVersionRef};
65
66#[derive(Clone)]
68pub struct CompactionVersion {
69 pub(crate) metadata: RegionMetadataRef,
74 pub(crate) options: RegionOptions,
76 pub(crate) ssts: SstVersionRef,
78 pub(crate) memtable_min_sequence: Option<SequenceNumber>,
82 pub(crate) compaction_time_window: Option<Duration>,
84}
85
86impl From<VersionRef> for CompactionVersion {
87 fn from(value: VersionRef) -> Self {
88 Self {
89 metadata: value.metadata.clone(),
90 options: value.options.clone(),
91 ssts: value.ssts.clone(),
92 memtable_min_sequence: if value.options.merge_mode() == MergeMode::LastNonNull {
93 value.memtables.min_sequence()
94 } else {
95 None
96 },
97 compaction_time_window: value.compaction_time_window,
98 }
99 }
100}
101
102#[derive(Clone)]
105pub struct CompactionRegion {
106 pub region_id: RegionId,
107 pub region_options: RegionOptions,
108
109 pub(crate) engine_config: Arc<MitoConfig>,
110 pub(crate) region_metadata: RegionMetadataRef,
111 pub(crate) cache_manager: CacheManagerRef,
112 pub access_layer: AccessLayerRef,
114 pub(crate) manifest_ctx: Arc<ManifestContext>,
115 pub(crate) current_version: CompactionVersion,
116 pub(crate) file_purger: Option<Arc<LocalFilePurger>>,
117 pub(crate) ttl: Option<TimeToLive>,
118
119 pub max_parallelism: usize,
124
125 pub(crate) plugins: Plugins,
126}
127
128#[derive(Clone)]
133pub struct CompactionContext {
134 pub region_id: RegionId,
136 pub region_options: RegionOptions,
138 version_options: RegionOptions,
140 pub(crate) engine_config: Arc<MitoConfig>,
141 region_metadata: RegionMetadataRef,
142 pub(crate) cache_manager: CacheManagerRef,
143 pub(crate) access_layer: AccessLayerRef,
144 manifest_ctx: Arc<ManifestContext>,
145 plugins: Plugins,
146}
147
148impl From<&CompactionRegion> for CompactionContext {
149 fn from(region: &CompactionRegion) -> Self {
150 Self {
151 region_id: region.region_id,
152 region_options: region.region_options.clone(),
153 version_options: region.current_version.options.clone(),
154 engine_config: region.engine_config.clone(),
155 region_metadata: region.region_metadata.clone(),
156 cache_manager: region.cache_manager.clone(),
157 access_layer: region.access_layer.clone(),
158 manifest_ctx: region.manifest_ctx.clone(),
159 plugins: region.plugins.clone(),
160 }
161 }
162}
163
164#[cfg(test)]
166pub(crate) async fn new_test_compaction_region() -> CompactionRegion {
167 let env = crate::test_util::scheduler_util::SchedulerEnv::new().await;
168 let metadata = crate::test_util::memtable_util::metadata_for_test();
169 let manifest_ctx = env.mock_manifest_context(metadata.clone()).await;
170 CompactionRegion {
171 region_id: RegionId::new(1, 1),
172 region_options: RegionOptions::default(),
173 engine_config: Arc::new(MitoConfig::default()),
174 region_metadata: metadata.clone(),
175 cache_manager: Arc::new(CacheManager::default()),
176 access_layer: env.access_layer.clone(),
177 manifest_ctx,
178 current_version: CompactionVersion {
179 metadata: metadata.clone(),
180 options: RegionOptions::default(),
181 ssts: Arc::new(SstVersion::new(metadata)),
182 memtable_min_sequence: None,
183 compaction_time_window: None,
184 },
185 file_purger: None,
186 ttl: None,
187 max_parallelism: 1,
188 plugins: Plugins::new(),
189 }
190}
191
192#[derive(Clone)]
194pub struct OpenCompactionRegionRequest {
195 pub region_id: RegionId,
196 pub table_dir: String,
197 pub path_type: PathType,
198 pub region_options: RegionOptions,
199 pub max_parallelism: usize,
200 pub plugins: Plugins,
202}
203
204#[derive(Clone)]
210pub struct CompactionRegionOpener {
211 mito_config: Arc<MitoConfig>,
212 object_store_manager: ObjectStoreManagerRef,
213 puffin_manager_factory: PuffinManagerFactory,
214 intermediate_manager: IntermediateManager,
215}
216
217impl CompactionRegionOpener {
218 pub async fn new(
221 mito_config: MitoConfig,
222 object_store_manager: ObjectStoreManagerRef,
223 ) -> Result<Self> {
224 let puffin_manager_factory = PuffinManagerFactory::new(
225 &mito_config.index.aux_path,
226 mito_config.index.staging_size.as_bytes(),
227 Some(mito_config.index.write_buffer_size.as_bytes() as _),
228 mito_config.index.staging_ttl,
229 )
230 .await?;
231 let intermediate_manager =
232 IntermediateManager::init_fs(&mito_config.index.aux_path).await?;
233
234 Ok(Self {
235 mito_config: Arc::new(mito_config),
236 object_store_manager,
237 puffin_manager_factory,
238 intermediate_manager,
239 })
240 }
241
242 pub async fn open(
244 &self,
245 req: &OpenCompactionRegionRequest,
246 ttl_provider: Either<TimeToLive, SchemaMetadataManagerRef>,
247 ) -> Result<CompactionRegion> {
248 let mito_config = &self.mito_config;
249 let object_store = {
250 let name = &req.region_options.storage;
251 if let Some(name) = name {
252 self.object_store_manager
253 .find(name)
254 .with_context(|| ObjectStoreNotFoundSnafu {
255 object_store: name.clone(),
256 })?
257 } else {
258 self.object_store_manager.default_object_store()
259 }
260 };
261
262 let access_layer = Arc::new(AccessLayer::new(
263 &req.table_dir,
264 req.path_type,
265 object_store.clone(),
266 self.puffin_manager_factory.clone(),
267 self.intermediate_manager.clone(),
268 ));
269
270 let manifest_manager = {
271 let region_dir =
272 region_dir_from_table_dir(&req.table_dir, req.region_id, req.path_type);
273 let region_manifest_options =
274 RegionManifestOptions::new(mito_config, ®ion_dir, object_store);
275
276 RegionManifestManager::open(region_manifest_options, &Default::default())
277 .await?
278 .with_context(|| EmptyRegionDirSnafu {
279 region_id: req.region_id,
280 region_dir: region_dir_from_table_dir(
281 &req.table_dir,
282 req.region_id,
283 req.path_type,
284 ),
285 })?
286 };
287
288 let manifest = manifest_manager.manifest();
289 let region_metadata = manifest.metadata.clone();
290 let hook: Option<RegionHookRef> = req.plugins.get();
291 let manifest_ctx = Arc::new(ManifestContext::new(
292 manifest_manager,
293 RegionRoleState::Leader(RegionLeaderState::Writable),
294 hook,
295 Default::default(),
296 ));
297
298 let file_purger = {
299 let purge_scheduler = Arc::new(LocalScheduler::new(mito_config.max_background_purges));
300 Arc::new(LocalFilePurger::new(
301 purge_scheduler.clone(),
302 access_layer.clone(),
303 None,
304 ))
305 };
306
307 let current_version = {
308 let mut ssts = SstVersion::new(region_metadata.clone());
309 ssts.add_files(file_purger.clone(), manifest.files.values().cloned());
310 CompactionVersion {
311 metadata: region_metadata.clone(),
312 options: req.region_options.clone(),
313 ssts: Arc::new(ssts),
314 memtable_min_sequence: Some(0),
317 compaction_time_window: manifest.compaction_time_window,
318 }
319 };
320
321 let ttl = match ttl_provider {
322 Either::Left(ttl) => ttl,
324 Either::Right(schema_metadata_manager) => {
326 let (_, ttl) = find_dynamic_options(
327 req.region_id,
328 &req.region_options,
329 &schema_metadata_manager,
330 )
331 .await
332 .unwrap_or_else(|e| {
333 warn!(e; "Failed to get ttl for region: {}", region_metadata.region_id);
334 (
335 crate::region::options::CompactionOptions::default(),
336 TimeToLive::default(),
337 )
338 });
339 ttl
340 }
341 };
342
343 Ok(CompactionRegion {
344 region_id: req.region_id,
345 region_options: req.region_options.clone(),
346 engine_config: self.mito_config.clone(),
347 region_metadata: region_metadata.clone(),
348 cache_manager: Arc::new(
350 CacheManager::builder()
351 .sst_meta_cache_size(mito_config.sst_meta_cache_size.as_bytes())
352 .disable_sst_meta_cache_on_write()
354 .build(),
355 ),
356 access_layer,
357 manifest_ctx,
358 current_version,
359 file_purger: Some(file_purger),
360 ttl: Some(ttl),
361 max_parallelism: req.max_parallelism,
362 plugins: req.plugins.clone(),
363 })
364 }
365}
366
367impl CompactionRegion {
368 pub fn file_purger(&self) -> Option<Arc<LocalFilePurger>> {
370 self.file_purger.clone()
371 }
372
373 pub async fn stop_purger_scheduler(&self) -> Result<()> {
375 if let Some(file_purger) = &self.file_purger {
376 file_purger.stop_scheduler().await
377 } else {
378 Ok(())
379 }
380 }
381
382 pub async fn invoke_sst_hook(&self, merge_output: &MergeOutput) {
387 CompactionContext::from(self)
388 .invoke_sst_hook(merge_output)
389 .await;
390 }
391}
392
393impl CompactionContext {
394 pub(crate) async fn invoke_sst_hook(&self, merge_output: &MergeOutput) {
396 let Some(hook) = self.plugins.get::<RegionHookRef>() else {
397 return;
398 };
399
400 let synthesized: Vec<SstInfo>;
405 let infos: &[SstInfo] = if merge_output.sst_infos.is_empty() {
406 synthesized = merge_output
407 .files_to_add
408 .iter()
409 .map(sst_info_from_file_meta)
410 .collect();
411 &synthesized
412 } else {
413 if merge_output.sst_infos.len() != merge_output.files_to_add.len() {
417 warn!(
418 "sst_infos length ({}) does not match files_to_add length ({}) for region {}",
419 merge_output.sst_infos.len(),
420 merge_output.files_to_add.len(),
421 self.region_id
422 );
423 }
424 &merge_output.sst_infos
425 };
426
427 let files: Vec<SstFileInfo<'_>> = merge_output
428 .files_to_add
429 .iter()
430 .zip(infos)
431 .map(|(meta, info)| SstFileInfo {
432 sst_info_ref: info,
433 file_meta: meta,
434 })
435 .collect();
436 hook.on_sst_files_written(self.region_id, &self.region_metadata, &files)
437 .await;
438 }
439
440 pub(crate) async fn update_manifest(
442 &self,
443 merge_output: MergeOutput,
444 ) -> Result<(RegionEdit, ManifestVersion)> {
445 let edit = RegionEdit {
446 files_to_add: merge_output.files_to_add,
447 files_to_remove: merge_output.files_to_remove,
448 timestamp_ms: Some(chrono::Utc::now().timestamp_millis()),
449 compaction_time_window: merge_output
450 .compaction_time_window
451 .map(|seconds| Duration::from_secs(seconds as u64)),
452 flushed_entry_id: None,
453 flushed_sequence: None,
454 committed_sequence: None,
455 };
456
457 let action_list = RegionMetaActionList::with_action(RegionMetaAction::Edit(edit.clone()));
458 let manifest_version = self
459 .manifest_ctx
460 .update_manifest_for_compaction(action_list)
461 .await?;
462
463 Ok((edit, manifest_version))
464 }
465}
466
467fn sst_info_from_file_meta(meta: &FileMeta) -> SstInfo {
490 SstInfo {
491 file_id: meta.file_id,
492 time_range: meta.time_range,
493 file_size: meta.file_size,
494 max_row_group_uncompressed_size: meta.max_row_group_uncompressed_size,
495 num_rows: meta.num_rows as usize,
496 num_row_groups: meta.num_row_groups,
497 num_series: meta.num_series,
498 ..Default::default()
499 }
500}
501
502#[derive(Default, Debug, Serialize, Deserialize)]
504pub struct MergeOutput {
505 pub files_to_add: Vec<FileMeta>,
506 pub files_to_remove: Vec<FileMeta>,
507 pub compaction_time_window: Option<i64>,
508 #[serde(skip)]
509 pub sst_infos: Vec<SstInfo>,
510}
511
512impl MergeOutput {
513 pub fn is_empty(&self) -> bool {
514 self.files_to_add.is_empty() && self.files_to_remove.is_empty()
515 }
516
517 pub fn input_file_size(&self) -> u64 {
518 self.files_to_remove.iter().map(|f| f.file_size).sum()
519 }
520
521 pub fn output_file_size(&self) -> u64 {
522 self.files_to_add.iter().map(|f| f.file_size).sum()
523 }
524}
525
526#[async_trait::async_trait]
528pub trait Compactor: Send + Sync + 'static {
529 async fn merge_ssts(
531 &self,
532 compaction_region: &CompactionRegion,
533 picker_output: PickerOutput,
534 ) -> Result<MergeOutput>;
535
536 async fn update_manifest(
538 &self,
539 compaction_region: &CompactionRegion,
540 merge_output: MergeOutput,
541 ) -> Result<(RegionEdit, ManifestVersion)>;
542}
543
544#[async_trait::async_trait]
549pub trait SstMerger: Send + Sync + 'static {
550 async fn merge_single_output(
551 &self,
552 context: CompactionContext,
553 output: CompactionOutput,
554 write_opts: WriteOptions,
555 ) -> Result<(Vec<FileMeta>, Vec<SstInfo>)>;
556}
557
558#[derive(Clone)]
560pub struct DefaultSstMerger;
561
562struct OutputSequenceMetadata {
564 file_sequence: Option<NonZeroU64>,
565 exact_sequence_trusted: bool,
566}
567
568fn output_sequence_metadata(
572 context: &CompactionContext,
573 inputs: &[FileHandle],
574) -> OutputSequenceMetadata {
575 let exact_sequence_trusted = context.region_options.preserve_row_sequence
576 && inputs
577 .iter()
578 .all(|f| f.is_effective_target_sequence_trusted(context.region_id));
579 OutputSequenceMetadata {
580 file_sequence: known_max_input_sequence(inputs),
581 exact_sequence_trusted,
582 }
583}
584
585fn known_max_input_sequence(inputs: &[FileHandle]) -> Option<NonZeroU64> {
588 crate::sst::file::known_max_input_sequence(inputs.iter().map(FileHandle::meta_ref))
589}
590
591#[async_trait::async_trait]
592impl SstMerger for DefaultSstMerger {
593 async fn merge_single_output(
594 &self,
595 context: CompactionContext,
596 output: CompactionOutput,
597 write_opts: WriteOptions,
598 ) -> Result<(Vec<FileMeta>, Vec<SstInfo>)> {
599 let region_id = context.region_id;
600 let index_options = context.version_options.index_options.clone();
601 let append_mode = context.version_options.append_mode;
602 let merge_mode = context.version_options.merge_mode();
603 let flat_format = context
604 .region_options
605 .sst_format
606 .map(|format| format == FormatType::Flat)
607 .unwrap_or(context.engine_config.default_flat_format);
608
609 let index_config = context.engine_config.index.clone();
610 let inverted_index_config = context.engine_config.inverted_index.clone();
611 let fulltext_index_config = context.engine_config.fulltext_index.clone();
612 let bloom_filter_index_config = context.engine_config.bloom_filter_index.clone();
613
614 let input_file_names = output
615 .inputs
616 .iter()
617 .map(|f| f.file_id().to_string())
618 .join(",");
619 let sequence_metadata = output_sequence_metadata(&context, &output.inputs);
620 let builder = CompactionSstReaderBuilder {
621 metadata: context.region_metadata.clone(),
622 sst_layer: context.access_layer.clone(),
623 cache: context.cache_manager.clone(),
624 inputs: &output.inputs,
625 append_mode,
626 filter_deleted: output.filter_deleted,
627 time_range: output.output_time_range,
628 merge_mode,
629 read_parallelism: write_opts.encode_parallelism,
630 };
631 let source = spawn_merge(builder.build_flat_sst_reader().await?);
632
633 let mut metrics = Metrics::new(WriteType::Compaction);
634 let region_metadata = context.region_metadata.clone();
635 let sst_infos = context
636 .access_layer
637 .write_sst(
638 SstWriteRequest {
639 op_type: OperationType::Compact,
640 metadata: region_metadata.clone(),
641 source,
642 cache_manager: context.cache_manager.clone(),
643 max_sequence: None,
647 sst_write_format: if flat_format {
648 FormatType::Flat
649 } else {
650 FormatType::PrimaryKey
651 },
652 preserve_row_sequence: sequence_metadata.exact_sequence_trusted,
653 index_options,
654 index_config,
655 inverted_index_config,
656 fulltext_index_config,
657 bloom_filter_index_config,
658 },
659 &write_opts,
660 &mut metrics,
661 )
662 .await?;
663 let partition_expr = match ®ion_metadata.partition_expr {
665 None => None,
666 Some(json_str) if json_str.is_empty() => None,
667 Some(json_str) => PartitionExpr::from_json_str(json_str).with_context(|_| {
668 InvalidPartitionExprSnafu {
669 expr: json_str.clone(),
670 }
671 })?,
672 };
673
674 let output_files = sst_infos
675 .iter()
676 .map(|sst_info| {
677 let pk_range = sst_info
678 .file_metadata
679 .as_ref()
680 .and_then(|meta| extract_primary_key_range(meta, ®ion_metadata));
681 let (primary_key_min, primary_key_max) = match pk_range {
682 Some((min, max)) => (Some(min), Some(max)),
683 None => (None, None),
684 };
685
686 FileMeta {
687 region_id,
688 file_id: sst_info.file_id,
689 time_range: sst_info.time_range,
690 level: output.output_level,
691 file_size: sst_info.file_size,
692 max_row_group_uncompressed_size: sst_info.max_row_group_uncompressed_size,
693 available_indexes: sst_info.index_metadata.build_available_indexes(),
694 indexes: sst_info.index_metadata.build_indexes(),
695 index_file_size: sst_info.index_metadata.file_size,
696 index_version: 0,
697 num_rows: sst_info.num_rows as u64,
698 num_row_groups: sst_info.num_row_groups,
699 sequence: sequence_metadata.file_sequence,
700 partition_expr: partition_expr.clone(),
701 num_series: sst_info.num_series,
702 primary_key_min,
703 primary_key_max,
704 preserve_row_sequence: sequence_metadata.exact_sequence_trusted,
705 }
706 })
707 .collect::<Vec<_>>();
708 let output_file_names = output_files.iter().map(|f| f.file_id.to_string()).join(",");
709 info!(
710 "Region {} compaction inputs: [{}], outputs: [{}], flat_format: {}, metrics: {:?}",
711 region_id, input_file_names, output_file_names, flat_format, metrics
712 );
713 metrics.observe();
714 Ok((output_files, sst_infos.into_iter().collect()))
715 }
716}
717
718pub struct DefaultCompactor<M = DefaultSstMerger> {
723 merger: M,
724 cancel_handle: Arc<CancellationHandle>,
725 uncommitted: Option<UncommittedSsts>,
726}
727
728#[cfg(test)]
729impl<M: SstMerger> DefaultCompactor<M> {
730 pub fn with_merger(merger: M) -> Self {
731 Self {
732 merger,
733 cancel_handle: Arc::new(CancellationHandle::default()),
734 uncommitted: None,
735 }
736 }
737}
738
739impl DefaultCompactor {
740 pub fn new_with_cancel_handle(cancel_handle: Arc<CancellationHandle>) -> Self {
750 Self {
751 merger: DefaultSstMerger,
752 cancel_handle,
753 uncommitted: None,
754 }
755 }
756
757 pub(crate) fn with_cancel_handle(
758 cancel_handle: Arc<CancellationHandle>,
759 uncommitted: UncommittedSsts,
760 ) -> Self {
761 Self {
762 merger: DefaultSstMerger,
763 cancel_handle,
764 uncommitted: Some(uncommitted),
765 }
766 }
767}
768
769impl<M: SstMerger> DefaultCompactor<M> {
770 pub(crate) async fn merge_unit(
772 &self,
773 context: &CompactionContext,
774 unit: &CompactionUnit,
775 ) -> Result<MergeOutput> {
776 let mut result = MergeOutput {
777 files_to_remove: unit.inputs.iter().map(|f| f.meta_ref().clone()).collect(),
778 compaction_time_window: Some(unit.time_window_size),
779 ..Default::default()
780 };
781 for output in &unit.outputs {
782 if self.cancel_handle.is_cancelled() {
783 return error::CompactionCancelledSnafu.fail();
784 }
785 let (files, infos) = self
788 .merger
789 .merge_single_output(
790 context.clone(),
791 output.clone(),
792 WriteOptions {
793 write_buffer_size: context.engine_config.sst_write_buffer_size,
794 max_file_size: unit.max_file_size,
795 row_group_size: context.region_options.row_group_size(),
796 float_field_encoding: context.region_options.float_field_encoding,
797 encode_parallelism: context
798 .region_options
799 .compaction_output_parallelism_or(
800 context
801 .engine_config
802 .experimental_compaction_output_parallelism,
803 ),
804 encode_runtime: EncodeRuntime::Compact,
805 },
806 )
807 .await?;
808 if let Some(uncommitted) = &self.uncommitted {
809 uncommitted.track(&infos);
810 }
811 result.files_to_add.extend(files);
812 result.sst_infos.extend(infos);
813 }
814 if self.cancel_handle.is_cancelled() {
815 return error::CompactionCancelledSnafu.fail();
816 }
817 Ok(result)
818 }
819}
820
821#[async_trait::async_trait]
822impl<M: SstMerger> Compactor for DefaultCompactor<M>
823where
824 M: Clone,
825{
826 async fn merge_ssts(
827 &self,
828 compaction_region: &CompactionRegion,
829 mut picker_output: PickerOutput,
830 ) -> Result<MergeOutput> {
831 let internal_parallelism = compaction_region.max_parallelism.max(1);
832 let compaction_time_window = picker_output.time_window_size;
833 let region_id = compaction_region.region_id;
834 let context = CompactionContext::from(compaction_region);
835
836 let mut tasks: Vec<(Vec<FileMeta>, _)> = Vec::with_capacity(picker_output.outputs.len());
839
840 for output in picker_output.outputs.drain(..) {
841 let inputs_to_remove: Vec<_> =
842 output.inputs.iter().map(|f| f.meta_ref().clone()).collect();
843 let write_opts = WriteOptions {
844 write_buffer_size: compaction_region.engine_config.sst_write_buffer_size,
845 max_file_size: picker_output.max_file_size,
846 row_group_size: compaction_region.region_options.row_group_size(),
847 float_field_encoding: compaction_region.region_options.float_field_encoding,
848 encode_parallelism: compaction_region
849 .region_options
850 .compaction_output_parallelism_or(
851 compaction_region
852 .engine_config
853 .experimental_compaction_output_parallelism,
854 ),
855 encode_runtime: EncodeRuntime::Compact,
856 };
857 let merger = self.merger.clone();
858 let context = context.clone();
859 let uncommitted = self.uncommitted.clone();
860 let fut = async move {
861 let result = merger
862 .merge_single_output(context, output, write_opts)
863 .await;
864 if let (Some(uncommitted), Ok((_, infos))) = (&uncommitted, &result) {
865 uncommitted.track(infos);
866 }
867 result
868 };
869 tasks.push((inputs_to_remove, fut));
870 }
871
872 let hook: Option<RegionHookRef> = compaction_region.plugins.get();
873 let mut output_files = Vec::with_capacity(tasks.len());
874 let mut all_sst_infos: Vec<SstInfo> = Vec::new();
875 let mut compacted_inputs = Vec::with_capacity(
876 tasks.iter().map(|(inputs, _)| inputs.len()).sum::<usize>()
877 + picker_output.expired_ssts.len(),
878 );
879
880 while !tasks.is_empty() && !self.cancel_handle.is_cancelled() {
881 let mut chunk: Vec<(Vec<FileMeta>, _)> = Vec::with_capacity(internal_parallelism);
882 for _ in 0..internal_parallelism {
883 if let Some(task) = tasks.pop() {
884 chunk.push(task);
885 }
886 }
887 let mut spawned: Vec<_> = chunk
888 .into_iter()
889 .map(|(inputs, fut)| {
890 let handle = common_runtime::spawn_compact(fut);
891 (inputs, handle)
892 })
893 .collect();
894
895 let mut join_error = None;
899 while let Some((inputs, handle)) = spawned.pop() {
900 match handle.await {
901 Ok(Ok((files, infos))) => {
902 output_files.extend(files);
903 if hook.is_some() {
904 all_sst_infos.extend(infos);
905 }
906 compacted_inputs.extend(inputs);
907 }
908 Ok(Err(e)) => {
909 warn!(
910 e; "Failed to merge compaction output for region: {}, inputs: [{}]",
911 region_id,
912 inputs.iter().map(|f| f.file_id.to_string()).join(",")
913 );
914 }
915 Err(e) => {
916 warn!(
917 "Region {} compaction task join error for inputs: [{}]: {}",
918 region_id,
919 inputs.iter().map(|f| f.file_id.to_string()).join(","),
920 e
921 );
922 join_error.get_or_insert(e);
924 }
925 }
926 }
927 if let Some(e) = join_error {
928 return Err(e).context(error::JoinSnafu);
929 }
930
931 if self.cancel_handle.is_cancelled() {
932 info!("Compaction merge cancelled for region: {}", region_id);
933 break;
934 }
935 }
936
937 compacted_inputs.extend(
939 picker_output
940 .expired_ssts
941 .iter()
942 .map(|f| f.meta_ref().clone()),
943 );
944
945 Ok(MergeOutput {
946 files_to_add: output_files,
947 files_to_remove: compacted_inputs,
948 compaction_time_window: Some(compaction_time_window),
949 sst_infos: all_sst_infos,
950 })
951 }
952
953 async fn update_manifest(
954 &self,
955 compaction_region: &CompactionRegion,
956 merge_output: MergeOutput,
957 ) -> Result<(RegionEdit, ManifestVersion)> {
958 CompactionContext::from(compaction_region)
959 .update_manifest(merge_output)
960 .await
961 }
962}
963
964#[cfg(test)]
965mod tests {
966 use std::sync::atomic::{AtomicUsize, Ordering};
967 use std::sync::{Arc, Mutex, Weak};
968 use std::time::Duration;
969
970 use datatypes::arrow::record_batch::RecordBatch;
971 use parquet::arrow::ArrowWriter;
972 use store_api::storage::{FileId, RegionId};
973 use tokio::sync::{mpsc, oneshot};
974 use tokio::time::timeout;
975
976 use super::{DefaultCompactor, *};
977 use crate::compaction::memory_manager::new_compaction_memory_manager;
978 use crate::compaction::picker::PickerOutput;
979 use crate::error::Result;
980 use crate::sst::file::FileHandle;
981 use crate::sst::file_purger::NoopFilePurger;
982 use crate::test_util::sst_util::new_record_batch_by_range;
983
984 fn dummy_file_meta() -> FileMeta {
985 FileMeta {
986 region_id: RegionId::new(1, 1),
987 file_id: FileId::random(),
988 file_size: 100,
989 ..Default::default()
990 }
991 }
992
993 fn new_file_handle(meta: FileMeta) -> FileHandle {
994 FileHandle::new(meta, Arc::new(NoopFilePurger))
995 }
996
997 #[test]
998 fn test_effective_target_trust_and_max_sequence() {
999 let target = RegionId::new(1, 1);
1000 let local_trusted = new_file_handle(FileMeta {
1001 region_id: target,
1002 sequence: NonZeroU64::new(3),
1003 preserve_row_sequence: true,
1004 ..dummy_file_meta()
1005 });
1006 let foreign_untrusted_marker = new_file_handle(FileMeta {
1007 region_id: RegionId::new(1, 2),
1008 sequence: NonZeroU64::new(9),
1009 preserve_row_sequence: false,
1010 ..dummy_file_meta()
1011 });
1012 let local_untrusted = new_file_handle(FileMeta {
1013 region_id: target,
1014 sequence: NonZeroU64::new(11),
1015 preserve_row_sequence: false,
1016 ..dummy_file_meta()
1017 });
1018 assert!(local_trusted.is_effective_target_sequence_trusted(target));
1019 assert!(foreign_untrusted_marker.is_effective_target_sequence_trusted(target));
1020 assert!(!local_untrusted.is_effective_target_sequence_trusted(target));
1021 assert_eq!(
1022 NonZeroU64::new(9),
1023 known_max_input_sequence(&[local_trusted, foreign_untrusted_marker])
1024 );
1025 }
1026
1027 #[test]
1028 fn test_known_max_input_sequence() {
1029 let meta_with = |sequence| FileMeta {
1030 sequence,
1031 ..dummy_file_meta()
1032 };
1033
1034 let inputs = vec![
1035 new_file_handle(meta_with(NonZeroU64::new(3))),
1036 new_file_handle(meta_with(NonZeroU64::new(9))),
1037 new_file_handle(meta_with(NonZeroU64::new(5))),
1038 ];
1039 assert_eq!(NonZeroU64::new(9), known_max_input_sequence(&inputs));
1040
1041 let inputs = vec![
1042 new_file_handle(meta_with(NonZeroU64::new(9))),
1043 new_file_handle(meta_with(None)),
1044 ];
1045 assert_eq!(None, known_max_input_sequence(&inputs));
1046
1047 assert_eq!(None, known_max_input_sequence(&[]));
1048 }
1049
1050 #[tokio::test]
1051 async fn test_output_sequence_metadata_preserves_trust_and_unknown_bounds() {
1052 let mut region = new_test_compaction_region().await;
1053 let local = region.region_id;
1054 let foreign = RegionId::new(1, 2);
1055 let file = |region_id, sequence: Option<u64>, preserve_row_sequence| {
1056 new_file_handle(FileMeta {
1057 region_id,
1058 sequence: sequence.and_then(NonZeroU64::new),
1059 preserve_row_sequence,
1060 ..dummy_file_meta()
1061 })
1062 };
1063 let cases = [
1066 (
1067 vec![file(local, Some(3), true), file(foreign, Some(9), false)],
1068 Some(9),
1069 true,
1070 ),
1071 (
1072 vec![file(local, Some(3), true), file(local, Some(6), false)],
1073 Some(6),
1074 false,
1075 ),
1076 (
1077 vec![file(foreign, Some(9), false), file(foreign, Some(11), true)],
1078 Some(11),
1079 true,
1080 ),
1081 (
1082 vec![file(local, Some(3), true), file(local, None, true)],
1083 None,
1084 true,
1085 ),
1086 (
1087 vec![file(local, None, false), file(local, Some(6), true)],
1088 None,
1089 false,
1090 ),
1091 (
1092 vec![file(local, Some(3), true), file(foreign, None, true)],
1093 None,
1094 false,
1095 ),
1096 (vec![], None, true),
1097 (
1098 vec![file(local, Some(u64::MAX), false)],
1099 Some(u64::MAX),
1100 false,
1101 ),
1102 ];
1103 for committed_sequence in [100, 101] {
1104 region
1105 .manifest_ctx
1106 .manifest_manager
1107 .write()
1108 .await
1109 .update(
1110 RegionMetaActionList::with_action(RegionMetaAction::Edit(RegionEdit {
1111 committed_sequence: Some(committed_sequence),
1112 files_to_add: vec![],
1113 files_to_remove: vec![],
1114 timestamp_ms: None,
1115 compaction_time_window: None,
1116 flushed_entry_id: None,
1117 flushed_sequence: None,
1118 })),
1119 false,
1120 )
1121 .await
1122 .unwrap();
1123 for preserve in [false, true] {
1124 region.region_options.preserve_row_sequence = preserve;
1125 for (inputs, known_bound, inputs_trusted) in &cases {
1126 let metadata =
1127 output_sequence_metadata(&CompactionContext::from(®ion), inputs);
1128 let trusted = preserve && *inputs_trusted;
1129 assert_eq!(trusted, metadata.exact_sequence_trusted);
1130 assert_eq!(
1131 known_bound.and_then(NonZeroU64::new),
1132 metadata.file_sequence
1133 );
1134 }
1135 }
1136 }
1137 }
1138
1139 #[rstest::rstest]
1143 #[case(true)]
1144 #[case(false)]
1145 #[tokio::test]
1146 async fn test_independent_outputs_preserve_delete_in_every_visibility_state(
1147 #[case] flat_format: bool,
1148 #[values(false, true)] unknown_put_bound: bool,
1149 ) {
1150 use datatypes::arrow::array::AsArray;
1151 use datatypes::arrow::datatypes::TimestampMillisecondType;
1152 use store_api::region_engine::RegionEngine;
1153 use store_api::region_request::RegionRequest;
1154
1155 use crate::compaction::CompactionOutput;
1156 use crate::compaction::compactor::{CompactionRegion, Compactor, DefaultCompactor};
1157 use crate::compaction::picker::PickerOutput;
1158 use crate::compaction::reader::CompactionSstReaderBuilder;
1159 use crate::engine::compaction_test::{delete_and_flush, put_and_flush};
1160 use crate::region::options::MergeMode;
1161 use crate::sst::file::FileHandle;
1162 use crate::sst::file_purger::NoopFilePurger;
1163 use crate::test_util::{CreateRequestBuilder, TestEnv, rows_schema};
1164
1165 let mut env = TestEnv::new().await;
1166 let region_id = RegionId::new(1, 1);
1167 let config = MitoConfig {
1168 default_flat_format: flat_format,
1169 min_compaction_interval: Duration::from_secs(3600),
1170 ..Default::default()
1171 };
1172 let engine = env.create_engine(config).await;
1173 let request = CreateRequestBuilder::new().build();
1174 let columns = rows_schema(&request);
1175 engine
1176 .handle_request(region_id, RegionRequest::Create(request))
1177 .await
1178 .unwrap();
1179 put_and_flush(&engine, region_id, &columns, 0..1).await;
1180 delete_and_flush(&engine, region_id, &columns, 0..1).await;
1181 put_and_flush(&engine, region_id, &columns, 10..11).await;
1182 let region = engine.get_region(region_id).unwrap();
1183 let version = region.version();
1184 let mut files: Vec<_> = version
1185 .ssts
1186 .levels()
1187 .iter()
1188 .flat_map(|level| level.files())
1189 .cloned()
1190 .collect();
1191 assert_eq!(3, files.len());
1192 files.sort_unstable_by_key(|file| file.meta_ref().sequence);
1193 if unknown_put_bound {
1194 let mut meta = files[0].meta_ref().clone();
1197 meta.sequence = None;
1198 files[0] = FileHandle::new(meta, Arc::new(NoopFilePurger));
1199 }
1200 let compaction_region = CompactionRegion {
1201 region_id,
1202 region_options: version.options.clone(),
1203 engine_config: Arc::new(MitoConfig {
1204 default_flat_format: flat_format,
1205 ..Default::default()
1206 }),
1207 region_metadata: version.metadata.clone(),
1208 cache_manager: engine.cache_manager(),
1209 access_layer: region.access_layer.clone(),
1210 manifest_ctx: region.manifest_ctx.clone(),
1211 current_version: version.into(),
1212 file_purger: None,
1213 ttl: None,
1214 max_parallelism: 2,
1215 plugins: common_base::Plugins::new(),
1216 };
1217 let puts = vec![files[0].clone(), files[2].clone()];
1218 let deletes = vec![files[1].clone()];
1219 let picker_output = PickerOutput {
1220 outputs: [puts.clone(), deletes.clone()]
1221 .into_iter()
1222 .map(|inputs| CompactionOutput {
1223 output_level: 1,
1224 inputs,
1225 filter_deleted: false,
1226 output_time_range: None,
1227 })
1228 .collect(),
1229 expired_ssts: vec![],
1230 time_window_size: 3600,
1231 max_file_size: None,
1232 };
1233 let merged = DefaultCompactor::with_merger(DefaultSstMerger)
1234 .merge_ssts(&compaction_region, picker_output)
1235 .await
1236 .unwrap();
1237 assert_eq!(2, merged.files_to_add.len());
1238 let outputs: Vec<_> = merged
1239 .files_to_add
1240 .into_iter()
1241 .map(|meta| FileHandle::new(meta, Arc::new(NoopFilePurger)))
1242 .collect();
1243 let put_output = outputs
1244 .iter()
1245 .find(|file| file.meta_ref().num_rows == 2)
1246 .unwrap();
1247 let delete_output = outputs
1248 .iter()
1249 .find(|file| file.meta_ref().num_rows == 1)
1250 .unwrap();
1251 assert_eq!(
1252 if unknown_put_bound {
1253 None
1254 } else {
1255 NonZeroU64::new(3)
1256 },
1257 put_output.meta_ref().sequence,
1258 );
1259 assert_eq!(NonZeroU64::new(2), delete_output.meta_ref().sequence);
1260 assert!(
1261 outputs
1262 .iter()
1263 .all(|file| !file.meta_ref().preserve_row_sequence)
1264 );
1265
1266 for (put_done, delete_done) in [(false, false), (true, false), (false, true), (true, true)]
1269 {
1270 let mut visible = if put_done {
1271 vec![put_output.clone()]
1272 } else {
1273 puts.clone()
1274 };
1275 visible.extend(if delete_done {
1276 vec![delete_output.clone()]
1277 } else {
1278 deletes.clone()
1279 });
1280 let mut reader = CompactionSstReaderBuilder {
1281 metadata: compaction_region.region_metadata.clone(),
1282 sst_layer: region.access_layer.clone(),
1283 cache: engine.cache_manager(),
1284 inputs: &visible,
1285 append_mode: false,
1286 filter_deleted: true,
1287 time_range: None,
1288 merge_mode: MergeMode::LastRow,
1289 read_parallelism: 1,
1290 }
1291 .build_flat_sst_reader()
1292 .await
1293 .unwrap();
1294 let mut timestamps = Vec::new();
1295 while let Some(batch) = reader.next_batch().await.unwrap() {
1296 timestamps.extend(
1297 batch
1298 .column_by_name("ts")
1299 .unwrap()
1300 .as_primitive::<TimestampMillisecondType>()
1301 .values()
1302 .iter()
1303 .copied(),
1304 );
1305 }
1306 assert_eq!(
1307 vec![10_000],
1308 timestamps,
1309 "put_done={put_done}, delete_done={delete_done}"
1310 );
1311 }
1312 }
1313
1314 #[rstest::rstest]
1315 #[case::primary_key(false)]
1316 #[case::flat(true)]
1317 #[tokio::test]
1318 async fn test_compaction_skips_missing_input(#[case] flat_format: bool) {
1319 use datatypes::arrow::array::AsArray;
1320 use datatypes::arrow::datatypes::TimestampMillisecondType;
1321 use store_api::region_engine::RegionEngine;
1322 use store_api::region_request::RegionRequest;
1323
1324 use crate::engine::compaction_test::put_and_flush;
1325 use crate::test_util::{CreateRequestBuilder, TestEnv, rows_schema};
1326
1327 let mut env = TestEnv::new().await;
1328 let config = MitoConfig {
1329 default_flat_format: flat_format,
1330 min_compaction_interval: Duration::from_secs(3600),
1331 ..Default::default()
1332 };
1333 let engine = env.create_engine(config.clone()).await;
1334 let region_id = RegionId::new(1, 1);
1335 let request = CreateRequestBuilder::new().build();
1336 let columns = rows_schema(&request);
1337 engine
1338 .handle_request(region_id, RegionRequest::Create(request))
1339 .await
1340 .unwrap();
1341 for file in 0..9 {
1343 put_and_flush(&engine, region_id, &columns, file * 2..file * 2 + 2).await;
1344 }
1345 let region = engine.get_region(region_id).unwrap();
1346 let version = region.version();
1347 let mut inputs: Vec<_> = version.ssts.levels()[0].files().cloned().collect();
1348 assert_eq!(9, inputs.len());
1349 inputs.sort_unstable_by_key(|file| file.meta_ref().sequence);
1350 let missing = inputs[4].clone();
1351 let output = CompactionOutput {
1352 output_level: 1,
1353 inputs,
1354 filter_deleted: true,
1355 output_time_range: None,
1356 };
1357 let context = CompactionContext {
1360 region_id,
1361 region_options: version.options.clone(),
1362 version_options: version.options.clone(),
1363 engine_config: Arc::new(config),
1364 region_metadata: version.metadata.clone(),
1365 cache_manager: Arc::new(
1366 CacheManager::builder()
1367 .sst_meta_cache_size(1024 * 1024)
1368 .build(),
1369 ),
1370 access_layer: region.access_layer.clone(),
1371 manifest_ctx: region.manifest_ctx.clone(),
1372 plugins: Plugins::new(),
1373 };
1374 region
1375 .access_layer
1376 .object_store()
1377 .delete(&missing.file_path(
1378 region.access_layer.table_dir(),
1379 region.access_layer.path_type(),
1380 ))
1381 .await
1382 .unwrap();
1383 let (files, _) = DefaultSstMerger
1384 .merge_single_output(context, output, WriteOptions::default())
1385 .await
1386 .unwrap();
1387 assert_eq!(1, files.len());
1388 let outputs: Vec<_> = files.into_iter().map(new_file_handle).collect();
1389 let mut reader = CompactionSstReaderBuilder {
1390 metadata: version.metadata.clone(),
1391 sst_layer: region.access_layer.clone(),
1392 cache: Arc::new(CacheManager::default()),
1393 inputs: &outputs,
1394 append_mode: false,
1395 filter_deleted: true,
1396 time_range: None,
1397 merge_mode: MergeMode::LastRow,
1398 read_parallelism: 1,
1399 }
1400 .build_flat_sst_reader()
1401 .await
1402 .unwrap();
1403 let mut timestamps = Vec::new();
1404 while let Some(batch) = reader.next_batch().await.unwrap() {
1405 timestamps.extend_from_slice(
1406 batch
1407 .column_by_name("ts")
1408 .unwrap()
1409 .as_primitive::<TimestampMillisecondType>()
1410 .values(),
1411 );
1412 }
1413 let expected: Vec<i64> = (0..18)
1414 .filter(|ts| !(8..10).contains(ts))
1415 .map(|ts| ts * 1000)
1416 .collect();
1417 assert_eq!(expected, timestamps);
1418 }
1419
1420 #[derive(Clone)]
1424 struct MockMerger {
1425 results: Arc<Mutex<Vec<Result<Vec<FileMeta>>>>>,
1426 call_idx: Arc<AtomicUsize>,
1427 }
1428
1429 impl MockMerger {
1430 fn new(results: Vec<Result<Vec<FileMeta>>>) -> Self {
1431 Self {
1432 results: Arc::new(Mutex::new(results)),
1433 call_idx: Arc::new(AtomicUsize::new(0)),
1434 }
1435 }
1436 }
1437
1438 #[async_trait::async_trait]
1439 impl SstMerger for MockMerger {
1440 async fn merge_single_output(
1441 &self,
1442 _context: CompactionContext,
1443 _output: CompactionOutput,
1444 _write_opts: WriteOptions,
1445 ) -> Result<(Vec<FileMeta>, Vec<SstInfo>)> {
1446 let idx = self.call_idx.fetch_add(1, Ordering::SeqCst);
1447 match self.results.lock().unwrap().get(idx) {
1448 Some(Ok(files)) => Ok((files.clone(), Vec::new())),
1449 Some(Err(_)) => error::InvalidMetaSnafu {
1450 reason: format!("simulated failure at index {idx}"),
1451 }
1452 .fail(),
1453 None => panic!("MockMerger: no result configured for call index {idx}"),
1454 }
1455 }
1456 }
1457
1458 #[tokio::test]
1459 async fn test_compaction_unit_group_failure_is_atomic() {
1460 let context = CompactionContext::from(&new_test_compaction_region().await);
1461 let input = new_file_handle(dummy_file_meta());
1462 let unit = CompactionUnit::from_picker(PickerOutput {
1463 outputs: (0..2)
1464 .map(|_| CompactionOutput {
1465 output_level: 1,
1466 inputs: vec![input.clone()],
1467 filter_deleted: false,
1468 output_time_range: None,
1469 })
1470 .collect(),
1471 ..Default::default()
1472 })
1473 .unwrap()
1474 .pop()
1475 .unwrap();
1476 for fail_second in [false, true] {
1477 let second = if fail_second {
1478 error::InvalidMetaSnafu {
1479 reason: "second output failed",
1480 }
1481 .fail()
1482 } else {
1483 Ok(vec![dummy_file_meta(), dummy_file_meta()])
1484 };
1485 let compactor =
1486 DefaultCompactor::with_merger(MockMerger::new(vec![Ok(Vec::new()), second]));
1487 let result = compactor.merge_unit(&context, &unit).await;
1488 if fail_second {
1489 assert!(
1490 result.is_err(),
1491 "cannot publish a successful subset of a dependency group"
1492 );
1493 } else {
1494 let result = result.unwrap();
1495 assert_eq!(2, result.files_to_add.len());
1496 assert_eq!(
1497 vec![input.meta_ref().file_id],
1498 result
1499 .files_to_remove
1500 .iter()
1501 .map(|f| f.file_id)
1502 .collect::<Vec<_>>()
1503 );
1504 }
1505 }
1506 }
1507
1508 #[tokio::test]
1509 async fn test_partial_merge_failure_collects_only_successful_outputs() {
1510 common_telemetry::init_default_ut_logging();
1511
1512 let compaction_region = new_test_compaction_region().await;
1513
1514 let input_meta_0 = dummy_file_meta();
1516 let input_meta_1 = dummy_file_meta();
1517 let input_meta_2 = dummy_file_meta();
1518
1519 let output_meta_0 = vec![dummy_file_meta()];
1520 let output_meta_2 = vec![dummy_file_meta(), dummy_file_meta()];
1521
1522 let merger = MockMerger::new(vec![
1523 Ok(output_meta_0.clone()),
1524 Err(error::InvalidMetaSnafu {
1525 reason: "boom".to_string(),
1526 }
1527 .build()),
1528 Ok(output_meta_2.clone()),
1529 ]);
1530 let compactor = DefaultCompactor::with_merger(merger);
1531
1532 let picker_output = PickerOutput {
1533 outputs: vec![
1534 CompactionOutput {
1535 output_level: 1,
1536 inputs: vec![new_file_handle(input_meta_0.clone())],
1537 filter_deleted: false,
1538 output_time_range: None,
1539 },
1540 CompactionOutput {
1541 output_level: 1,
1542 inputs: vec![new_file_handle(input_meta_1.clone())],
1543 filter_deleted: false,
1544 output_time_range: None,
1545 },
1546 CompactionOutput {
1547 output_level: 1,
1548 inputs: vec![new_file_handle(input_meta_2.clone())],
1549 filter_deleted: false,
1550 output_time_range: None,
1551 },
1552 ],
1553 expired_ssts: vec![],
1554 time_window_size: 3600,
1555 max_file_size: None,
1556 };
1557
1558 let merge_output = compactor
1559 .merge_ssts(&compaction_region, picker_output)
1560 .await
1561 .unwrap();
1562
1563 assert_eq!(merge_output.files_to_add.len(), 3);
1565 assert_eq!(merge_output.files_to_remove.len(), 2);
1567
1568 let removed_ids: Vec<_> = merge_output
1569 .files_to_remove
1570 .iter()
1571 .map(|f| f.file_id)
1572 .collect();
1573 assert!(removed_ids.contains(&input_meta_0.file_id));
1574 assert!(removed_ids.contains(&input_meta_2.file_id));
1575 assert!(!removed_ids.contains(&input_meta_1.file_id));
1577 }
1578
1579 #[tokio::test]
1580 async fn test_all_outputs_succeed() {
1581 common_telemetry::init_default_ut_logging();
1582
1583 let compaction_region = new_test_compaction_region().await;
1584 let input_meta = dummy_file_meta();
1585 let output_meta = vec![dummy_file_meta()];
1586
1587 let merger = MockMerger::new(vec![Ok(output_meta.clone())]);
1588 let compactor = DefaultCompactor::with_merger(merger);
1589
1590 let picker_output = PickerOutput {
1591 outputs: vec![CompactionOutput {
1592 output_level: 1,
1593 inputs: vec![new_file_handle(input_meta.clone())],
1594 filter_deleted: false,
1595 output_time_range: None,
1596 }],
1597 expired_ssts: vec![],
1598 time_window_size: 3600,
1599 max_file_size: None,
1600 };
1601
1602 let merge_output = compactor
1603 .merge_ssts(&compaction_region, picker_output)
1604 .await
1605 .unwrap();
1606
1607 assert_eq!(merge_output.files_to_add.len(), 1);
1608 assert_eq!(merge_output.files_to_add[0].file_id, output_meta[0].file_id);
1609 assert_eq!(merge_output.files_to_remove.len(), 1);
1610 assert_eq!(merge_output.files_to_remove[0].file_id, input_meta.file_id);
1611 }
1612
1613 #[tokio::test]
1614 async fn test_expired_ssts_always_removed() {
1615 common_telemetry::init_default_ut_logging();
1616
1617 let compaction_region = new_test_compaction_region().await;
1618 let input_meta = dummy_file_meta();
1619 let expired_meta = dummy_file_meta();
1620
1621 let merger = MockMerger::new(vec![Err(error::InvalidMetaSnafu {
1623 reason: "fail".to_string(),
1624 }
1625 .build())]);
1626 let compactor = DefaultCompactor::with_merger(merger);
1627
1628 let picker_output = PickerOutput {
1629 outputs: vec![CompactionOutput {
1630 output_level: 1,
1631 inputs: vec![new_file_handle(input_meta.clone())],
1632 filter_deleted: false,
1633 output_time_range: None,
1634 }],
1635 expired_ssts: vec![new_file_handle(expired_meta.clone())],
1636 time_window_size: 3600,
1637 max_file_size: None,
1638 };
1639
1640 let merge_output = compactor
1641 .merge_ssts(&compaction_region, picker_output)
1642 .await
1643 .unwrap();
1644
1645 assert!(merge_output.files_to_add.is_empty());
1647 assert_eq!(merge_output.files_to_remove.len(), 1);
1649 assert_eq!(
1650 merge_output.files_to_remove[0].file_id,
1651 expired_meta.file_id
1652 );
1653 }
1654
1655 #[derive(Debug)]
1656 struct PendingEncoding {
1657 input_id: FileId,
1658 output_id: FileId,
1659 batch: Weak<RecordBatch>,
1660 release: oneshot::Sender<()>,
1661 }
1662
1663 #[derive(Clone)]
1665 struct GatedEncodingMerger {
1666 started: mpsc::UnboundedSender<PendingEncoding>,
1667 panic_input: Option<FileId>,
1668 }
1669
1670 #[async_trait::async_trait]
1671 impl SstMerger for GatedEncodingMerger {
1672 async fn merge_single_output(
1673 &self,
1674 _context: CompactionContext,
1675 output: CompactionOutput,
1676 _write_opts: WriteOptions,
1677 ) -> Result<(Vec<FileMeta>, Vec<SstInfo>)> {
1678 let input_id = output.inputs[0].meta_ref().file_id;
1679 let mut file = dummy_file_meta();
1680 let batch = Arc::new(new_record_batch_by_range(&["a", "b"], 0, 4));
1681 let (release, wait) = oneshot::channel();
1682 let pending = PendingEncoding {
1683 input_id,
1684 output_id: file.file_id,
1685 batch: Arc::downgrade(&batch),
1686 release,
1687 };
1688 let started = self.started.clone();
1689 file.file_size = common_runtime::spawn_blocking_compact(move || {
1690 started.send(pending).unwrap();
1691 wait.blocking_recv().unwrap();
1692 let mut bytes = Vec::new();
1693 let mut writer = ArrowWriter::try_new(&mut bytes, batch.schema(), None)?;
1694 writer.write(&batch)?;
1695 writer.close()?;
1696 Ok::<_, parquet::errors::ParquetError>(bytes.len() as u64)
1697 })
1698 .await
1699 .context(error::JoinSnafu)?
1700 .context(error::WriteParquetSnafu)?;
1701 if self.panic_input == Some(input_id) {
1702 panic!("merge task panicked");
1703 }
1704 Ok((vec![file], Vec::new()))
1705 }
1706 }
1707
1708 #[rstest::rstest]
1709 #[case::successful_outputs(false)]
1710 #[case::join_error(true)]
1711 #[tokio::test]
1712 async fn test_merge_ssts_cancellation_waits_for_encoders(#[case] panic_merge: bool) {
1713 let mut region = new_test_compaction_region().await;
1714 region.max_parallelism = 2;
1715 region.region_options.compaction_output_parallelism = Some(2);
1716 let inputs: Vec<_> = (0..3).map(|_| dummy_file_meta()).collect();
1717 let picker_output = PickerOutput {
1718 outputs: inputs
1719 .iter()
1720 .map(|input| CompactionOutput {
1721 output_level: 1,
1722 inputs: vec![new_file_handle(input.clone())],
1723 filter_deleted: false,
1724 output_time_range: None,
1725 })
1726 .collect(),
1727 ..Default::default()
1728 };
1729 let cancel_handle = Arc::new(CancellationHandle::default());
1730 let (started_tx, mut started_rx) = mpsc::unbounded_channel();
1731 let compactor = DefaultCompactor {
1732 merger: GatedEncodingMerger {
1733 started: started_tx,
1734 panic_input: panic_merge.then_some(inputs[1].file_id),
1736 },
1737 cancel_handle: cancel_handle.clone(),
1738 uncommitted: None,
1739 };
1740 let memory = new_compaction_memory_manager(1024 * 1024);
1741 let budget = memory.try_acquire(memory.limit_bytes()).unwrap();
1742 let mut merge = Box::pin(async {
1743 let _budget = budget;
1744 compactor.merge_ssts(®ion, picker_output).await
1745 });
1746 assert!(futures::poll!(merge.as_mut()).is_pending());
1747 let mut encoders = Vec::new();
1748 for _ in 0..2 {
1749 encoders.push(
1750 timeout(Duration::from_secs(10), started_rx.recv())
1751 .await
1752 .unwrap()
1753 .unwrap(),
1754 );
1755 }
1756 cancel_handle.cancel();
1757 assert!(futures::poll!(merge.as_mut()).is_pending());
1758 assert!(memory.try_acquire(memory.limit_bytes()).is_none());
1759 assert!(
1760 encoders
1761 .iter()
1762 .all(|encoder| encoder.batch.upgrade().is_some())
1763 );
1764 let retained_batches: Vec<_> = encoders
1765 .iter()
1766 .map(|encoder| encoder.batch.clone())
1767 .collect();
1768 let mut expected_inputs: Vec<_> = encoders.iter().map(|encoder| encoder.input_id).collect();
1769 let mut expected_outputs: Vec<_> =
1770 encoders.iter().map(|encoder| encoder.output_id).collect();
1771 for encoder in encoders {
1772 encoder.release.send(()).unwrap();
1773 }
1774 let result = timeout(Duration::from_secs(10), merge).await.unwrap();
1775 assert!(
1776 retained_batches
1777 .iter()
1778 .all(|batch| batch.upgrade().is_none())
1779 );
1780 assert!(memory.try_acquire(memory.limit_bytes()).is_some());
1781 assert!(
1782 started_rx.try_recv().is_err(),
1783 "cancelled work must not start another output"
1784 );
1785 if panic_merge {
1786 assert!(matches!(result, Err(error::Error::Join { error, .. }) if error.is_panic()));
1787 } else {
1788 let merged = result.unwrap();
1789 let mut removed: Vec<_> = merged
1790 .files_to_remove
1791 .iter()
1792 .map(|file| file.file_id)
1793 .collect();
1794 let mut added: Vec<_> = merged
1795 .files_to_add
1796 .iter()
1797 .map(|file| file.file_id)
1798 .collect();
1799 for ids in [
1800 &mut removed,
1801 &mut added,
1802 &mut expected_inputs,
1803 &mut expected_outputs,
1804 ] {
1805 ids.sort_by(|a, b| a.as_bytes().cmp(b.as_bytes()));
1806 }
1807 assert_eq!(expected_inputs, removed);
1808 assert_eq!(expected_outputs, added);
1809 assert!(merged.files_to_add.iter().all(|file| file.file_size > 0));
1810 }
1811 }
1812
1813 #[tokio::test]
1814 async fn test_merge_ssts_cancelled_before_start_skips_outputs() {
1815 let region = new_test_compaction_region().await;
1816 let merger = MockMerger::new(vec![Ok(vec![dummy_file_meta()])]);
1817 let calls = merger.call_idx.clone();
1818 let compactor = DefaultCompactor::with_merger(merger);
1819 compactor.cancel_handle.cancel();
1820 let picker_output = PickerOutput {
1821 outputs: vec![CompactionOutput {
1822 output_level: 1,
1823 inputs: vec![new_file_handle(dummy_file_meta())],
1824 filter_deleted: false,
1825 output_time_range: None,
1826 }],
1827 ..Default::default()
1828 };
1829 let merged = compactor.merge_ssts(®ion, picker_output).await.unwrap();
1830 assert!(merged.is_empty());
1831 assert_eq!(0, calls.load(Ordering::SeqCst));
1832 }
1833}