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 index_options = compaction_region
465 .current_version
466 .options
467 .index_options
468 .clone();
469 let append_mode = compaction_region.current_version.options.append_mode;
470 let merge_mode = compaction_region.current_version.options.merge_mode();
471 let flat_format = compaction_region
472 .region_options
473 .sst_format
474 .map(|format| format == FormatType::Flat)
475 .unwrap_or(compaction_region.engine_config.default_flat_format);
476
477 let index_config = compaction_region.engine_config.index.clone();
478 let inverted_index_config = compaction_region.engine_config.inverted_index.clone();
479 let fulltext_index_config = compaction_region.engine_config.fulltext_index.clone();
480 let bloom_filter_index_config = compaction_region.engine_config.bloom_filter_index.clone();
481
482 let input_file_names = output
483 .inputs
484 .iter()
485 .map(|f| f.file_id().to_string())
486 .join(",");
487 let sequence_metadata = output_sequence_metadata(&compaction_region, &output.inputs);
488 let builder = CompactionSstReaderBuilder {
489 metadata: compaction_region.region_metadata.clone(),
490 sst_layer: compaction_region.access_layer.clone(),
491 cache: compaction_region.cache_manager.clone(),
492 inputs: &output.inputs,
493 append_mode,
494 filter_deleted: output.filter_deleted,
495 time_range: output.output_time_range,
496 merge_mode,
497 };
498 let source = builder.build_flat_sst_reader().await?;
499
500 let mut metrics = Metrics::new(WriteType::Compaction);
501 let region_metadata = compaction_region.region_metadata.clone();
502 let sst_infos = compaction_region
503 .access_layer
504 .write_sst(
505 SstWriteRequest {
506 op_type: OperationType::Compact,
507 metadata: region_metadata.clone(),
508 source,
509 cache_manager: compaction_region.cache_manager.clone(),
510 max_sequence: None,
514 sst_write_format: if flat_format {
515 FormatType::Flat
516 } else {
517 FormatType::PrimaryKey
518 },
519 preserve_row_sequence: sequence_metadata.exact_sequence_trusted,
520 index_options,
521 index_config,
522 inverted_index_config,
523 fulltext_index_config,
524 bloom_filter_index_config,
525 },
526 &write_opts,
527 &mut metrics,
528 )
529 .await?;
530 let partition_expr = match ®ion_metadata.partition_expr {
532 None => None,
533 Some(json_str) if json_str.is_empty() => None,
534 Some(json_str) => PartitionExpr::from_json_str(json_str).with_context(|_| {
535 InvalidPartitionExprSnafu {
536 expr: json_str.clone(),
537 }
538 })?,
539 };
540
541 let output_files = sst_infos
542 .iter()
543 .map(|sst_info| {
544 let pk_range = sst_info
545 .file_metadata
546 .as_ref()
547 .and_then(|meta| extract_primary_key_range(meta, ®ion_metadata));
548 let (primary_key_min, primary_key_max) = match pk_range {
549 Some((min, max)) => (Some(min), Some(max)),
550 None => (None, None),
551 };
552
553 FileMeta {
554 region_id,
555 file_id: sst_info.file_id,
556 time_range: sst_info.time_range,
557 level: output.output_level,
558 file_size: sst_info.file_size,
559 max_row_group_uncompressed_size: sst_info.max_row_group_uncompressed_size,
560 available_indexes: sst_info.index_metadata.build_available_indexes(),
561 indexes: sst_info.index_metadata.build_indexes(),
562 index_file_size: sst_info.index_metadata.file_size,
563 index_version: 0,
564 num_rows: sst_info.num_rows as u64,
565 num_row_groups: sst_info.num_row_groups,
566 sequence: sequence_metadata.file_sequence,
567 partition_expr: partition_expr.clone(),
568 num_series: sst_info.num_series,
569 primary_key_min,
570 primary_key_max,
571 preserve_row_sequence: sequence_metadata.exact_sequence_trusted,
572 }
573 })
574 .collect::<Vec<_>>();
575 let output_file_names = output_files.iter().map(|f| f.file_id.to_string()).join(",");
576 info!(
577 "Region {} compaction inputs: [{}], outputs: [{}], flat_format: {}, metrics: {:?}",
578 region_id, input_file_names, output_file_names, flat_format, metrics
579 );
580 metrics.observe();
581 Ok((output_files, sst_infos.into_iter().collect()))
582 }
583}
584
585pub struct DefaultCompactor<M = DefaultSstMerger> {
590 merger: M,
591 cancel_handle: Arc<CancellationHandle>,
592 uncommitted: Option<UncommittedSsts>,
593}
594
595#[cfg(test)]
596impl<M: SstMerger> DefaultCompactor<M> {
597 pub fn with_merger(merger: M) -> Self {
598 Self {
599 merger,
600 cancel_handle: Arc::new(CancellationHandle::default()),
601 uncommitted: None,
602 }
603 }
604}
605
606impl DefaultCompactor {
607 pub fn new_with_cancel_handle(cancel_handle: Arc<CancellationHandle>) -> Self {
613 Self {
614 merger: DefaultSstMerger,
615 cancel_handle,
616 uncommitted: None,
617 }
618 }
619
620 pub(crate) fn with_cancel_handle(
621 cancel_handle: Arc<CancellationHandle>,
622 uncommitted: UncommittedSsts,
623 ) -> Self {
624 Self {
625 merger: DefaultSstMerger,
626 cancel_handle,
627 uncommitted: Some(uncommitted),
628 }
629 }
630}
631
632#[async_trait::async_trait]
633impl<M: SstMerger> Compactor for DefaultCompactor<M>
634where
635 M: Clone,
636{
637 async fn merge_ssts(
638 &self,
639 compaction_region: &CompactionRegion,
640 mut picker_output: PickerOutput,
641 ) -> Result<MergeOutput> {
642 let internal_parallelism = compaction_region.max_parallelism.max(1);
643 let compaction_time_window = picker_output.time_window_size;
644 let region_id = compaction_region.region_id;
645
646 let mut tasks: Vec<(Vec<FileMeta>, _)> = Vec::with_capacity(picker_output.outputs.len());
649
650 for output in picker_output.outputs.drain(..) {
651 let inputs_to_remove: Vec<_> =
652 output.inputs.iter().map(|f| f.meta_ref().clone()).collect();
653 let write_opts = WriteOptions {
654 write_buffer_size: compaction_region.engine_config.sst_write_buffer_size,
655 max_file_size: picker_output.max_file_size,
656 row_group_size: compaction_region.region_options.row_group_size(),
657 float_field_encoding: compaction_region.region_options.float_field_encoding,
658 };
659 let merger = self.merger.clone();
660 let compaction_region = compaction_region.clone();
661 let uncommitted = self.uncommitted.clone();
662 let fut = async move {
663 let result = merger
664 .merge_single_output(compaction_region, output, write_opts)
665 .await;
666 if let (Some(uncommitted), Ok((_, infos))) = (&uncommitted, &result) {
667 uncommitted.track(infos);
668 }
669 result
670 };
671 tasks.push((inputs_to_remove, fut));
672 }
673
674 let hook: Option<RegionHookRef> = compaction_region.plugins.get();
675 let mut output_files = Vec::with_capacity(tasks.len());
676 let mut all_sst_infos: Vec<SstInfo> = Vec::new();
677 let mut compacted_inputs = Vec::with_capacity(
678 tasks.iter().map(|(inputs, _)| inputs.len()).sum::<usize>()
679 + picker_output.expired_ssts.len(),
680 );
681
682 while !tasks.is_empty() {
683 let mut chunk: Vec<(Vec<FileMeta>, _)> = Vec::with_capacity(internal_parallelism);
684 for _ in 0..internal_parallelism {
685 if let Some(task) = tasks.pop() {
686 chunk.push(task);
687 }
688 }
689 let mut spawned: Vec<_> = chunk
690 .into_iter()
691 .map(|(inputs, fut)| {
692 let handle = common_runtime::spawn_compact(fut);
693 (inputs, handle)
694 })
695 .collect();
696
697 while let Some((inputs, mut handle)) = spawned.pop() {
698 let abort_handle = handle.abort_handle();
699 match CancellableFuture::new(&mut handle, self.cancel_handle.clone()).await {
700 Ok(Ok(Ok((files, infos)))) => {
701 output_files.extend(files);
702 if hook.is_some() {
703 all_sst_infos.extend(infos);
704 }
705 compacted_inputs.extend(inputs);
706 }
707 Ok(Ok(Err(e))) => {
708 warn!(
709 e; "Failed to merge compaction output for region: {}, inputs: [{}]",
710 region_id,
711 inputs.iter().map(|f| f.file_id.to_string()).join(",")
712 );
713 }
714 Ok(Err(e)) => {
715 warn!(
716 "Region {} compaction task join error for inputs: [{}], skipping: {}",
717 region_id,
718 inputs.iter().map(|f| f.file_id.to_string()).join(","),
719 e
720 );
721 if self.cancel_handle.is_cancelled() {
724 abort_handle.abort();
725 for (_, handle) in &spawned {
726 handle.abort();
727 }
728 for (_, handle) in spawned {
729 let _ = handle.await;
730 }
731 }
732 return Err(e).context(error::JoinSnafu);
733 }
734 Err(_) => {
735 debug!(
736 "Compaction merge cancelled for region: {}, aborting remaining {} spawned tasks",
737 region_id,
738 spawned.len(),
739 );
740 abort_handle.abort();
741 for (_, handle) in &spawned {
742 handle.abort();
743 }
744 let _ = handle.await;
745 for (_, handle) in spawned {
746 let _ = handle.await;
747 }
748 break;
749 }
750 }
751 }
752
753 if self.cancel_handle.is_cancelled() {
754 info!("Compaction merge cancelled for region: {}", region_id);
755 break;
756 }
757 }
758
759 compacted_inputs.extend(
761 picker_output
762 .expired_ssts
763 .iter()
764 .map(|f| f.meta_ref().clone()),
765 );
766
767 Ok(MergeOutput {
768 files_to_add: output_files,
769 files_to_remove: compacted_inputs,
770 compaction_time_window: Some(compaction_time_window),
771 sst_infos: all_sst_infos,
772 })
773 }
774
775 async fn update_manifest(
776 &self,
777 compaction_region: &CompactionRegion,
778 merge_output: MergeOutput,
779 ) -> Result<(RegionEdit, ManifestVersion)> {
780 let edit = RegionEdit {
782 files_to_add: merge_output.files_to_add,
783 files_to_remove: merge_output.files_to_remove,
784 timestamp_ms: Some(chrono::Utc::now().timestamp_millis()),
786 compaction_time_window: merge_output
787 .compaction_time_window
788 .map(|seconds| Duration::from_secs(seconds as u64)),
789 flushed_entry_id: None,
790 flushed_sequence: None,
791 committed_sequence: None,
792 };
793
794 let action_list = RegionMetaActionList::with_action(RegionMetaAction::Edit(edit.clone()));
795 let manifest_version = compaction_region
797 .manifest_ctx
798 .update_manifest_for_compaction(action_list)
799 .await?;
800
801 Ok((edit, manifest_version))
802 }
803}
804
805#[cfg(test)]
806mod tests {
807 use std::sync::atomic::{AtomicUsize, Ordering};
808 use std::sync::{Arc, Mutex};
809 use std::time::Duration;
810
811 use store_api::storage::{FileId, RegionId};
812 use tokio::time::sleep;
813
814 use super::{DefaultCompactor, *};
815 use crate::cache::CacheManager;
816 use crate::compaction::picker::PickerOutput;
817 use crate::error::Result;
818 use crate::sst::file::FileHandle;
819 use crate::sst::file_purger::NoopFilePurger;
820 use crate::sst::version::SstVersion;
821 use crate::test_util::memtable_util::metadata_for_test;
822 use crate::test_util::scheduler_util::SchedulerEnv;
823
824 fn dummy_file_meta() -> FileMeta {
825 FileMeta {
826 region_id: RegionId::new(1, 1),
827 file_id: FileId::random(),
828 file_size: 100,
829 ..Default::default()
830 }
831 }
832
833 fn new_file_handle(meta: FileMeta) -> FileHandle {
834 FileHandle::new(meta, Arc::new(NoopFilePurger))
835 }
836
837 #[test]
838 fn test_effective_target_trust_and_max_sequence() {
839 let target = RegionId::new(1, 1);
840 let local_trusted = new_file_handle(FileMeta {
841 region_id: target,
842 sequence: NonZeroU64::new(3),
843 preserve_row_sequence: true,
844 ..dummy_file_meta()
845 });
846 let foreign_untrusted_marker = new_file_handle(FileMeta {
847 region_id: RegionId::new(1, 2),
848 sequence: NonZeroU64::new(9),
849 preserve_row_sequence: false,
850 ..dummy_file_meta()
851 });
852 let local_untrusted = new_file_handle(FileMeta {
853 region_id: target,
854 sequence: NonZeroU64::new(11),
855 preserve_row_sequence: false,
856 ..dummy_file_meta()
857 });
858 assert!(local_trusted.is_effective_target_sequence_trusted(target));
859 assert!(foreign_untrusted_marker.is_effective_target_sequence_trusted(target));
860 assert!(!local_untrusted.is_effective_target_sequence_trusted(target));
861 assert_eq!(
862 NonZeroU64::new(9),
863 known_max_input_sequence(&[local_trusted, foreign_untrusted_marker])
864 );
865 }
866
867 #[test]
868 fn test_known_max_input_sequence() {
869 let meta_with = |sequence| FileMeta {
870 sequence,
871 ..dummy_file_meta()
872 };
873
874 let inputs = vec![
875 new_file_handle(meta_with(NonZeroU64::new(3))),
876 new_file_handle(meta_with(NonZeroU64::new(9))),
877 new_file_handle(meta_with(NonZeroU64::new(5))),
878 ];
879 assert_eq!(NonZeroU64::new(9), known_max_input_sequence(&inputs));
880
881 let inputs = vec![
882 new_file_handle(meta_with(NonZeroU64::new(9))),
883 new_file_handle(meta_with(None)),
884 ];
885 assert_eq!(None, known_max_input_sequence(&inputs));
886
887 assert_eq!(None, known_max_input_sequence(&[]));
888 }
889
890 #[tokio::test]
891 async fn test_output_sequence_metadata_preserves_trust_and_unknown_bounds() {
892 let mut region = new_test_compaction_region().await;
893 let local = region.region_id;
894 let foreign = RegionId::new(1, 2);
895 let file = |region_id, sequence: Option<u64>, preserve_row_sequence| {
896 new_file_handle(FileMeta {
897 region_id,
898 sequence: sequence.and_then(NonZeroU64::new),
899 preserve_row_sequence,
900 ..dummy_file_meta()
901 })
902 };
903 let cases = [
906 (
907 vec![file(local, Some(3), true), file(foreign, Some(9), false)],
908 Some(9),
909 true,
910 ),
911 (
912 vec![file(local, Some(3), true), file(local, Some(6), false)],
913 Some(6),
914 false,
915 ),
916 (
917 vec![file(foreign, Some(9), false), file(foreign, Some(11), true)],
918 Some(11),
919 true,
920 ),
921 (
922 vec![file(local, Some(3), true), file(local, None, true)],
923 None,
924 true,
925 ),
926 (
927 vec![file(local, None, false), file(local, Some(6), true)],
928 None,
929 false,
930 ),
931 (
932 vec![file(local, Some(3), true), file(foreign, None, true)],
933 None,
934 false,
935 ),
936 (vec![], None, true),
937 (
938 vec![file(local, Some(u64::MAX), false)],
939 Some(u64::MAX),
940 false,
941 ),
942 ];
943 for committed_sequence in [100, 101] {
944 region
945 .manifest_ctx
946 .manifest_manager
947 .write()
948 .await
949 .update(
950 RegionMetaActionList::with_action(RegionMetaAction::Edit(RegionEdit {
951 committed_sequence: Some(committed_sequence),
952 files_to_add: vec![],
953 files_to_remove: vec![],
954 timestamp_ms: None,
955 compaction_time_window: None,
956 flushed_entry_id: None,
957 flushed_sequence: None,
958 })),
959 false,
960 )
961 .await
962 .unwrap();
963 for preserve in [false, true] {
964 region.region_options.preserve_row_sequence = preserve;
965 for (inputs, known_bound, inputs_trusted) in &cases {
966 let metadata = output_sequence_metadata(®ion, inputs);
967 let trusted = preserve && *inputs_trusted;
968 assert_eq!(trusted, metadata.exact_sequence_trusted);
969 assert_eq!(
970 known_bound.and_then(NonZeroU64::new),
971 metadata.file_sequence
972 );
973 }
974 }
975 }
976 }
977
978 #[rstest::rstest]
982 #[case(true)]
983 #[case(false)]
984 #[tokio::test]
985 async fn test_independent_outputs_preserve_delete_in_every_visibility_state(
986 #[case] flat_format: bool,
987 #[values(false, true)] unknown_put_bound: bool,
988 ) {
989 use datatypes::arrow::array::AsArray;
990 use datatypes::arrow::datatypes::TimestampMillisecondType;
991 use store_api::region_engine::RegionEngine;
992 use store_api::region_request::RegionRequest;
993
994 use crate::compaction::CompactionOutput;
995 use crate::compaction::compactor::{CompactionRegion, Compactor, DefaultCompactor};
996 use crate::compaction::picker::PickerOutput;
997 use crate::compaction::reader::CompactionSstReaderBuilder;
998 use crate::engine::compaction_test::{delete_and_flush, put_and_flush};
999 use crate::region::options::MergeMode;
1000 use crate::sst::file::FileHandle;
1001 use crate::sst::file_purger::NoopFilePurger;
1002 use crate::test_util::{CreateRequestBuilder, TestEnv, rows_schema};
1003
1004 let mut env = TestEnv::new().await;
1005 let region_id = RegionId::new(1, 1);
1006 let config = MitoConfig {
1007 default_flat_format: flat_format,
1008 min_compaction_interval: Duration::from_secs(3600),
1009 ..Default::default()
1010 };
1011 let engine = env.create_engine(config).await;
1012 let request = CreateRequestBuilder::new().build();
1013 let columns = rows_schema(&request);
1014 engine
1015 .handle_request(region_id, RegionRequest::Create(request))
1016 .await
1017 .unwrap();
1018 put_and_flush(&engine, region_id, &columns, 0..1).await;
1019 delete_and_flush(&engine, region_id, &columns, 0..1).await;
1020 put_and_flush(&engine, region_id, &columns, 10..11).await;
1021 let region = engine.get_region(region_id).unwrap();
1022 let version = region.version();
1023 let mut files: Vec<_> = version
1024 .ssts
1025 .levels()
1026 .iter()
1027 .flat_map(|level| level.files())
1028 .cloned()
1029 .collect();
1030 assert_eq!(3, files.len());
1031 files.sort_unstable_by_key(|file| file.meta_ref().sequence);
1032 if unknown_put_bound {
1033 let mut meta = files[0].meta_ref().clone();
1036 meta.sequence = None;
1037 files[0] = FileHandle::new(meta, Arc::new(NoopFilePurger));
1038 }
1039 let compaction_region = CompactionRegion {
1040 region_id,
1041 region_options: version.options.clone(),
1042 engine_config: Arc::new(MitoConfig {
1043 default_flat_format: flat_format,
1044 ..Default::default()
1045 }),
1046 region_metadata: version.metadata.clone(),
1047 cache_manager: engine.cache_manager(),
1048 access_layer: region.access_layer.clone(),
1049 manifest_ctx: region.manifest_ctx.clone(),
1050 current_version: version.into(),
1051 file_purger: None,
1052 ttl: None,
1053 max_parallelism: 2,
1054 plugins: common_base::Plugins::new(),
1055 };
1056 let puts = vec![files[0].clone(), files[2].clone()];
1057 let deletes = vec![files[1].clone()];
1058 let picker_output = PickerOutput {
1059 outputs: [puts.clone(), deletes.clone()]
1060 .into_iter()
1061 .map(|inputs| CompactionOutput {
1062 output_level: 1,
1063 inputs,
1064 filter_deleted: false,
1065 output_time_range: None,
1066 })
1067 .collect(),
1068 expired_ssts: vec![],
1069 time_window_size: 3600,
1070 max_file_size: None,
1071 };
1072 let merged = DefaultCompactor::with_merger(DefaultSstMerger)
1073 .merge_ssts(&compaction_region, picker_output)
1074 .await
1075 .unwrap();
1076 assert_eq!(2, merged.files_to_add.len());
1077 let outputs: Vec<_> = merged
1078 .files_to_add
1079 .into_iter()
1080 .map(|meta| FileHandle::new(meta, Arc::new(NoopFilePurger)))
1081 .collect();
1082 let put_output = outputs
1083 .iter()
1084 .find(|file| file.meta_ref().num_rows == 2)
1085 .unwrap();
1086 let delete_output = outputs
1087 .iter()
1088 .find(|file| file.meta_ref().num_rows == 1)
1089 .unwrap();
1090 assert_eq!(
1091 if unknown_put_bound {
1092 None
1093 } else {
1094 NonZeroU64::new(3)
1095 },
1096 put_output.meta_ref().sequence,
1097 );
1098 assert_eq!(NonZeroU64::new(2), delete_output.meta_ref().sequence);
1099 assert!(
1100 outputs
1101 .iter()
1102 .all(|file| !file.meta_ref().preserve_row_sequence)
1103 );
1104
1105 for (put_done, delete_done) in [(false, false), (true, false), (false, true), (true, true)]
1108 {
1109 let mut visible = if put_done {
1110 vec![put_output.clone()]
1111 } else {
1112 puts.clone()
1113 };
1114 visible.extend(if delete_done {
1115 vec![delete_output.clone()]
1116 } else {
1117 deletes.clone()
1118 });
1119 let mut reader = CompactionSstReaderBuilder {
1120 metadata: compaction_region.region_metadata.clone(),
1121 sst_layer: region.access_layer.clone(),
1122 cache: engine.cache_manager(),
1123 inputs: &visible,
1124 append_mode: false,
1125 filter_deleted: true,
1126 time_range: None,
1127 merge_mode: MergeMode::LastRow,
1128 }
1129 .build_flat_sst_reader()
1130 .await
1131 .unwrap();
1132 let mut timestamps = Vec::new();
1133 while let Some(batch) = reader.next_batch().await.unwrap() {
1134 timestamps.extend(
1135 batch
1136 .column_by_name("ts")
1137 .unwrap()
1138 .as_primitive::<TimestampMillisecondType>()
1139 .values()
1140 .iter()
1141 .copied(),
1142 );
1143 }
1144 assert_eq!(
1145 vec![10_000],
1146 timestamps,
1147 "put_done={put_done}, delete_done={delete_done}"
1148 );
1149 }
1150 }
1151
1152 async fn new_test_compaction_region() -> CompactionRegion {
1155 let env = SchedulerEnv::new().await;
1156 let metadata = metadata_for_test();
1157 let manifest_ctx = env.mock_manifest_context(metadata.clone()).await;
1158 CompactionRegion {
1159 region_id: RegionId::new(1, 1),
1160 region_options: RegionOptions::default(),
1161 engine_config: Arc::new(MitoConfig::default()),
1162 region_metadata: metadata.clone(),
1163 cache_manager: Arc::new(CacheManager::default()),
1164 access_layer: env.access_layer.clone(),
1165 manifest_ctx,
1166 current_version: CompactionVersion {
1167 metadata: metadata.clone(),
1168 options: RegionOptions::default(),
1169 ssts: Arc::new(SstVersion::new(metadata)),
1170 memtable_min_sequence: None,
1171 compaction_time_window: None,
1172 },
1173 file_purger: None,
1174 ttl: None,
1175 max_parallelism: 1,
1176 plugins: Plugins::new(),
1177 }
1178 }
1179
1180 #[derive(Clone)]
1184 struct MockMerger {
1185 results: Arc<Mutex<Vec<Result<Vec<FileMeta>>>>>,
1186 call_idx: Arc<AtomicUsize>,
1187 }
1188
1189 impl MockMerger {
1190 fn new(results: Vec<Result<Vec<FileMeta>>>) -> Self {
1191 Self {
1192 results: Arc::new(Mutex::new(results)),
1193 call_idx: Arc::new(AtomicUsize::new(0)),
1194 }
1195 }
1196 }
1197
1198 #[async_trait::async_trait]
1199 impl SstMerger for MockMerger {
1200 async fn merge_single_output(
1201 &self,
1202 _compaction_region: CompactionRegion,
1203 _output: CompactionOutput,
1204 _write_opts: WriteOptions,
1205 ) -> Result<(Vec<FileMeta>, Vec<SstInfo>)> {
1206 let idx = self.call_idx.fetch_add(1, Ordering::SeqCst);
1207 match self.results.lock().unwrap().get(idx) {
1208 Some(Ok(files)) => Ok((files.clone(), Vec::new())),
1209 Some(Err(_)) => error::InvalidMetaSnafu {
1210 reason: format!("simulated failure at index {idx}"),
1211 }
1212 .fail(),
1213 None => panic!("MockMerger: no result configured for call index {idx}"),
1214 }
1215 }
1216 }
1217
1218 #[tokio::test]
1219 async fn test_partial_merge_failure_collects_only_successful_outputs() {
1220 common_telemetry::init_default_ut_logging();
1221
1222 let compaction_region = new_test_compaction_region().await;
1223
1224 let input_meta_0 = dummy_file_meta();
1226 let input_meta_1 = dummy_file_meta();
1227 let input_meta_2 = dummy_file_meta();
1228
1229 let output_meta_0 = vec![dummy_file_meta()];
1230 let output_meta_2 = vec![dummy_file_meta(), dummy_file_meta()];
1231
1232 let merger = MockMerger::new(vec![
1233 Ok(output_meta_0.clone()),
1234 Err(error::InvalidMetaSnafu {
1235 reason: "boom".to_string(),
1236 }
1237 .build()),
1238 Ok(output_meta_2.clone()),
1239 ]);
1240 let compactor = DefaultCompactor::with_merger(merger);
1241
1242 let picker_output = PickerOutput {
1243 outputs: vec![
1244 CompactionOutput {
1245 output_level: 1,
1246 inputs: vec![new_file_handle(input_meta_0.clone())],
1247 filter_deleted: false,
1248 output_time_range: None,
1249 },
1250 CompactionOutput {
1251 output_level: 1,
1252 inputs: vec![new_file_handle(input_meta_1.clone())],
1253 filter_deleted: false,
1254 output_time_range: None,
1255 },
1256 CompactionOutput {
1257 output_level: 1,
1258 inputs: vec![new_file_handle(input_meta_2.clone())],
1259 filter_deleted: false,
1260 output_time_range: None,
1261 },
1262 ],
1263 expired_ssts: vec![],
1264 time_window_size: 3600,
1265 max_file_size: None,
1266 };
1267
1268 let merge_output = compactor
1269 .merge_ssts(&compaction_region, picker_output)
1270 .await
1271 .unwrap();
1272
1273 assert_eq!(merge_output.files_to_add.len(), 3);
1275 assert_eq!(merge_output.files_to_remove.len(), 2);
1277
1278 let removed_ids: Vec<_> = merge_output
1279 .files_to_remove
1280 .iter()
1281 .map(|f| f.file_id)
1282 .collect();
1283 assert!(removed_ids.contains(&input_meta_0.file_id));
1284 assert!(removed_ids.contains(&input_meta_2.file_id));
1285 assert!(!removed_ids.contains(&input_meta_1.file_id));
1287 }
1288
1289 #[tokio::test]
1290 async fn test_all_outputs_succeed() {
1291 common_telemetry::init_default_ut_logging();
1292
1293 let compaction_region = new_test_compaction_region().await;
1294 let input_meta = dummy_file_meta();
1295 let output_meta = vec![dummy_file_meta()];
1296
1297 let merger = MockMerger::new(vec![Ok(output_meta.clone())]);
1298 let compactor = DefaultCompactor::with_merger(merger);
1299
1300 let picker_output = PickerOutput {
1301 outputs: vec![CompactionOutput {
1302 output_level: 1,
1303 inputs: vec![new_file_handle(input_meta.clone())],
1304 filter_deleted: false,
1305 output_time_range: None,
1306 }],
1307 expired_ssts: vec![],
1308 time_window_size: 3600,
1309 max_file_size: None,
1310 };
1311
1312 let merge_output = compactor
1313 .merge_ssts(&compaction_region, picker_output)
1314 .await
1315 .unwrap();
1316
1317 assert_eq!(merge_output.files_to_add.len(), 1);
1318 assert_eq!(merge_output.files_to_add[0].file_id, output_meta[0].file_id);
1319 assert_eq!(merge_output.files_to_remove.len(), 1);
1320 assert_eq!(merge_output.files_to_remove[0].file_id, input_meta.file_id);
1321 }
1322
1323 #[tokio::test]
1324 async fn test_expired_ssts_always_removed() {
1325 common_telemetry::init_default_ut_logging();
1326
1327 let compaction_region = new_test_compaction_region().await;
1328 let input_meta = dummy_file_meta();
1329 let expired_meta = dummy_file_meta();
1330
1331 let merger = MockMerger::new(vec![Err(error::InvalidMetaSnafu {
1333 reason: "fail".to_string(),
1334 }
1335 .build())]);
1336 let compactor = DefaultCompactor::with_merger(merger);
1337
1338 let picker_output = PickerOutput {
1339 outputs: vec![CompactionOutput {
1340 output_level: 1,
1341 inputs: vec![new_file_handle(input_meta.clone())],
1342 filter_deleted: false,
1343 output_time_range: None,
1344 }],
1345 expired_ssts: vec![new_file_handle(expired_meta.clone())],
1346 time_window_size: 3600,
1347 max_file_size: None,
1348 };
1349
1350 let merge_output = compactor
1351 .merge_ssts(&compaction_region, picker_output)
1352 .await
1353 .unwrap();
1354
1355 assert!(merge_output.files_to_add.is_empty());
1357 assert_eq!(merge_output.files_to_remove.len(), 1);
1359 assert_eq!(
1360 merge_output.files_to_remove[0].file_id,
1361 expired_meta.file_id
1362 );
1363 }
1364
1365 #[derive(Clone)]
1366 struct BlockingMerger {
1367 call_idx: Arc<AtomicUsize>,
1368 }
1369
1370 #[async_trait::async_trait]
1371 impl SstMerger for BlockingMerger {
1372 async fn merge_single_output(
1373 &self,
1374 _compaction_region: CompactionRegion,
1375 _output: CompactionOutput,
1376 _write_opts: WriteOptions,
1377 ) -> Result<(Vec<FileMeta>, Vec<SstInfo>)> {
1378 self.call_idx.fetch_add(1, Ordering::SeqCst);
1379 std::future::pending().await
1380 }
1381 }
1382
1383 #[tokio::test(flavor = "multi_thread")]
1384 async fn test_merge_ssts_cancels_spawned_tasks() {
1385 common_telemetry::init_default_ut_logging();
1386
1387 let mut compaction_region = new_test_compaction_region().await;
1388 compaction_region.max_parallelism = 2;
1389
1390 let cancel_handle = Arc::new(CancellationHandle::default());
1391 let call_idx = Arc::new(AtomicUsize::new(0));
1392 let compactor = DefaultCompactor {
1393 merger: BlockingMerger {
1394 call_idx: call_idx.clone(),
1395 },
1396 cancel_handle: cancel_handle.clone(),
1397 uncommitted: None,
1398 };
1399
1400 let picker_output = PickerOutput {
1401 outputs: vec![
1402 CompactionOutput {
1403 output_level: 1,
1404 inputs: vec![new_file_handle(dummy_file_meta())],
1405 filter_deleted: false,
1406 output_time_range: None,
1407 },
1408 CompactionOutput {
1409 output_level: 1,
1410 inputs: vec![new_file_handle(dummy_file_meta())],
1411 filter_deleted: false,
1412 output_time_range: None,
1413 },
1414 CompactionOutput {
1415 output_level: 1,
1416 inputs: vec![new_file_handle(dummy_file_meta())],
1417 filter_deleted: false,
1418 output_time_range: None,
1419 },
1420 ],
1421 expired_ssts: vec![],
1422 time_window_size: 3600,
1423 max_file_size: None,
1424 };
1425
1426 let task = tokio::spawn(async move {
1427 compactor
1428 .merge_ssts(&compaction_region, picker_output)
1429 .await
1430 });
1431
1432 sleep(Duration::from_millis(100)).await;
1433 cancel_handle.cancel();
1434
1435 let merge_output = task
1436 .await
1437 .expect("merge_ssts should stop after cancellation")
1438 .unwrap();
1439
1440 let started = call_idx.load(Ordering::SeqCst);
1441
1442 assert!(merge_output.files_to_add.is_empty());
1443 assert!(merge_output.files_to_remove.is_empty());
1444 assert_eq!(started, 2);
1445 }
1446}