1use std::fmt;
16use std::future::Future;
17use std::sync::{Arc, Mutex};
18use std::time::Instant;
19
20use api::v1::region::compact_request;
21use common_base::Plugins;
22use common_meta::key::SchemaMetadataManagerRef;
23use common_telemetry::{debug, error, info, warn};
24use common_time::TimeToLive;
25use common_time::range::TimestampRange;
26use futures::FutureExt;
27use snafu::ResultExt;
28use store_api::storage::RegionId;
29use tokio::sync::mpsc::{self, Sender};
30
31use crate::access_layer::AccessLayerRef;
32use crate::cache::CacheManagerRef;
33use crate::compaction::compactor::{CompactionRegion, CompactionVersion, DefaultCompactor};
34use crate::compaction::picker::{CompactionTask, PickerOutput, new_picker};
35use crate::compaction::scheduler::state::{CompactingFiles, CompactionExecution, CompactionPhase};
36use crate::compaction::scheduler::{CompactionScheduler, CompactionTransition};
37use crate::compaction::task::CompactionTaskImpl;
38use crate::compaction::{CompactionOutput, find_dynamic_options};
39use crate::config::MitoConfig;
40use crate::error::{CompactRegionSnafu, Error, RemoteCompactionSnafu, Result, UnexpectedSnafu};
41use crate::metrics::{
42 COMPACTION_MEMORY_REJECTED, COMPACTION_STAGE_ELAPSED, INFLIGHT_COMPACTION_COUNT,
43};
44use crate::region::ManifestContextRef;
45use crate::region::options::RegionOptions;
46use crate::request::{BackgroundNotify, OutputTx, WorkerRequest, WorkerRequestWithTime};
47use crate::schedule::CancellableTaskState;
48use crate::schedule::remote_job_scheduler::{
49 CompactionJob, DefaultNotifier, RemoteJob, RemoteJobSchedulerRef,
50};
51use crate::sst::file::{FileHandle, UncommittedSsts};
52use crate::sst::version::SstVersion;
53use crate::worker::WorkerListener;
54
55pub struct CompactionRequest {
57 pub(crate) engine_config: Arc<MitoConfig>,
58 pub(crate) current_version: CompactionVersion,
59 pub(crate) access_layer: AccessLayerRef,
60 pub(crate) request_sender: mpsc::Sender<WorkerRequestWithTime>,
62 pub(crate) start_time: Instant,
64 pub(crate) cache_manager: CacheManagerRef,
65 pub(crate) manifest_ctx: ManifestContextRef,
66 pub(crate) listener: WorkerListener,
67 pub(crate) schema_metadata_manager: SchemaMetadataManagerRef,
68 pub(crate) max_parallelism: usize,
69}
70
71impl CompactionRequest {
72 pub(crate) fn region_id(&self) -> RegionId {
73 self.current_version.metadata.region_id
74 }
75}
76
77pub(crate) enum CompactionPlanningResult {
79 Prepared(PreparedCompaction),
80 NoPlan,
81 Error(Arc<Error>),
82}
83
84impl fmt::Debug for CompactionPlanningResult {
85 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86 match self {
87 Self::Prepared(prepared) => f
88 .debug_tuple("Prepared")
89 .field(&prepared.compaction_region.region_id)
90 .finish(),
91 Self::NoPlan => f.write_str("NoPlan"),
92 Self::Error(err) => f.debug_tuple("Error").field(err).finish(),
93 }
94 }
95}
96
97#[derive(Debug)]
99pub(crate) struct CompactionPickFinished {
100 pub(crate) region_id: RegionId,
101 pub(crate) plan_id: u64,
102 pub(crate) result: CompactionPlanningResult,
103}
104
105pub(crate) struct PreparedCompaction {
106 pub(super) compaction_region: CompactionRegion,
107 pub(super) picker_output: PickerOutput,
108 start_time: Instant,
109 ttl: TimeToLive,
110}
111
112impl CompactionScheduler {
113 pub(super) fn dispatch_compaction_planning(
114 &self,
115 plan_id: u64,
116 request: CompactionRequest,
117 options: compact_request::Options,
118 time_range: Option<TimestampRange>,
119 ) {
120 let plugins = self.plugins.clone();
121 let max_background_compactions = self.engine_config.max_background_compactions;
122 common_runtime::spawn_compact(async move {
123 let region_id = request.region_id();
124 let request_sender = request.request_sender.clone();
125 let planning = Self::prepare_compaction(
126 request,
127 options,
128 plugins,
129 max_background_compactions,
130 time_range,
131 );
132 Self::notify_planning_result(region_id, plan_id, request_sender, planning).await;
133 });
134 }
135
136 pub(super) async fn notify_planning_result(
145 region_id: RegionId,
146 plan_id: u64,
147 request_sender: Sender<WorkerRequestWithTime>,
148 planning: impl Future<Output = CompactionPlanningResult> + Send,
149 ) {
150 let result = std::panic::AssertUnwindSafe(planning).catch_unwind().await.unwrap_or_else(|payload| {
152 let reason = if let Some(message) = payload.as_ref().downcast_ref::<&str>() {
153 message.to_string()
154 } else if let Some(message) = payload.as_ref().downcast_ref::<String>() {
155 message.clone()
156 } else {
157 "unknown panic".to_string()
158 };
159 CompactionPlanningResult::Error(Arc::new(
160 UnexpectedSnafu {
161 reason: format!(
162 "Compaction planning panicked for region {region_id}, plan_id {plan_id}: {reason}"
163 ),
164 }
165 .build(),
166 ))
167 });
168 if let CompactionPlanningResult::Error(err) = &result {
169 error!(err; "Compaction planning failed for region {}, plan_id: {}", region_id, plan_id);
170 }
171 let request = WorkerRequestWithTime::new(WorkerRequest::Background {
172 region_id,
173 notify: BackgroundNotify::CompactionPickFinished(CompactionPickFinished {
174 region_id,
175 plan_id,
176 result,
177 }),
178 });
179 if request_sender.send(request).await.is_err() {
180 warn!("Failed to send compaction planning result for region {region_id}");
181 }
182 }
183
184 async fn prepare_compaction(
185 request: CompactionRequest,
186 options: compact_request::Options,
187 plugins: Plugins,
188 max_background_compactions: usize,
189 time_range: Option<TimestampRange>,
190 ) -> CompactionPlanningResult {
191 let region_id = request.region_id();
192 let (dynamic_compaction_opts, ttl) = find_dynamic_options(
193 region_id,
194 &request.current_version.options,
195 &request.schema_metadata_manager,
196 )
197 .await
198 .unwrap_or_else(|e| {
199 warn!(e; "Failed to find dynamic options for region: {}", region_id);
200 (
201 request.current_version.options.compaction.clone(),
202 request.current_version.options.ttl.unwrap_or_default(),
203 )
204 });
205
206 let picker = new_picker(
207 &options,
208 &dynamic_compaction_opts,
209 request.current_version.options.append_mode,
210 Some(max_background_compactions),
211 time_range,
212 );
213 let region_id = request.region_id();
214 let CompactionRequest {
215 engine_config,
216 current_version,
217 access_layer,
218 request_sender: _,
219 start_time,
220 cache_manager,
221 manifest_ctx,
222 listener,
223 schema_metadata_manager: _,
224 max_parallelism,
225 } = request;
226
227 debug!(
228 "Pick compaction strategy {:?} for region: {}, ttl: {:?}",
229 picker, region_id, ttl
230 );
231
232 let compaction_region = CompactionRegion {
233 region_id,
234 current_version: current_version.clone(),
235 region_options: RegionOptions {
236 compaction: dynamic_compaction_opts.clone(),
237 ..current_version.options.clone()
238 },
239 engine_config: engine_config.clone(),
240 region_metadata: current_version.metadata.clone(),
241 cache_manager: cache_manager.clone(),
242 access_layer: access_layer.clone(),
243 manifest_ctx: manifest_ctx.clone(),
244 file_purger: None,
245 ttl: Some(ttl),
246 max_parallelism,
247 plugins,
248 };
249
250 listener.on_compaction_pick_begin(region_id).await;
251 let _pick_timer = COMPACTION_STAGE_ELAPSED
252 .with_label_values(&["pick"])
253 .start_timer();
254 let picker_output = match picker.pick(&compaction_region).await {
255 Ok(output) => output,
256 Err(err) => return CompactionPlanningResult::Error(Arc::new(err)),
257 };
258
259 let Some(picker_output) = picker_output else {
260 return CompactionPlanningResult::NoPlan;
261 };
262
263 CompactionPlanningResult::Prepared(PreparedCompaction {
264 compaction_region,
265 picker_output,
266 start_time,
267 ttl,
268 })
269 }
270
271 pub(super) async fn handle_compaction_pick_finished_inner(
280 &mut self,
281 finished: CompactionPickFinished,
282 manifest_ctx: &ManifestContextRef,
283 schema_metadata_manager: SchemaMetadataManagerRef,
284 ) -> CompactionTransition {
285 let region_id = finished.region_id;
286 let plan_id = finished.plan_id;
287 let Some(status) = self.region_status.get(®ion_id) else {
288 return CompactionTransition::NoAction;
289 };
290
291 if !status.is_picking(finished.plan_id) {
292 return CompactionTransition::NoAction;
293 }
294 if !status.accept_plan(finished.plan_id) {
297 return CompactionTransition::from_pending_ddls(
298 self.remove_region_on_cancel(region_id),
299 );
300 }
301
302 match finished.result {
303 CompactionPlanningResult::Prepared(mut prepared) => {
304 let current = status.version_control.current().version;
305 let Some(picker_output) =
306 refresh_picker_output(prepared.picker_output, ¤t.ssts)
307 else {
308 return self
309 .finish_compaction_planning(
310 region_id,
311 None,
312 manifest_ctx,
313 schema_metadata_manager,
314 )
315 .await;
316 };
317 let Some(files) = CompactingFiles::try_new(&picker_output) else {
318 return self
319 .finish_compaction_planning(
320 region_id,
321 None,
322 manifest_ctx,
323 schema_metadata_manager,
324 )
325 .await;
326 };
327 prepared.picker_output = picker_output;
328 let Some(status) = self.region_status.get_mut(®ion_id) else {
329 return CompactionTransition::NoAction;
330 };
331 let waiters = status.take_waiters();
332 match self
333 .submit_prepared_compaction(prepared, files, waiters, plan_id)
334 .await
335 {
336 Ok(Some(phase)) => {
337 if let Some(status) = self.region_status.get_mut(®ion_id) {
338 status.set_phase(phase);
339 }
340 CompactionTransition::NoAction
343 }
344 Ok(None) => {
345 self.finish_compaction_planning(
346 region_id,
347 None,
348 manifest_ctx,
349 schema_metadata_manager,
350 )
351 .await
352 }
353 Err(err) => {
354 self.remove_region_on_failure(region_id, Arc::new(err));
355 CompactionTransition::NoAction
356 }
357 }
358 }
359 CompactionPlanningResult::NoPlan => {
362 self.finish_compaction_planning(
363 region_id,
364 None,
365 manifest_ctx,
366 schema_metadata_manager,
367 )
368 .await
369 }
370 CompactionPlanningResult::Error(err) => {
371 self.finish_compaction_planning(
372 region_id,
373 Some(err),
374 manifest_ctx,
375 schema_metadata_manager,
376 )
377 .await
378 }
379 }
380 }
381
382 async fn finish_compaction_planning(
383 &mut self,
384 region_id: RegionId,
385 err: Option<Arc<Error>>,
386 manifest_ctx: &ManifestContextRef,
387 schema_metadata_manager: SchemaMetadataManagerRef,
388 ) -> CompactionTransition {
389 let Some(status) = self.region_status.get_mut(®ion_id) else {
390 return CompactionTransition::NoAction;
391 };
392 for waiter in status.take_waiters() {
393 if let Some(err) = &err {
394 waiter.send(Err(err.clone()).context(CompactRegionSnafu { region_id }));
395 } else {
396 waiter.send(Ok(0));
397 }
398 }
399
400 if self.handle_pending_compaction_request(
401 region_id,
402 manifest_ctx,
403 schema_metadata_manager.clone(),
404 ) {
405 return CompactionTransition::NoAction;
406 }
407
408 let Some(status) = self.region_status.get_mut(®ion_id) else {
409 return CompactionTransition::NoAction;
410 };
411
412 let pending_ddls = std::mem::take(&mut status.pending_ddl_requests);
415 if !pending_ddls.is_empty() {
416 self.region_status.remove(®ion_id);
417 return CompactionTransition::DdlReady(pending_ddls);
418 }
419
420 if status.active.reset_automatic_followup()
421 && self.schedule_automatic_followup(region_id, manifest_ctx, schema_metadata_manager)
422 {
423 return CompactionTransition::AutomaticFollowupScheduled;
424 }
425
426 self.region_status.remove(®ion_id);
427 CompactionTransition::NoAction
428 }
429
430 async fn submit_prepared_compaction(
431 &mut self,
432 prepared: PreparedCompaction,
433 files: CompactingFiles,
434 waiters: Vec<OutputTx>,
435 mut plan_id: u64,
436 ) -> Result<Option<CompactionPhase>> {
437 let PreparedCompaction {
438 compaction_region,
439 picker_output,
440 start_time,
441 ttl,
442 } = prepared;
443 let region_id = compaction_region.region_id;
444 let dynamic_compaction_opts = &compaction_region.region_options.compaction;
445
446 let waiters = if dynamic_compaction_opts.remote_compaction() {
449 if let Some(remote_job_scheduler) = &self.plugins.get::<RemoteJobSchedulerRef>() {
450 let execution = CompactionExecution::new(plan_id, files.clone());
451 let remote_compaction_job = CompactionJob {
452 compaction_region: compaction_region.clone(),
453 picker_output: picker_output.clone(),
454 start_time,
455 waiters,
456 ttl,
457 };
458
459 let result = remote_job_scheduler
460 .schedule(
461 RemoteJob::CompactionJob(remote_compaction_job),
462 Box::new(DefaultNotifier::new(
463 self.request_sender.clone(),
464 execution.clone(),
465 )),
466 )
467 .await;
468
469 match result {
470 Ok(job_id) => {
471 info!(
472 "Scheduled remote compaction job {} for region {}",
473 job_id, region_id
474 );
475 INFLIGHT_COMPACTION_COUNT.inc();
476 return Ok(Some(CompactionPhase::Remote { execution }));
477 }
478 Err(e) => {
479 if !dynamic_compaction_opts.fallback_to_local() {
480 error!(e; "Failed to schedule remote compaction job for region {}", region_id);
481 if let Some(status) = self.region_status.get_mut(®ion_id) {
482 status.extend_waiters(e.waiters);
483 }
484 return RemoteCompactionSnafu {
485 region_id,
486 job_id: None,
487 reason: e.reason,
488 }
489 .fail();
490 }
491
492 error!(e; "Failed to schedule remote compaction job for region {}, fallback to local compaction", region_id);
493 plan_id = Self::next_plan_id(&mut self.next_plan_id);
496 e.waiters
497 }
498 }
499 } else {
500 debug!(
501 "Remote compaction is not enabled, fallback to local compaction for region {}",
502 region_id
503 );
504 waiters
505 }
506 } else {
507 waiters
508 };
509
510 let estimated_bytes = estimate_compaction_bytes(&picker_output);
512 if let Some(limit_bytes) = self.exceeds_compaction_memory_limit(estimated_bytes) {
513 COMPACTION_MEMORY_REJECTED
514 .with_label_values(&["oversized"])
515 .inc();
516 warn!(
517 "Skip compaction for region {} because estimated memory {} bytes exceeds compaction memory limit {} bytes",
518 region_id, estimated_bytes, limit_bytes,
519 );
520 for waiter in waiters {
521 waiter.send(Ok(0));
522 }
523 return Ok(None);
524 }
525
526 let state = CancellableTaskState::new();
527 let cancel_handle = state.cancel_handle();
528 let execution = CompactionExecution::new(plan_id, files);
529 let uncommitted = UncommittedSsts::new(
530 region_id,
531 compaction_region.access_layer.clone(),
532 Some(compaction_region.cache_manager.clone()),
533 );
534 let local_compaction_task = Box::new(CompactionTaskImpl {
535 state: state.clone(),
536 execution: execution.clone(),
537 request_sender: self.request_sender.clone(),
538 waiters,
539 start_time,
540 listener: self.listener.clone(),
541 picker_output,
542 compaction_region,
543 compactor: Arc::new(DefaultCompactor::with_cancel_handle(
544 cancel_handle.clone(),
545 uncommitted.clone(),
546 )),
547 memory_manager: self.memory_manager.clone(),
548 memory_policy: self.memory_policy,
549 estimated_memory_bytes: estimated_bytes,
550 uncommitted,
551 });
552
553 match self.submit_compaction_task(local_compaction_task, region_id) {
554 Ok(()) => Ok(Some(CompactionPhase::Local { state, execution })),
555 Err((err, task)) => {
556 if let (Some(status), Some(mut task)) =
557 (self.region_status.get_mut(®ion_id), task)
558 {
559 status.append_waiters(&mut task.waiters);
560 }
561 Err(err)
562 }
563 }
564 }
565
566 fn submit_compaction_task(
567 &mut self,
568 task: Box<CompactionTaskImpl>,
569 region_id: RegionId,
570 ) -> std::result::Result<(), (Error, Option<Box<CompactionTaskImpl>>)> {
571 let task = Arc::new(Mutex::new(Some(task)));
572 let task_to_run = task.clone();
573 match self.scheduler.schedule(Box::pin(async move {
574 let task = task_to_run
575 .lock()
576 .unwrap_or_else(|poisoned| poisoned.into_inner())
577 .take();
578 if let Some(mut task) = task {
579 INFLIGHT_COMPACTION_COUNT.inc();
580 task.run().await;
581 INFLIGHT_COMPACTION_COUNT.dec();
582 } else {
583 error!("Compaction task was missing when the scheduled job started");
584 }
585 })) {
586 Ok(()) => Ok(()),
587 Err(err) => {
588 error!(err; "Failed to submit compaction request for region {}", region_id);
589 let task = task
590 .lock()
591 .unwrap_or_else(|poisoned| poisoned.into_inner())
592 .take();
593 Err((err, task))
594 }
595 }
596 }
597
598 fn exceeds_compaction_memory_limit(&self, estimated_bytes: u64) -> Option<u64> {
599 let limit_bytes = self.memory_manager.limit_bytes();
600 if limit_bytes > 0 && estimated_bytes > limit_bytes {
601 Some(limit_bytes)
602 } else {
603 None
604 }
605 }
606}
607
608fn estimate_compaction_bytes(picker_output: &PickerOutput) -> u64 {
611 picker_output
612 .outputs
613 .iter()
614 .flat_map(|output| output.inputs.iter())
615 .map(|file: &FileHandle| {
616 let meta = file.meta_ref();
617 meta.max_row_group_uncompressed_size
618 })
619 .sum()
620}
621
622fn refresh_picker_output(output: PickerOutput, current: &SstVersion) -> Option<PickerOutput> {
632 let refresh = |file: FileHandle| {
633 current
634 .file_for_compaction(&file)
635 .filter(|current| !current.is_deleted() && !current.compacting())
636 .cloned()
637 };
638 let outputs = output
639 .outputs
640 .into_iter()
641 .map(|output| {
642 let inputs = output
643 .inputs
644 .into_iter()
645 .map(&refresh)
646 .collect::<Option<Vec<_>>>()?;
647 Some(CompactionOutput { inputs, ..output })
648 })
649 .collect::<Option<Vec<_>>>()?;
650 let expired_ssts = output
651 .expired_ssts
652 .into_iter()
653 .map(refresh)
654 .collect::<Option<Vec<_>>>()?;
655
656 Some(PickerOutput {
657 outputs,
658 expired_ssts,
659 time_window_size: output.time_window_size,
660 max_file_size: output.max_file_size,
661 })
662}