1use std::fmt;
16use std::future::Future;
17use std::sync::Arc;
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};
34use crate::compaction::picker::{PickerOutput, new_picker};
35use crate::compaction::scheduler::state::{CompactingFiles, CompactionExecution, CompactionPhase};
36use crate::compaction::scheduler::{CompactionScheduler, CompactionTransition};
37use crate::compaction::unit::CompactionUnit;
38use crate::compaction::{CompactionOutput, find_dynamic_options, format_panic_reason};
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::remote_job_scheduler::{
48 CompactionJob, DefaultNotifier, RemoteJob, RemoteJobSchedulerRef,
49};
50use crate::sst::file::FileHandle;
51use crate::sst::version::SstVersion;
52use crate::worker::WorkerListener;
53
54pub struct CompactionRequest {
56 pub(crate) engine_config: Arc<MitoConfig>,
57 pub(crate) current_version: CompactionVersion,
58 pub(crate) access_layer: AccessLayerRef,
59 pub(crate) request_sender: mpsc::Sender<WorkerRequestWithTime>,
61 pub(crate) start_time: Instant,
63 pub(crate) cache_manager: CacheManagerRef,
64 pub(crate) manifest_ctx: ManifestContextRef,
65 pub(crate) listener: WorkerListener,
66 pub(crate) schema_metadata_manager: SchemaMetadataManagerRef,
67 pub(crate) max_parallelism: usize,
68}
69
70impl CompactionRequest {
71 pub(crate) fn region_id(&self) -> RegionId {
72 self.current_version.metadata.region_id
73 }
74}
75
76pub(crate) enum CompactionPlanningResult {
78 Prepared(PreparedCompaction),
79 NoPlan,
80 Error(Arc<Error>),
81}
82
83impl fmt::Debug for CompactionPlanningResult {
84 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85 match self {
86 Self::Prepared(prepared) => f
87 .debug_tuple("Prepared")
88 .field(&prepared.compaction_region.region_id)
89 .finish(),
90 Self::NoPlan => f.write_str("NoPlan"),
91 Self::Error(err) => f.debug_tuple("Error").field(err).finish(),
92 }
93 }
94}
95
96#[derive(Debug)]
98pub(crate) struct CompactionPickFinished {
99 pub(crate) region_id: RegionId,
100 pub(crate) plan_id: u64,
101 pub(crate) result: CompactionPlanningResult,
102}
103
104pub(crate) struct PreparedCompaction {
105 pub(super) compaction_region: CompactionRegion,
106 pub(super) picker_output: PickerOutput,
107 start_time: Instant,
108 ttl: TimeToLive,
109}
110
111impl CompactionScheduler {
112 pub(super) fn dispatch_compaction_planning(
113 &self,
114 plan_id: u64,
115 request: CompactionRequest,
116 options: compact_request::Options,
117 time_range: Option<TimestampRange>,
118 ) {
119 let plugins = self.plugins.clone();
120 let max_background_compactions = self.engine_config.max_background_compactions;
121 common_runtime::spawn_compact(async move {
122 let region_id = request.region_id();
123 let request_sender = request.request_sender.clone();
124 let planning = Self::prepare_compaction(
125 request,
126 options,
127 plugins,
128 max_background_compactions,
129 time_range,
130 );
131 Self::notify_planning_result(region_id, plan_id, request_sender, planning).await;
132 });
133 }
134
135 pub(super) async fn notify_planning_result(
144 region_id: RegionId,
145 plan_id: u64,
146 request_sender: Sender<WorkerRequestWithTime>,
147 planning: impl Future<Output = CompactionPlanningResult> + Send,
148 ) {
149 let result = std::panic::AssertUnwindSafe(planning).catch_unwind().await.unwrap_or_else(|payload| {
151 let reason = format_panic_reason(payload.as_ref());
152 CompactionPlanningResult::Error(Arc::new(
153 UnexpectedSnafu {
154 reason: format!(
155 "Compaction planning panicked for region {region_id}, plan_id {plan_id}: {reason}"
156 ),
157 }
158 .build(),
159 ))
160 });
161 if let CompactionPlanningResult::Error(err) = &result {
162 error!(err; "Compaction planning failed for region {}, plan_id: {}", region_id, plan_id);
163 }
164 let request = WorkerRequestWithTime::new(WorkerRequest::Background {
165 region_id,
166 notify: BackgroundNotify::CompactionPickFinished(CompactionPickFinished {
167 region_id,
168 plan_id,
169 result,
170 }),
171 });
172 if request_sender.send(request).await.is_err() {
173 warn!("Failed to send compaction planning result for region {region_id}");
174 }
175 }
176
177 async fn prepare_compaction(
178 request: CompactionRequest,
179 options: compact_request::Options,
180 plugins: Plugins,
181 max_background_compactions: usize,
182 time_range: Option<TimestampRange>,
183 ) -> CompactionPlanningResult {
184 let region_id = request.region_id();
185 let (dynamic_compaction_opts, ttl) = find_dynamic_options(
186 region_id,
187 &request.current_version.options,
188 &request.schema_metadata_manager,
189 )
190 .await
191 .unwrap_or_else(|e| {
192 warn!(e; "Failed to find dynamic options for region: {}", region_id);
193 (
194 request.current_version.options.compaction.clone(),
195 request.current_version.options.ttl.unwrap_or_default(),
196 )
197 });
198
199 let max_picker_outputs = max_background_compactions.min(request.max_parallelism.max(1));
202 let region_options = RegionOptions {
203 compaction: dynamic_compaction_opts,
204 ..request.current_version.options.clone()
205 };
206 let picker = new_picker(
207 &options,
208 ®ion_options,
209 Some(max_picker_outputs),
210 time_range,
211 );
212 let region_id = request.region_id();
213 let CompactionRequest {
214 engine_config,
215 current_version,
216 access_layer,
217 request_sender: _,
218 start_time,
219 cache_manager,
220 manifest_ctx,
221 listener,
222 schema_metadata_manager: _,
223 max_parallelism,
224 } = request;
225
226 debug!(
227 "Pick compaction strategy {:?} for region: {}, ttl: {:?}",
228 picker, region_id, ttl
229 );
230
231 let compaction_region = CompactionRegion {
232 region_id,
233 current_version: current_version.clone(),
234 region_options,
235 engine_config: engine_config.clone(),
236 region_metadata: current_version.metadata.clone(),
237 cache_manager: cache_manager.clone(),
238 access_layer: access_layer.clone(),
239 manifest_ctx: manifest_ctx.clone(),
240 file_purger: None,
241 ttl: Some(ttl),
242 max_parallelism,
243 plugins,
244 };
245
246 listener.on_compaction_pick_begin(region_id).await;
247 let _pick_timer = COMPACTION_STAGE_ELAPSED
248 .with_label_values(&["pick"])
249 .start_timer();
250 let picker_output = match picker.pick(&compaction_region).await {
251 Ok(output) => output,
252 Err(err) => return CompactionPlanningResult::Error(Arc::new(err)),
253 };
254
255 let Some(picker_output) = picker_output else {
256 return CompactionPlanningResult::NoPlan;
257 };
258
259 CompactionPlanningResult::Prepared(PreparedCompaction {
260 compaction_region,
261 picker_output,
262 start_time,
263 ttl,
264 })
265 }
266
267 pub(super) async fn handle_compaction_pick_finished_inner(
276 &mut self,
277 finished: CompactionPickFinished,
278 manifest_ctx: &ManifestContextRef,
279 schema_metadata_manager: SchemaMetadataManagerRef,
280 ) -> CompactionTransition {
281 let region_id = finished.region_id;
282 let plan_id = finished.plan_id;
283 let Some(status) = self.region_status.get(®ion_id) else {
284 return CompactionTransition::NoAction;
285 };
286
287 if !status.is_picking(finished.plan_id) {
288 return CompactionTransition::NoAction;
289 }
290 if !status.accept_plan(finished.plan_id) {
293 return CompactionTransition::from_pending_ddls(
294 self.finish_compaction_on_cancel(region_id),
295 );
296 }
297
298 match finished.result {
299 CompactionPlanningResult::Prepared(mut prepared) => {
300 let current = status.version_control.current().version;
301 let Some(picker_output) =
302 refresh_picker_output(prepared.picker_output, ¤t.ssts)
303 else {
304 return self
305 .finish_compaction_planning(
306 region_id,
307 None,
308 manifest_ctx,
309 schema_metadata_manager,
310 )
311 .await;
312 };
313 let Some(files) = CompactingFiles::try_new(&picker_output) else {
314 return self
315 .finish_compaction_planning(
316 region_id,
317 None,
318 manifest_ctx,
319 schema_metadata_manager,
320 )
321 .await;
322 };
323 prepared.picker_output = picker_output;
324 let Some(status) = self.region_status.get_mut(®ion_id) else {
325 return CompactionTransition::NoAction;
326 };
327 let waiters = status.take_waiters();
328 match self
329 .prepare_or_submit_compaction(prepared, files, waiters, plan_id)
330 .await
331 {
332 Ok(Some(phase)) => {
333 if let Some(status) = self.region_status.get_mut(®ion_id) {
334 status.set_phase(phase);
335 }
336 self.dispatch_local_units(region_id);
337 self.finish_local_units(region_id, manifest_ctx, schema_metadata_manager)
340 .await
341 }
342 Ok(None) => {
343 self.finish_compaction_planning(
344 region_id,
345 None,
346 manifest_ctx,
347 schema_metadata_manager,
348 )
349 .await
350 }
351 Err(err) => {
352 self.fail_compaction(region_id, Arc::new(err));
353 CompactionTransition::NoAction
354 }
355 }
356 }
357 CompactionPlanningResult::NoPlan => {
360 self.finish_compaction_planning(
361 region_id,
362 None,
363 manifest_ctx,
364 schema_metadata_manager,
365 )
366 .await
367 }
368 CompactionPlanningResult::Error(err) => {
369 self.finish_compaction_planning(
370 region_id,
371 Some(err),
372 manifest_ctx,
373 schema_metadata_manager,
374 )
375 .await
376 }
377 }
378 }
379
380 async fn finish_compaction_planning(
381 &mut self,
382 region_id: RegionId,
383 err: Option<Arc<Error>>,
384 manifest_ctx: &ManifestContextRef,
385 schema_metadata_manager: SchemaMetadataManagerRef,
386 ) -> CompactionTransition {
387 let Some(status) = self.region_status.get_mut(®ion_id) else {
388 return CompactionTransition::NoAction;
389 };
390 for waiter in status.take_waiters() {
391 if let Some(err) = &err {
392 waiter.send(Err(err.clone()).context(CompactRegionSnafu { region_id }));
393 } else {
394 waiter.send(Ok(0));
395 }
396 }
397
398 if let Some(ddls) = self.take_ready_ddls(region_id) {
399 return CompactionTransition::DdlReady(ddls);
400 }
401 if self.handle_pending_compaction_request(
402 region_id,
403 manifest_ctx,
404 schema_metadata_manager.clone(),
405 ) {
406 return CompactionTransition::NoAction;
407 }
408
409 let Some(status) = self.region_status.get_mut(®ion_id) else {
410 return CompactionTransition::NoAction;
411 };
412
413 if status.active.reset_automatic_followup()
414 && self.schedule_automatic_followup(region_id, manifest_ctx, schema_metadata_manager)
415 {
416 return CompactionTransition::AutomaticFollowupScheduled;
417 }
418
419 if let Some(status) = self.region_status.get_mut(®ion_id) {
420 status.become_idle();
421 }
422 CompactionTransition::NoAction
423 }
424
425 async fn prepare_or_submit_compaction(
428 &mut self,
429 prepared: PreparedCompaction,
430 files: CompactingFiles,
431 waiters: Vec<OutputTx>,
432 plan_id: u64,
433 ) -> Result<Option<CompactionPhase>> {
434 let PreparedCompaction {
435 compaction_region,
436 picker_output,
437 start_time,
438 ttl,
439 } = prepared;
440 let region_id = compaction_region.region_id;
441 let dynamic_compaction_opts = &compaction_region.region_options.compaction;
442
443 let waiters = if dynamic_compaction_opts.remote_compaction() {
446 if let Some(remote_job_scheduler) = &self.plugins.get::<RemoteJobSchedulerRef>() {
447 let execution = CompactionExecution::new(plan_id, files.clone());
448 let remote_compaction_job = CompactionJob {
449 compaction_region: compaction_region.clone(),
450 picker_output: picker_output.clone(),
451 start_time,
452 waiters,
453 ttl,
454 };
455
456 let result = remote_job_scheduler
457 .schedule(
458 RemoteJob::CompactionJob(remote_compaction_job),
459 Box::new(DefaultNotifier::new(
460 self.request_sender.clone(),
461 execution.clone(),
462 )),
463 )
464 .await;
465
466 match result {
467 Ok(job_id) => {
468 info!(
469 "Scheduled remote compaction job {} for region {}",
470 job_id, region_id
471 );
472 INFLIGHT_COMPACTION_COUNT.inc();
473 return Ok(Some(CompactionPhase::Remote { execution }));
474 }
475 Err(e) => {
476 if !dynamic_compaction_opts.fallback_to_local() {
477 error!(e; "Failed to schedule remote compaction job for region {}", region_id);
478 if let Some(status) = self.region_status.get_mut(®ion_id) {
479 status.extend_waiters(e.waiters);
480 }
481 return RemoteCompactionSnafu {
482 region_id,
483 job_id: None,
484 reason: e.reason,
485 }
486 .fail();
487 }
488
489 error!(e; "Failed to schedule remote compaction job for region {}, fallback to local compaction", region_id);
490 e.waiters
493 }
494 }
495 } else {
496 debug!(
497 "Remote compaction is not enabled, fallback to local compaction for region {}",
498 region_id
499 );
500 waiters
501 }
502 } else {
503 waiters
504 };
505
506 let units = CompactionUnit::from_picker(picker_output);
507 if let Some(status) = self.region_status.get_mut(®ion_id) {
508 status.extend_waiters(waiters);
509 }
510 let Some(mut units) = units else {
511 return UnexpectedSnafu {
512 reason: "Compaction plan expires a merge input",
513 }
514 .fail();
515 };
516 units.retain(|unit| {
517 let estimated_bytes = unit.estimated_memory_bytes();
518 let Some(limit_bytes) = self.exceeds_compaction_memory_limit(estimated_bytes) else { return true };
519 COMPACTION_MEMORY_REJECTED
520 .with_label_values(&["oversized"])
521 .inc();
522 warn!(
523 "Skip compaction for region {} because estimated memory {} bytes exceeds compaction memory limit {} bytes",
524 region_id, estimated_bytes, limit_bytes,
525 );
526 false
527 });
528 if units.is_empty() {
529 return Ok(None);
530 }
531 Ok(Some(self.prepare_local_units(
532 compaction_region,
533 units,
534 files,
535 )))
536 }
537
538 fn exceeds_compaction_memory_limit(&self, estimated_bytes: u64) -> Option<u64> {
539 let limit_bytes = self.memory_manager.limit_bytes();
540 if limit_bytes > 0 && estimated_bytes > limit_bytes {
541 Some(limit_bytes)
542 } else {
543 None
544 }
545 }
546}
547
548fn refresh_picker_output(output: PickerOutput, current: &SstVersion) -> Option<PickerOutput> {
558 let refresh = |file: FileHandle| {
559 current
560 .file_for_compaction(&file)
561 .filter(|current| !current.is_deleted() && !current.compacting())
562 .cloned()
563 };
564 let outputs = output
565 .outputs
566 .into_iter()
567 .map(|output| {
568 let inputs = output
569 .inputs
570 .into_iter()
571 .map(&refresh)
572 .collect::<Option<Vec<_>>>()?;
573 Some(CompactionOutput { inputs, ..output })
574 })
575 .collect::<Option<Vec<_>>>()?;
576 let expired_ssts = output
577 .expired_ssts
578 .into_iter()
579 .map(refresh)
580 .collect::<Option<Vec<_>>>()?;
581
582 Some(PickerOutput {
583 outputs,
584 expired_ssts,
585 time_window_size: output.time_window_size,
586 max_file_size: output.max_file_size,
587 })
588}