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
204pub async fn open_compaction_region(
207 req: &OpenCompactionRegionRequest,
208 mito_config: &MitoConfig,
209 object_store_manager: ObjectStoreManagerRef,
210 ttl_provider: Either<TimeToLive, SchemaMetadataManagerRef>,
211) -> Result<CompactionRegion> {
212 let object_store = {
213 let name = &req.region_options.storage;
214 if let Some(name) = name {
215 object_store_manager
216 .find(name)
217 .with_context(|| ObjectStoreNotFoundSnafu {
218 object_store: name.clone(),
219 })?
220 } else {
221 object_store_manager.default_object_store()
222 }
223 };
224
225 let access_layer = {
226 let puffin_manager_factory = PuffinManagerFactory::new(
227 &mito_config.index.aux_path,
228 mito_config.index.staging_size.as_bytes(),
229 Some(mito_config.index.write_buffer_size.as_bytes() as _),
230 mito_config.index.staging_ttl,
231 )
232 .await?;
233 let intermediate_manager =
234 IntermediateManager::init_fs(mito_config.index.aux_path.clone()).await?;
235
236 Arc::new(AccessLayer::new(
237 &req.table_dir,
238 req.path_type,
239 object_store.clone(),
240 puffin_manager_factory,
241 intermediate_manager,
242 ))
243 };
244
245 let manifest_manager = {
246 let region_dir = region_dir_from_table_dir(&req.table_dir, req.region_id, req.path_type);
247 let region_manifest_options =
248 RegionManifestOptions::new(mito_config, ®ion_dir, object_store);
249
250 RegionManifestManager::open(region_manifest_options, &Default::default())
251 .await?
252 .with_context(|| EmptyRegionDirSnafu {
253 region_id: req.region_id,
254 region_dir: region_dir_from_table_dir(&req.table_dir, req.region_id, req.path_type),
255 })?
256 };
257
258 let manifest = manifest_manager.manifest();
259 let region_metadata = manifest.metadata.clone();
260 let hook: Option<RegionHookRef> = req.plugins.get();
261 let manifest_ctx = Arc::new(ManifestContext::new(
262 manifest_manager,
263 RegionRoleState::Leader(RegionLeaderState::Writable),
264 hook,
265 ));
266
267 let file_purger = {
268 let purge_scheduler = Arc::new(LocalScheduler::new(mito_config.max_background_purges));
269 Arc::new(LocalFilePurger::new(
270 purge_scheduler.clone(),
271 access_layer.clone(),
272 None,
273 ))
274 };
275
276 let current_version = {
277 let mut ssts = SstVersion::new(region_metadata.clone());
278 ssts.add_files(file_purger.clone(), manifest.files.values().cloned());
279 CompactionVersion {
280 metadata: region_metadata.clone(),
281 options: req.region_options.clone(),
282 ssts: Arc::new(ssts),
283 memtable_min_sequence: Some(0),
286 compaction_time_window: manifest.compaction_time_window,
287 }
288 };
289
290 let ttl = match ttl_provider {
291 Either::Left(ttl) => ttl,
293 Either::Right(schema_metadata_manager) => {
295 let (_, ttl) =
296 find_dynamic_options(req.region_id, &req.region_options, &schema_metadata_manager)
297 .await
298 .unwrap_or_else(|e| {
299 warn!(e; "Failed to get ttl for region: {}", region_metadata.region_id);
300 (
301 crate::region::options::CompactionOptions::default(),
302 TimeToLive::default(),
303 )
304 });
305 ttl
306 }
307 };
308
309 Ok(CompactionRegion {
310 region_id: req.region_id,
311 region_options: req.region_options.clone(),
312 engine_config: Arc::new(mito_config.clone()),
313 region_metadata: region_metadata.clone(),
314 cache_manager: Arc::new(CacheManager::default()),
315 access_layer,
316 manifest_ctx,
317 current_version,
318 file_purger: Some(file_purger),
319 ttl: Some(ttl),
320 max_parallelism: req.max_parallelism,
321 plugins: req.plugins.clone(),
322 })
323}
324
325impl CompactionRegion {
326 pub fn file_purger(&self) -> Option<Arc<LocalFilePurger>> {
328 self.file_purger.clone()
329 }
330
331 pub async fn stop_purger_scheduler(&self) -> Result<()> {
333 if let Some(file_purger) = &self.file_purger {
334 file_purger.stop_scheduler().await
335 } else {
336 Ok(())
337 }
338 }
339
340 pub async fn invoke_sst_hook(&self, merge_output: &MergeOutput) {
345 CompactionContext::from(self)
346 .invoke_sst_hook(merge_output)
347 .await;
348 }
349}
350
351impl CompactionContext {
352 pub(crate) async fn invoke_sst_hook(&self, merge_output: &MergeOutput) {
354 let Some(hook) = self.plugins.get::<RegionHookRef>() else {
355 return;
356 };
357
358 let synthesized: Vec<SstInfo>;
363 let infos: &[SstInfo] = if merge_output.sst_infos.is_empty() {
364 synthesized = merge_output
365 .files_to_add
366 .iter()
367 .map(sst_info_from_file_meta)
368 .collect();
369 &synthesized
370 } else {
371 if merge_output.sst_infos.len() != merge_output.files_to_add.len() {
375 warn!(
376 "sst_infos length ({}) does not match files_to_add length ({}) for region {}",
377 merge_output.sst_infos.len(),
378 merge_output.files_to_add.len(),
379 self.region_id
380 );
381 }
382 &merge_output.sst_infos
383 };
384
385 let files: Vec<SstFileInfo<'_>> = merge_output
386 .files_to_add
387 .iter()
388 .zip(infos)
389 .map(|(meta, info)| SstFileInfo {
390 sst_info_ref: info,
391 file_meta: meta,
392 })
393 .collect();
394 hook.on_sst_files_written(self.region_id, &self.region_metadata, &files)
395 .await;
396 }
397
398 pub(crate) async fn update_manifest(
400 &self,
401 merge_output: MergeOutput,
402 ) -> Result<(RegionEdit, ManifestVersion)> {
403 let edit = RegionEdit {
404 files_to_add: merge_output.files_to_add,
405 files_to_remove: merge_output.files_to_remove,
406 timestamp_ms: Some(chrono::Utc::now().timestamp_millis()),
407 compaction_time_window: merge_output
408 .compaction_time_window
409 .map(|seconds| Duration::from_secs(seconds as u64)),
410 flushed_entry_id: None,
411 flushed_sequence: None,
412 committed_sequence: None,
413 };
414
415 let action_list = RegionMetaActionList::with_action(RegionMetaAction::Edit(edit.clone()));
416 let manifest_version = self
417 .manifest_ctx
418 .update_manifest_for_compaction(action_list)
419 .await?;
420
421 Ok((edit, manifest_version))
422 }
423}
424
425fn sst_info_from_file_meta(meta: &FileMeta) -> SstInfo {
448 SstInfo {
449 file_id: meta.file_id,
450 time_range: meta.time_range,
451 file_size: meta.file_size,
452 max_row_group_uncompressed_size: meta.max_row_group_uncompressed_size,
453 num_rows: meta.num_rows as usize,
454 num_row_groups: meta.num_row_groups,
455 num_series: meta.num_series,
456 ..Default::default()
457 }
458}
459
460#[derive(Default, Debug, Serialize, Deserialize)]
462pub struct MergeOutput {
463 pub files_to_add: Vec<FileMeta>,
464 pub files_to_remove: Vec<FileMeta>,
465 pub compaction_time_window: Option<i64>,
466 #[serde(skip)]
467 pub sst_infos: Vec<SstInfo>,
468}
469
470impl MergeOutput {
471 pub fn is_empty(&self) -> bool {
472 self.files_to_add.is_empty() && self.files_to_remove.is_empty()
473 }
474
475 pub fn input_file_size(&self) -> u64 {
476 self.files_to_remove.iter().map(|f| f.file_size).sum()
477 }
478
479 pub fn output_file_size(&self) -> u64 {
480 self.files_to_add.iter().map(|f| f.file_size).sum()
481 }
482}
483
484#[async_trait::async_trait]
486pub trait Compactor: Send + Sync + 'static {
487 async fn merge_ssts(
489 &self,
490 compaction_region: &CompactionRegion,
491 picker_output: PickerOutput,
492 ) -> Result<MergeOutput>;
493
494 async fn update_manifest(
496 &self,
497 compaction_region: &CompactionRegion,
498 merge_output: MergeOutput,
499 ) -> Result<(RegionEdit, ManifestVersion)>;
500}
501
502#[async_trait::async_trait]
507pub trait SstMerger: Send + Sync + 'static {
508 async fn merge_single_output(
509 &self,
510 context: CompactionContext,
511 output: CompactionOutput,
512 write_opts: WriteOptions,
513 ) -> Result<(Vec<FileMeta>, Vec<SstInfo>)>;
514}
515
516#[derive(Clone)]
518pub struct DefaultSstMerger;
519
520struct OutputSequenceMetadata {
522 file_sequence: Option<NonZeroU64>,
523 exact_sequence_trusted: bool,
524}
525
526fn output_sequence_metadata(
530 context: &CompactionContext,
531 inputs: &[FileHandle],
532) -> OutputSequenceMetadata {
533 let exact_sequence_trusted = context.region_options.preserve_row_sequence
534 && inputs
535 .iter()
536 .all(|f| f.is_effective_target_sequence_trusted(context.region_id));
537 OutputSequenceMetadata {
538 file_sequence: known_max_input_sequence(inputs),
539 exact_sequence_trusted,
540 }
541}
542
543fn known_max_input_sequence(inputs: &[FileHandle]) -> Option<NonZeroU64> {
548 let mut max: Option<NonZeroU64> = None;
549 for input in inputs {
550 let sequence = input.meta_ref().sequence?;
551 max = Some(max.map_or(sequence, |current| current.max(sequence)));
552 }
553 max
554}
555
556#[async_trait::async_trait]
557impl SstMerger for DefaultSstMerger {
558 async fn merge_single_output(
559 &self,
560 context: CompactionContext,
561 output: CompactionOutput,
562 write_opts: WriteOptions,
563 ) -> Result<(Vec<FileMeta>, Vec<SstInfo>)> {
564 let region_id = context.region_id;
565 let index_options = context.version_options.index_options.clone();
566 let append_mode = context.version_options.append_mode;
567 let merge_mode = context.version_options.merge_mode();
568 let flat_format = context
569 .region_options
570 .sst_format
571 .map(|format| format == FormatType::Flat)
572 .unwrap_or(context.engine_config.default_flat_format);
573
574 let index_config = context.engine_config.index.clone();
575 let inverted_index_config = context.engine_config.inverted_index.clone();
576 let fulltext_index_config = context.engine_config.fulltext_index.clone();
577 let bloom_filter_index_config = context.engine_config.bloom_filter_index.clone();
578
579 let input_file_names = output
580 .inputs
581 .iter()
582 .map(|f| f.file_id().to_string())
583 .join(",");
584 let sequence_metadata = output_sequence_metadata(&context, &output.inputs);
585 let builder = CompactionSstReaderBuilder {
586 metadata: context.region_metadata.clone(),
587 sst_layer: context.access_layer.clone(),
588 cache: context.cache_manager.clone(),
589 inputs: &output.inputs,
590 append_mode,
591 filter_deleted: output.filter_deleted,
592 time_range: output.output_time_range,
593 merge_mode,
594 read_parallelism: write_opts.encode_parallelism,
595 };
596 let source = spawn_merge(builder.build_flat_sst_reader().await?);
597
598 let mut metrics = Metrics::new(WriteType::Compaction);
599 let region_metadata = context.region_metadata.clone();
600 let sst_infos = context
601 .access_layer
602 .write_sst(
603 SstWriteRequest {
604 op_type: OperationType::Compact,
605 metadata: region_metadata.clone(),
606 source,
607 cache_manager: context.cache_manager.clone(),
608 max_sequence: None,
612 sst_write_format: if flat_format {
613 FormatType::Flat
614 } else {
615 FormatType::PrimaryKey
616 },
617 preserve_row_sequence: sequence_metadata.exact_sequence_trusted,
618 index_options,
619 index_config,
620 inverted_index_config,
621 fulltext_index_config,
622 bloom_filter_index_config,
623 },
624 &write_opts,
625 &mut metrics,
626 )
627 .await?;
628 let partition_expr = match ®ion_metadata.partition_expr {
630 None => None,
631 Some(json_str) if json_str.is_empty() => None,
632 Some(json_str) => PartitionExpr::from_json_str(json_str).with_context(|_| {
633 InvalidPartitionExprSnafu {
634 expr: json_str.clone(),
635 }
636 })?,
637 };
638
639 let output_files = sst_infos
640 .iter()
641 .map(|sst_info| {
642 let pk_range = sst_info
643 .file_metadata
644 .as_ref()
645 .and_then(|meta| extract_primary_key_range(meta, ®ion_metadata));
646 let (primary_key_min, primary_key_max) = match pk_range {
647 Some((min, max)) => (Some(min), Some(max)),
648 None => (None, None),
649 };
650
651 FileMeta {
652 region_id,
653 file_id: sst_info.file_id,
654 time_range: sst_info.time_range,
655 level: output.output_level,
656 file_size: sst_info.file_size,
657 max_row_group_uncompressed_size: sst_info.max_row_group_uncompressed_size,
658 available_indexes: sst_info.index_metadata.build_available_indexes(),
659 indexes: sst_info.index_metadata.build_indexes(),
660 index_file_size: sst_info.index_metadata.file_size,
661 index_version: 0,
662 num_rows: sst_info.num_rows as u64,
663 num_row_groups: sst_info.num_row_groups,
664 sequence: sequence_metadata.file_sequence,
665 partition_expr: partition_expr.clone(),
666 num_series: sst_info.num_series,
667 primary_key_min,
668 primary_key_max,
669 preserve_row_sequence: sequence_metadata.exact_sequence_trusted,
670 }
671 })
672 .collect::<Vec<_>>();
673 let output_file_names = output_files.iter().map(|f| f.file_id.to_string()).join(",");
674 info!(
675 "Region {} compaction inputs: [{}], outputs: [{}], flat_format: {}, metrics: {:?}",
676 region_id, input_file_names, output_file_names, flat_format, metrics
677 );
678 metrics.observe();
679 Ok((output_files, sst_infos.into_iter().collect()))
680 }
681}
682
683pub struct DefaultCompactor<M = DefaultSstMerger> {
688 merger: M,
689 cancel_handle: Arc<CancellationHandle>,
690 uncommitted: Option<UncommittedSsts>,
691}
692
693#[cfg(test)]
694impl<M: SstMerger> DefaultCompactor<M> {
695 pub fn with_merger(merger: M) -> Self {
696 Self {
697 merger,
698 cancel_handle: Arc::new(CancellationHandle::default()),
699 uncommitted: None,
700 }
701 }
702}
703
704impl DefaultCompactor {
705 pub fn new_with_cancel_handle(cancel_handle: Arc<CancellationHandle>) -> Self {
715 Self {
716 merger: DefaultSstMerger,
717 cancel_handle,
718 uncommitted: None,
719 }
720 }
721
722 pub(crate) fn with_cancel_handle(
723 cancel_handle: Arc<CancellationHandle>,
724 uncommitted: UncommittedSsts,
725 ) -> Self {
726 Self {
727 merger: DefaultSstMerger,
728 cancel_handle,
729 uncommitted: Some(uncommitted),
730 }
731 }
732}
733
734impl<M: SstMerger> DefaultCompactor<M> {
735 pub(crate) async fn merge_unit(
737 &self,
738 context: &CompactionContext,
739 unit: &CompactionUnit,
740 ) -> Result<MergeOutput> {
741 let mut result = MergeOutput {
742 files_to_remove: unit.inputs.iter().map(|f| f.meta_ref().clone()).collect(),
743 compaction_time_window: Some(unit.time_window_size),
744 ..Default::default()
745 };
746 for output in &unit.outputs {
747 if self.cancel_handle.is_cancelled() {
748 return error::CompactionCancelledSnafu.fail();
749 }
750 let (files, infos) = self
753 .merger
754 .merge_single_output(
755 context.clone(),
756 output.clone(),
757 WriteOptions {
758 write_buffer_size: context.engine_config.sst_write_buffer_size,
759 max_file_size: unit.max_file_size,
760 row_group_size: context.region_options.row_group_size(),
761 float_field_encoding: context.region_options.float_field_encoding,
762 encode_parallelism: context
763 .region_options
764 .compaction_output_parallelism_or(
765 context
766 .engine_config
767 .experimental_compaction_output_parallelism,
768 ),
769 encode_runtime: EncodeRuntime::Compact,
770 },
771 )
772 .await?;
773 if let Some(uncommitted) = &self.uncommitted {
774 uncommitted.track(&infos);
775 }
776 result.files_to_add.extend(files);
777 result.sst_infos.extend(infos);
778 }
779 if self.cancel_handle.is_cancelled() {
780 return error::CompactionCancelledSnafu.fail();
781 }
782 Ok(result)
783 }
784}
785
786#[async_trait::async_trait]
787impl<M: SstMerger> Compactor for DefaultCompactor<M>
788where
789 M: Clone,
790{
791 async fn merge_ssts(
792 &self,
793 compaction_region: &CompactionRegion,
794 mut picker_output: PickerOutput,
795 ) -> Result<MergeOutput> {
796 let internal_parallelism = compaction_region.max_parallelism.max(1);
797 let compaction_time_window = picker_output.time_window_size;
798 let region_id = compaction_region.region_id;
799 let context = CompactionContext::from(compaction_region);
800
801 let mut tasks: Vec<(Vec<FileMeta>, _)> = Vec::with_capacity(picker_output.outputs.len());
804
805 for output in picker_output.outputs.drain(..) {
806 let inputs_to_remove: Vec<_> =
807 output.inputs.iter().map(|f| f.meta_ref().clone()).collect();
808 let write_opts = WriteOptions {
809 write_buffer_size: compaction_region.engine_config.sst_write_buffer_size,
810 max_file_size: picker_output.max_file_size,
811 row_group_size: compaction_region.region_options.row_group_size(),
812 float_field_encoding: compaction_region.region_options.float_field_encoding,
813 encode_parallelism: compaction_region
814 .region_options
815 .compaction_output_parallelism_or(
816 compaction_region
817 .engine_config
818 .experimental_compaction_output_parallelism,
819 ),
820 encode_runtime: EncodeRuntime::Compact,
821 };
822 let merger = self.merger.clone();
823 let context = context.clone();
824 let uncommitted = self.uncommitted.clone();
825 let fut = async move {
826 let result = merger
827 .merge_single_output(context, output, write_opts)
828 .await;
829 if let (Some(uncommitted), Ok((_, infos))) = (&uncommitted, &result) {
830 uncommitted.track(infos);
831 }
832 result
833 };
834 tasks.push((inputs_to_remove, fut));
835 }
836
837 let hook: Option<RegionHookRef> = compaction_region.plugins.get();
838 let mut output_files = Vec::with_capacity(tasks.len());
839 let mut all_sst_infos: Vec<SstInfo> = Vec::new();
840 let mut compacted_inputs = Vec::with_capacity(
841 tasks.iter().map(|(inputs, _)| inputs.len()).sum::<usize>()
842 + picker_output.expired_ssts.len(),
843 );
844
845 while !tasks.is_empty() && !self.cancel_handle.is_cancelled() {
846 let mut chunk: Vec<(Vec<FileMeta>, _)> = Vec::with_capacity(internal_parallelism);
847 for _ in 0..internal_parallelism {
848 if let Some(task) = tasks.pop() {
849 chunk.push(task);
850 }
851 }
852 let mut spawned: Vec<_> = chunk
853 .into_iter()
854 .map(|(inputs, fut)| {
855 let handle = common_runtime::spawn_compact(fut);
856 (inputs, handle)
857 })
858 .collect();
859
860 let mut join_error = None;
864 while let Some((inputs, handle)) = spawned.pop() {
865 match handle.await {
866 Ok(Ok((files, infos))) => {
867 output_files.extend(files);
868 if hook.is_some() {
869 all_sst_infos.extend(infos);
870 }
871 compacted_inputs.extend(inputs);
872 }
873 Ok(Err(e)) => {
874 warn!(
875 e; "Failed to merge compaction output for region: {}, inputs: [{}]",
876 region_id,
877 inputs.iter().map(|f| f.file_id.to_string()).join(",")
878 );
879 }
880 Err(e) => {
881 warn!(
882 "Region {} compaction task join error for inputs: [{}]: {}",
883 region_id,
884 inputs.iter().map(|f| f.file_id.to_string()).join(","),
885 e
886 );
887 join_error.get_or_insert(e);
889 }
890 }
891 }
892 if let Some(e) = join_error {
893 return Err(e).context(error::JoinSnafu);
894 }
895
896 if self.cancel_handle.is_cancelled() {
897 info!("Compaction merge cancelled for region: {}", region_id);
898 break;
899 }
900 }
901
902 compacted_inputs.extend(
904 picker_output
905 .expired_ssts
906 .iter()
907 .map(|f| f.meta_ref().clone()),
908 );
909
910 Ok(MergeOutput {
911 files_to_add: output_files,
912 files_to_remove: compacted_inputs,
913 compaction_time_window: Some(compaction_time_window),
914 sst_infos: all_sst_infos,
915 })
916 }
917
918 async fn update_manifest(
919 &self,
920 compaction_region: &CompactionRegion,
921 merge_output: MergeOutput,
922 ) -> Result<(RegionEdit, ManifestVersion)> {
923 CompactionContext::from(compaction_region)
924 .update_manifest(merge_output)
925 .await
926 }
927}
928
929#[cfg(test)]
930mod tests {
931 use std::sync::atomic::{AtomicUsize, Ordering};
932 use std::sync::{Arc, Mutex, Weak};
933 use std::time::Duration;
934
935 use datatypes::arrow::record_batch::RecordBatch;
936 use parquet::arrow::ArrowWriter;
937 use store_api::storage::{FileId, RegionId};
938 use tokio::sync::{mpsc, oneshot};
939 use tokio::time::timeout;
940
941 use super::{DefaultCompactor, *};
942 use crate::compaction::memory_manager::new_compaction_memory_manager;
943 use crate::compaction::picker::PickerOutput;
944 use crate::error::Result;
945 use crate::sst::file::FileHandle;
946 use crate::sst::file_purger::NoopFilePurger;
947 use crate::test_util::sst_util::new_record_batch_by_range;
948
949 fn dummy_file_meta() -> FileMeta {
950 FileMeta {
951 region_id: RegionId::new(1, 1),
952 file_id: FileId::random(),
953 file_size: 100,
954 ..Default::default()
955 }
956 }
957
958 fn new_file_handle(meta: FileMeta) -> FileHandle {
959 FileHandle::new(meta, Arc::new(NoopFilePurger))
960 }
961
962 #[test]
963 fn test_effective_target_trust_and_max_sequence() {
964 let target = RegionId::new(1, 1);
965 let local_trusted = new_file_handle(FileMeta {
966 region_id: target,
967 sequence: NonZeroU64::new(3),
968 preserve_row_sequence: true,
969 ..dummy_file_meta()
970 });
971 let foreign_untrusted_marker = new_file_handle(FileMeta {
972 region_id: RegionId::new(1, 2),
973 sequence: NonZeroU64::new(9),
974 preserve_row_sequence: false,
975 ..dummy_file_meta()
976 });
977 let local_untrusted = new_file_handle(FileMeta {
978 region_id: target,
979 sequence: NonZeroU64::new(11),
980 preserve_row_sequence: false,
981 ..dummy_file_meta()
982 });
983 assert!(local_trusted.is_effective_target_sequence_trusted(target));
984 assert!(foreign_untrusted_marker.is_effective_target_sequence_trusted(target));
985 assert!(!local_untrusted.is_effective_target_sequence_trusted(target));
986 assert_eq!(
987 NonZeroU64::new(9),
988 known_max_input_sequence(&[local_trusted, foreign_untrusted_marker])
989 );
990 }
991
992 #[test]
993 fn test_known_max_input_sequence() {
994 let meta_with = |sequence| FileMeta {
995 sequence,
996 ..dummy_file_meta()
997 };
998
999 let inputs = vec![
1000 new_file_handle(meta_with(NonZeroU64::new(3))),
1001 new_file_handle(meta_with(NonZeroU64::new(9))),
1002 new_file_handle(meta_with(NonZeroU64::new(5))),
1003 ];
1004 assert_eq!(NonZeroU64::new(9), known_max_input_sequence(&inputs));
1005
1006 let inputs = vec![
1007 new_file_handle(meta_with(NonZeroU64::new(9))),
1008 new_file_handle(meta_with(None)),
1009 ];
1010 assert_eq!(None, known_max_input_sequence(&inputs));
1011
1012 assert_eq!(None, known_max_input_sequence(&[]));
1013 }
1014
1015 #[tokio::test]
1016 async fn test_output_sequence_metadata_preserves_trust_and_unknown_bounds() {
1017 let mut region = new_test_compaction_region().await;
1018 let local = region.region_id;
1019 let foreign = RegionId::new(1, 2);
1020 let file = |region_id, sequence: Option<u64>, preserve_row_sequence| {
1021 new_file_handle(FileMeta {
1022 region_id,
1023 sequence: sequence.and_then(NonZeroU64::new),
1024 preserve_row_sequence,
1025 ..dummy_file_meta()
1026 })
1027 };
1028 let cases = [
1031 (
1032 vec![file(local, Some(3), true), file(foreign, Some(9), false)],
1033 Some(9),
1034 true,
1035 ),
1036 (
1037 vec![file(local, Some(3), true), file(local, Some(6), false)],
1038 Some(6),
1039 false,
1040 ),
1041 (
1042 vec![file(foreign, Some(9), false), file(foreign, Some(11), true)],
1043 Some(11),
1044 true,
1045 ),
1046 (
1047 vec![file(local, Some(3), true), file(local, None, true)],
1048 None,
1049 true,
1050 ),
1051 (
1052 vec![file(local, None, false), file(local, Some(6), true)],
1053 None,
1054 false,
1055 ),
1056 (
1057 vec![file(local, Some(3), true), file(foreign, None, true)],
1058 None,
1059 false,
1060 ),
1061 (vec![], None, true),
1062 (
1063 vec![file(local, Some(u64::MAX), false)],
1064 Some(u64::MAX),
1065 false,
1066 ),
1067 ];
1068 for committed_sequence in [100, 101] {
1069 region
1070 .manifest_ctx
1071 .manifest_manager
1072 .write()
1073 .await
1074 .update(
1075 RegionMetaActionList::with_action(RegionMetaAction::Edit(RegionEdit {
1076 committed_sequence: Some(committed_sequence),
1077 files_to_add: vec![],
1078 files_to_remove: vec![],
1079 timestamp_ms: None,
1080 compaction_time_window: None,
1081 flushed_entry_id: None,
1082 flushed_sequence: None,
1083 })),
1084 false,
1085 )
1086 .await
1087 .unwrap();
1088 for preserve in [false, true] {
1089 region.region_options.preserve_row_sequence = preserve;
1090 for (inputs, known_bound, inputs_trusted) in &cases {
1091 let metadata =
1092 output_sequence_metadata(&CompactionContext::from(®ion), inputs);
1093 let trusted = preserve && *inputs_trusted;
1094 assert_eq!(trusted, metadata.exact_sequence_trusted);
1095 assert_eq!(
1096 known_bound.and_then(NonZeroU64::new),
1097 metadata.file_sequence
1098 );
1099 }
1100 }
1101 }
1102 }
1103
1104 #[rstest::rstest]
1108 #[case(true)]
1109 #[case(false)]
1110 #[tokio::test]
1111 async fn test_independent_outputs_preserve_delete_in_every_visibility_state(
1112 #[case] flat_format: bool,
1113 #[values(false, true)] unknown_put_bound: bool,
1114 ) {
1115 use datatypes::arrow::array::AsArray;
1116 use datatypes::arrow::datatypes::TimestampMillisecondType;
1117 use store_api::region_engine::RegionEngine;
1118 use store_api::region_request::RegionRequest;
1119
1120 use crate::compaction::CompactionOutput;
1121 use crate::compaction::compactor::{CompactionRegion, Compactor, DefaultCompactor};
1122 use crate::compaction::picker::PickerOutput;
1123 use crate::compaction::reader::CompactionSstReaderBuilder;
1124 use crate::engine::compaction_test::{delete_and_flush, put_and_flush};
1125 use crate::region::options::MergeMode;
1126 use crate::sst::file::FileHandle;
1127 use crate::sst::file_purger::NoopFilePurger;
1128 use crate::test_util::{CreateRequestBuilder, TestEnv, rows_schema};
1129
1130 let mut env = TestEnv::new().await;
1131 let region_id = RegionId::new(1, 1);
1132 let config = MitoConfig {
1133 default_flat_format: flat_format,
1134 min_compaction_interval: Duration::from_secs(3600),
1135 ..Default::default()
1136 };
1137 let engine = env.create_engine(config).await;
1138 let request = CreateRequestBuilder::new().build();
1139 let columns = rows_schema(&request);
1140 engine
1141 .handle_request(region_id, RegionRequest::Create(request))
1142 .await
1143 .unwrap();
1144 put_and_flush(&engine, region_id, &columns, 0..1).await;
1145 delete_and_flush(&engine, region_id, &columns, 0..1).await;
1146 put_and_flush(&engine, region_id, &columns, 10..11).await;
1147 let region = engine.get_region(region_id).unwrap();
1148 let version = region.version();
1149 let mut files: Vec<_> = version
1150 .ssts
1151 .levels()
1152 .iter()
1153 .flat_map(|level| level.files())
1154 .cloned()
1155 .collect();
1156 assert_eq!(3, files.len());
1157 files.sort_unstable_by_key(|file| file.meta_ref().sequence);
1158 if unknown_put_bound {
1159 let mut meta = files[0].meta_ref().clone();
1162 meta.sequence = None;
1163 files[0] = FileHandle::new(meta, Arc::new(NoopFilePurger));
1164 }
1165 let compaction_region = CompactionRegion {
1166 region_id,
1167 region_options: version.options.clone(),
1168 engine_config: Arc::new(MitoConfig {
1169 default_flat_format: flat_format,
1170 ..Default::default()
1171 }),
1172 region_metadata: version.metadata.clone(),
1173 cache_manager: engine.cache_manager(),
1174 access_layer: region.access_layer.clone(),
1175 manifest_ctx: region.manifest_ctx.clone(),
1176 current_version: version.into(),
1177 file_purger: None,
1178 ttl: None,
1179 max_parallelism: 2,
1180 plugins: common_base::Plugins::new(),
1181 };
1182 let puts = vec![files[0].clone(), files[2].clone()];
1183 let deletes = vec![files[1].clone()];
1184 let picker_output = PickerOutput {
1185 outputs: [puts.clone(), deletes.clone()]
1186 .into_iter()
1187 .map(|inputs| CompactionOutput {
1188 output_level: 1,
1189 inputs,
1190 filter_deleted: false,
1191 output_time_range: None,
1192 })
1193 .collect(),
1194 expired_ssts: vec![],
1195 time_window_size: 3600,
1196 max_file_size: None,
1197 };
1198 let merged = DefaultCompactor::with_merger(DefaultSstMerger)
1199 .merge_ssts(&compaction_region, picker_output)
1200 .await
1201 .unwrap();
1202 assert_eq!(2, merged.files_to_add.len());
1203 let outputs: Vec<_> = merged
1204 .files_to_add
1205 .into_iter()
1206 .map(|meta| FileHandle::new(meta, Arc::new(NoopFilePurger)))
1207 .collect();
1208 let put_output = outputs
1209 .iter()
1210 .find(|file| file.meta_ref().num_rows == 2)
1211 .unwrap();
1212 let delete_output = outputs
1213 .iter()
1214 .find(|file| file.meta_ref().num_rows == 1)
1215 .unwrap();
1216 assert_eq!(
1217 if unknown_put_bound {
1218 None
1219 } else {
1220 NonZeroU64::new(3)
1221 },
1222 put_output.meta_ref().sequence,
1223 );
1224 assert_eq!(NonZeroU64::new(2), delete_output.meta_ref().sequence);
1225 assert!(
1226 outputs
1227 .iter()
1228 .all(|file| !file.meta_ref().preserve_row_sequence)
1229 );
1230
1231 for (put_done, delete_done) in [(false, false), (true, false), (false, true), (true, true)]
1234 {
1235 let mut visible = if put_done {
1236 vec![put_output.clone()]
1237 } else {
1238 puts.clone()
1239 };
1240 visible.extend(if delete_done {
1241 vec![delete_output.clone()]
1242 } else {
1243 deletes.clone()
1244 });
1245 let mut reader = CompactionSstReaderBuilder {
1246 metadata: compaction_region.region_metadata.clone(),
1247 sst_layer: region.access_layer.clone(),
1248 cache: engine.cache_manager(),
1249 inputs: &visible,
1250 append_mode: false,
1251 filter_deleted: true,
1252 time_range: None,
1253 merge_mode: MergeMode::LastRow,
1254 read_parallelism: 1,
1255 }
1256 .build_flat_sst_reader()
1257 .await
1258 .unwrap();
1259 let mut timestamps = Vec::new();
1260 while let Some(batch) = reader.next_batch().await.unwrap() {
1261 timestamps.extend(
1262 batch
1263 .column_by_name("ts")
1264 .unwrap()
1265 .as_primitive::<TimestampMillisecondType>()
1266 .values()
1267 .iter()
1268 .copied(),
1269 );
1270 }
1271 assert_eq!(
1272 vec![10_000],
1273 timestamps,
1274 "put_done={put_done}, delete_done={delete_done}"
1275 );
1276 }
1277 }
1278
1279 #[derive(Clone)]
1283 struct MockMerger {
1284 results: Arc<Mutex<Vec<Result<Vec<FileMeta>>>>>,
1285 call_idx: Arc<AtomicUsize>,
1286 }
1287
1288 impl MockMerger {
1289 fn new(results: Vec<Result<Vec<FileMeta>>>) -> Self {
1290 Self {
1291 results: Arc::new(Mutex::new(results)),
1292 call_idx: Arc::new(AtomicUsize::new(0)),
1293 }
1294 }
1295 }
1296
1297 #[async_trait::async_trait]
1298 impl SstMerger for MockMerger {
1299 async fn merge_single_output(
1300 &self,
1301 _context: CompactionContext,
1302 _output: CompactionOutput,
1303 _write_opts: WriteOptions,
1304 ) -> Result<(Vec<FileMeta>, Vec<SstInfo>)> {
1305 let idx = self.call_idx.fetch_add(1, Ordering::SeqCst);
1306 match self.results.lock().unwrap().get(idx) {
1307 Some(Ok(files)) => Ok((files.clone(), Vec::new())),
1308 Some(Err(_)) => error::InvalidMetaSnafu {
1309 reason: format!("simulated failure at index {idx}"),
1310 }
1311 .fail(),
1312 None => panic!("MockMerger: no result configured for call index {idx}"),
1313 }
1314 }
1315 }
1316
1317 #[tokio::test]
1318 async fn test_compaction_unit_group_failure_is_atomic() {
1319 let context = CompactionContext::from(&new_test_compaction_region().await);
1320 let input = new_file_handle(dummy_file_meta());
1321 let unit = CompactionUnit::from_picker(PickerOutput {
1322 outputs: (0..2)
1323 .map(|_| CompactionOutput {
1324 output_level: 1,
1325 inputs: vec![input.clone()],
1326 filter_deleted: false,
1327 output_time_range: None,
1328 })
1329 .collect(),
1330 ..Default::default()
1331 })
1332 .unwrap()
1333 .pop()
1334 .unwrap();
1335 for fail_second in [false, true] {
1336 let second = if fail_second {
1337 error::InvalidMetaSnafu {
1338 reason: "second output failed",
1339 }
1340 .fail()
1341 } else {
1342 Ok(vec![dummy_file_meta(), dummy_file_meta()])
1343 };
1344 let compactor =
1345 DefaultCompactor::with_merger(MockMerger::new(vec![Ok(Vec::new()), second]));
1346 let result = compactor.merge_unit(&context, &unit).await;
1347 if fail_second {
1348 assert!(
1349 result.is_err(),
1350 "cannot publish a successful subset of a dependency group"
1351 );
1352 } else {
1353 let result = result.unwrap();
1354 assert_eq!(2, result.files_to_add.len());
1355 assert_eq!(
1356 vec![input.meta_ref().file_id],
1357 result
1358 .files_to_remove
1359 .iter()
1360 .map(|f| f.file_id)
1361 .collect::<Vec<_>>()
1362 );
1363 }
1364 }
1365 }
1366
1367 #[tokio::test]
1368 async fn test_partial_merge_failure_collects_only_successful_outputs() {
1369 common_telemetry::init_default_ut_logging();
1370
1371 let compaction_region = new_test_compaction_region().await;
1372
1373 let input_meta_0 = dummy_file_meta();
1375 let input_meta_1 = dummy_file_meta();
1376 let input_meta_2 = dummy_file_meta();
1377
1378 let output_meta_0 = vec![dummy_file_meta()];
1379 let output_meta_2 = vec![dummy_file_meta(), dummy_file_meta()];
1380
1381 let merger = MockMerger::new(vec![
1382 Ok(output_meta_0.clone()),
1383 Err(error::InvalidMetaSnafu {
1384 reason: "boom".to_string(),
1385 }
1386 .build()),
1387 Ok(output_meta_2.clone()),
1388 ]);
1389 let compactor = DefaultCompactor::with_merger(merger);
1390
1391 let picker_output = PickerOutput {
1392 outputs: vec![
1393 CompactionOutput {
1394 output_level: 1,
1395 inputs: vec![new_file_handle(input_meta_0.clone())],
1396 filter_deleted: false,
1397 output_time_range: None,
1398 },
1399 CompactionOutput {
1400 output_level: 1,
1401 inputs: vec![new_file_handle(input_meta_1.clone())],
1402 filter_deleted: false,
1403 output_time_range: None,
1404 },
1405 CompactionOutput {
1406 output_level: 1,
1407 inputs: vec![new_file_handle(input_meta_2.clone())],
1408 filter_deleted: false,
1409 output_time_range: None,
1410 },
1411 ],
1412 expired_ssts: vec![],
1413 time_window_size: 3600,
1414 max_file_size: None,
1415 };
1416
1417 let merge_output = compactor
1418 .merge_ssts(&compaction_region, picker_output)
1419 .await
1420 .unwrap();
1421
1422 assert_eq!(merge_output.files_to_add.len(), 3);
1424 assert_eq!(merge_output.files_to_remove.len(), 2);
1426
1427 let removed_ids: Vec<_> = merge_output
1428 .files_to_remove
1429 .iter()
1430 .map(|f| f.file_id)
1431 .collect();
1432 assert!(removed_ids.contains(&input_meta_0.file_id));
1433 assert!(removed_ids.contains(&input_meta_2.file_id));
1434 assert!(!removed_ids.contains(&input_meta_1.file_id));
1436 }
1437
1438 #[tokio::test]
1439 async fn test_all_outputs_succeed() {
1440 common_telemetry::init_default_ut_logging();
1441
1442 let compaction_region = new_test_compaction_region().await;
1443 let input_meta = dummy_file_meta();
1444 let output_meta = vec![dummy_file_meta()];
1445
1446 let merger = MockMerger::new(vec![Ok(output_meta.clone())]);
1447 let compactor = DefaultCompactor::with_merger(merger);
1448
1449 let picker_output = PickerOutput {
1450 outputs: vec![CompactionOutput {
1451 output_level: 1,
1452 inputs: vec![new_file_handle(input_meta.clone())],
1453 filter_deleted: false,
1454 output_time_range: None,
1455 }],
1456 expired_ssts: vec![],
1457 time_window_size: 3600,
1458 max_file_size: None,
1459 };
1460
1461 let merge_output = compactor
1462 .merge_ssts(&compaction_region, picker_output)
1463 .await
1464 .unwrap();
1465
1466 assert_eq!(merge_output.files_to_add.len(), 1);
1467 assert_eq!(merge_output.files_to_add[0].file_id, output_meta[0].file_id);
1468 assert_eq!(merge_output.files_to_remove.len(), 1);
1469 assert_eq!(merge_output.files_to_remove[0].file_id, input_meta.file_id);
1470 }
1471
1472 #[tokio::test]
1473 async fn test_expired_ssts_always_removed() {
1474 common_telemetry::init_default_ut_logging();
1475
1476 let compaction_region = new_test_compaction_region().await;
1477 let input_meta = dummy_file_meta();
1478 let expired_meta = dummy_file_meta();
1479
1480 let merger = MockMerger::new(vec![Err(error::InvalidMetaSnafu {
1482 reason: "fail".to_string(),
1483 }
1484 .build())]);
1485 let compactor = DefaultCompactor::with_merger(merger);
1486
1487 let picker_output = PickerOutput {
1488 outputs: vec![CompactionOutput {
1489 output_level: 1,
1490 inputs: vec![new_file_handle(input_meta.clone())],
1491 filter_deleted: false,
1492 output_time_range: None,
1493 }],
1494 expired_ssts: vec![new_file_handle(expired_meta.clone())],
1495 time_window_size: 3600,
1496 max_file_size: None,
1497 };
1498
1499 let merge_output = compactor
1500 .merge_ssts(&compaction_region, picker_output)
1501 .await
1502 .unwrap();
1503
1504 assert!(merge_output.files_to_add.is_empty());
1506 assert_eq!(merge_output.files_to_remove.len(), 1);
1508 assert_eq!(
1509 merge_output.files_to_remove[0].file_id,
1510 expired_meta.file_id
1511 );
1512 }
1513
1514 #[derive(Debug)]
1515 struct PendingEncoding {
1516 input_id: FileId,
1517 output_id: FileId,
1518 batch: Weak<RecordBatch>,
1519 release: oneshot::Sender<()>,
1520 }
1521
1522 #[derive(Clone)]
1524 struct GatedEncodingMerger {
1525 started: mpsc::UnboundedSender<PendingEncoding>,
1526 panic_input: Option<FileId>,
1527 }
1528
1529 #[async_trait::async_trait]
1530 impl SstMerger for GatedEncodingMerger {
1531 async fn merge_single_output(
1532 &self,
1533 _context: CompactionContext,
1534 output: CompactionOutput,
1535 _write_opts: WriteOptions,
1536 ) -> Result<(Vec<FileMeta>, Vec<SstInfo>)> {
1537 let input_id = output.inputs[0].meta_ref().file_id;
1538 let mut file = dummy_file_meta();
1539 let batch = Arc::new(new_record_batch_by_range(&["a", "b"], 0, 4));
1540 let (release, wait) = oneshot::channel();
1541 let pending = PendingEncoding {
1542 input_id,
1543 output_id: file.file_id,
1544 batch: Arc::downgrade(&batch),
1545 release,
1546 };
1547 let started = self.started.clone();
1548 file.file_size = common_runtime::spawn_blocking_compact(move || {
1549 started.send(pending).unwrap();
1550 wait.blocking_recv().unwrap();
1551 let mut bytes = Vec::new();
1552 let mut writer = ArrowWriter::try_new(&mut bytes, batch.schema(), None)?;
1553 writer.write(&batch)?;
1554 writer.close()?;
1555 Ok::<_, parquet::errors::ParquetError>(bytes.len() as u64)
1556 })
1557 .await
1558 .context(error::JoinSnafu)?
1559 .context(error::WriteParquetSnafu)?;
1560 if self.panic_input == Some(input_id) {
1561 panic!("merge task panicked");
1562 }
1563 Ok((vec![file], Vec::new()))
1564 }
1565 }
1566
1567 #[rstest::rstest]
1568 #[case::successful_outputs(false)]
1569 #[case::join_error(true)]
1570 #[tokio::test]
1571 async fn test_merge_ssts_cancellation_waits_for_encoders(#[case] panic_merge: bool) {
1572 let mut region = new_test_compaction_region().await;
1573 region.max_parallelism = 2;
1574 region.region_options.compaction_output_parallelism = Some(2);
1575 let inputs: Vec<_> = (0..3).map(|_| dummy_file_meta()).collect();
1576 let picker_output = PickerOutput {
1577 outputs: inputs
1578 .iter()
1579 .map(|input| CompactionOutput {
1580 output_level: 1,
1581 inputs: vec![new_file_handle(input.clone())],
1582 filter_deleted: false,
1583 output_time_range: None,
1584 })
1585 .collect(),
1586 ..Default::default()
1587 };
1588 let cancel_handle = Arc::new(CancellationHandle::default());
1589 let (started_tx, mut started_rx) = mpsc::unbounded_channel();
1590 let compactor = DefaultCompactor {
1591 merger: GatedEncodingMerger {
1592 started: started_tx,
1593 panic_input: panic_merge.then_some(inputs[1].file_id),
1595 },
1596 cancel_handle: cancel_handle.clone(),
1597 uncommitted: None,
1598 };
1599 let memory = new_compaction_memory_manager(1024 * 1024);
1600 let budget = memory.try_acquire(memory.limit_bytes()).unwrap();
1601 let mut merge = Box::pin(async {
1602 let _budget = budget;
1603 compactor.merge_ssts(®ion, picker_output).await
1604 });
1605 assert!(futures::poll!(merge.as_mut()).is_pending());
1606 let mut encoders = Vec::new();
1607 for _ in 0..2 {
1608 encoders.push(
1609 timeout(Duration::from_secs(10), started_rx.recv())
1610 .await
1611 .unwrap()
1612 .unwrap(),
1613 );
1614 }
1615 cancel_handle.cancel();
1616 assert!(futures::poll!(merge.as_mut()).is_pending());
1617 assert!(memory.try_acquire(memory.limit_bytes()).is_none());
1618 assert!(
1619 encoders
1620 .iter()
1621 .all(|encoder| encoder.batch.upgrade().is_some())
1622 );
1623 let retained_batches: Vec<_> = encoders
1624 .iter()
1625 .map(|encoder| encoder.batch.clone())
1626 .collect();
1627 let mut expected_inputs: Vec<_> = encoders.iter().map(|encoder| encoder.input_id).collect();
1628 let mut expected_outputs: Vec<_> =
1629 encoders.iter().map(|encoder| encoder.output_id).collect();
1630 for encoder in encoders {
1631 encoder.release.send(()).unwrap();
1632 }
1633 let result = timeout(Duration::from_secs(10), merge).await.unwrap();
1634 assert!(
1635 retained_batches
1636 .iter()
1637 .all(|batch| batch.upgrade().is_none())
1638 );
1639 assert!(memory.try_acquire(memory.limit_bytes()).is_some());
1640 assert!(
1641 started_rx.try_recv().is_err(),
1642 "cancelled work must not start another output"
1643 );
1644 if panic_merge {
1645 assert!(matches!(result, Err(error::Error::Join { error, .. }) if error.is_panic()));
1646 } else {
1647 let merged = result.unwrap();
1648 let mut removed: Vec<_> = merged
1649 .files_to_remove
1650 .iter()
1651 .map(|file| file.file_id)
1652 .collect();
1653 let mut added: Vec<_> = merged
1654 .files_to_add
1655 .iter()
1656 .map(|file| file.file_id)
1657 .collect();
1658 for ids in [
1659 &mut removed,
1660 &mut added,
1661 &mut expected_inputs,
1662 &mut expected_outputs,
1663 ] {
1664 ids.sort_by(|a, b| a.as_bytes().cmp(b.as_bytes()));
1665 }
1666 assert_eq!(expected_inputs, removed);
1667 assert_eq!(expected_outputs, added);
1668 assert!(merged.files_to_add.iter().all(|file| file.file_size > 0));
1669 }
1670 }
1671
1672 #[tokio::test]
1673 async fn test_merge_ssts_cancelled_before_start_skips_outputs() {
1674 let region = new_test_compaction_region().await;
1675 let merger = MockMerger::new(vec![Ok(vec![dummy_file_meta()])]);
1676 let calls = merger.call_idx.clone();
1677 let compactor = DefaultCompactor::with_merger(merger);
1678 compactor.cancel_handle.cancel();
1679 let picker_output = PickerOutput {
1680 outputs: vec![CompactionOutput {
1681 output_level: 1,
1682 inputs: vec![new_file_handle(dummy_file_meta())],
1683 filter_deleted: false,
1684 output_time_range: None,
1685 }],
1686 ..Default::default()
1687 };
1688 let merged = compactor.merge_ssts(®ion, picker_output).await.unwrap();
1689 assert!(merged.is_empty());
1690 assert_eq!(0, calls.load(Ordering::SeqCst));
1691 }
1692}