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