1mod planning;
16mod state;
17
18use std::collections::HashMap;
19use std::sync::Arc;
20
21use api::v1::region::compact_request;
22use common_base::Plugins;
23use common_memory_manager::OnExhaustedPolicy;
24use common_meta::key::SchemaMetadataManagerRef;
25use common_telemetry::{debug, error, info};
26use common_time::range::TimestampRange;
27pub(crate) use planning::CompactionPickFinished;
28pub use planning::CompactionRequest;
29pub(crate) use state::CompactionExecution;
30use state::{CompactionStatus, CompactionTrigger, PendingCompaction};
31use store_api::storage::RegionId;
32use tokio::sync::mpsc::Sender;
33
34use crate::access_layer::AccessLayerRef;
35use crate::cache::CacheManagerRef;
36use crate::compaction::memory_manager::CompactionMemoryManager;
37use crate::compaction::task::MAX_PARALLEL_COMPACTION;
38use crate::config::MitoConfig;
39use crate::error::{
40 CompactionCancelledSnafu, Error, ManualCompactionAlreadyRunningSnafu, RegionClosedSnafu,
41 RegionDroppedSnafu, RegionTruncatedSnafu, Result,
42};
43use crate::region::version::VersionControlRef;
44use crate::region::{ManifestContextRef, RegionLeaderState, RegionRoleState};
45use crate::request::{DdlRequest, OptionOutputTx, SenderDdlRequest, WorkerRequestWithTime};
46#[cfg(test)]
47use crate::schedule::RequestCancelResult;
48use crate::schedule::scheduler::SchedulerRef;
49use crate::worker::WorkerListener;
50
51pub(crate) struct CompactionScheduler {
53 scheduler: SchedulerRef,
54 region_status: HashMap<RegionId, CompactionStatus>,
56 request_sender: Sender<WorkerRequestWithTime>,
58 cache_manager: CacheManagerRef,
59 engine_config: Arc<MitoConfig>,
60 memory_manager: Arc<CompactionMemoryManager>,
61 memory_policy: OnExhaustedPolicy,
62 listener: WorkerListener,
63 plugins: Plugins,
65 next_plan_id: u64,
68}
69
70#[derive(Debug)]
72pub(crate) enum CompactionTransition {
73 NoAction,
75 AutomaticFollowupScheduled,
77 DdlReady(Vec<SenderDdlRequest>),
79}
80
81impl CompactionTransition {
82 fn from_pending_ddls(pending_ddls: Vec<SenderDdlRequest>) -> Self {
83 if pending_ddls.is_empty() {
84 Self::NoAction
85 } else {
86 Self::DdlReady(pending_ddls)
87 }
88 }
89
90 #[cfg(test)]
91 fn is_empty(&self) -> bool {
92 !matches!(self, Self::DdlReady(pending_ddls) if !pending_ddls.is_empty())
93 }
94
95 #[cfg(test)]
96 fn len(&self) -> usize {
97 match self {
98 Self::DdlReady(pending_ddls) => pending_ddls.len(),
99 Self::NoAction | Self::AutomaticFollowupScheduled => 0,
100 }
101 }
102}
103
104impl CompactionScheduler {
106 #[allow(clippy::too_many_arguments)]
118 pub(crate) fn new(
119 scheduler: SchedulerRef,
120 request_sender: Sender<WorkerRequestWithTime>,
121 cache_manager: CacheManagerRef,
122 engine_config: Arc<MitoConfig>,
123 listener: WorkerListener,
124 plugins: Plugins,
125 memory_manager: Arc<CompactionMemoryManager>,
126 memory_policy: OnExhaustedPolicy,
127 ) -> Self {
128 Self {
129 scheduler,
130 region_status: HashMap::new(),
131 request_sender,
132 cache_manager,
133 engine_config,
134 memory_manager,
135 memory_policy,
136 listener,
137 plugins,
138 next_plan_id: 0,
139 }
140 }
141
142 pub(crate) fn schedule_automatic_compaction(
157 &mut self,
158 compact_options: compact_request::Options,
159 version_control: &VersionControlRef,
160 access_layer: &AccessLayerRef,
161 manifest_ctx: &ManifestContextRef,
162 schema_metadata_manager: SchemaMetadataManagerRef,
163 ) -> Result<bool> {
164 self.schedule_compaction(
165 CompactionTrigger::Automatic,
166 compact_options,
167 version_control,
168 access_layer,
169 OptionOutputTx::none(),
170 manifest_ctx,
171 schema_metadata_manager,
172 1, None,
174 )
175 }
176
177 #[allow(clippy::too_many_arguments)]
195 pub(crate) fn schedule_manual_compaction(
196 &mut self,
197 compact_options: compact_request::Options,
198 version_control: &VersionControlRef,
199 access_layer: &AccessLayerRef,
200 waiter: OptionOutputTx,
201 manifest_ctx: &ManifestContextRef,
202 schema_metadata_manager: SchemaMetadataManagerRef,
203 max_parallelism: usize,
204 time_range: Option<TimestampRange>,
205 ) -> Result<bool> {
206 self.schedule_compaction(
207 CompactionTrigger::Manual,
208 compact_options,
209 version_control,
210 access_layer,
211 waiter,
212 manifest_ctx,
213 schema_metadata_manager,
214 max_parallelism,
215 time_range,
216 )
217 }
218
219 pub(crate) async fn handle_compaction_pick_finished(
234 &mut self,
235 finished: CompactionPickFinished,
236 manifest_ctx: &ManifestContextRef,
237 schema_metadata_manager: SchemaMetadataManagerRef,
238 ) -> CompactionTransition {
239 self.handle_compaction_pick_finished_inner(finished, manifest_ctx, schema_metadata_manager)
240 .await
241 }
242
243 pub(crate) fn is_current_execution(
254 &self,
255 region_id: RegionId,
256 execution: &CompactionExecution,
257 ) -> bool {
258 self.region_status
259 .get(®ion_id)
260 .is_some_and(|status| status.matches_execution(execution))
261 }
262
263 pub(crate) async fn on_execution_finished(
281 &mut self,
282 region_id: RegionId,
283 execution: &CompactionExecution,
284 manifest_ctx: &ManifestContextRef,
285 schema_metadata_manager: SchemaMetadataManagerRef,
286 made_progress: bool,
287 ) -> CompactionTransition {
288 if !self.is_current_execution(region_id, execution) {
289 return CompactionTransition::NoAction;
290 }
291 self.on_compaction_finished(
292 region_id,
293 manifest_ctx,
294 schema_metadata_manager,
295 made_progress,
296 )
297 .await
298 }
299
300 pub(crate) async fn on_execution_cancelled(
312 &mut self,
313 region_id: RegionId,
314 execution: &CompactionExecution,
315 ) -> Vec<SenderDdlRequest> {
316 if !self.is_current_execution(region_id, execution) {
317 return Vec::new();
318 }
319 self.on_compaction_cancelled(region_id).await
320 }
321
322 pub(crate) fn on_execution_failed(
334 &mut self,
335 region_id: RegionId,
336 execution: &CompactionExecution,
337 err: Arc<Error>,
338 ) {
339 if !self.is_current_execution(region_id, execution) {
340 return;
341 }
342 self.on_compaction_failed(region_id, err);
343 }
344
345 pub(crate) fn on_region_dropped(&mut self, region_id: RegionId) {
355 self.remove_region_on_failure(
356 region_id,
357 Arc::new(RegionDroppedSnafu { region_id }.build()),
358 );
359 }
360
361 pub(crate) fn on_region_closed(&mut self, region_id: RegionId) {
371 self.remove_region_on_failure(region_id, Arc::new(RegionClosedSnafu { region_id }.build()));
372 }
373
374 pub(crate) fn on_region_truncated(&mut self, region_id: RegionId) {
384 self.remove_region_on_failure(
385 region_id,
386 Arc::new(RegionTruncatedSnafu { region_id }.build()),
387 );
388 }
389
390 pub(crate) fn try_cancel_and_add_ddl<T>(
406 &mut self,
407 region_id: RegionId,
408 sender: OptionOutputTx,
409 request: T,
410 into_ddl_request: impl FnOnce(T) -> DdlRequest,
411 ) -> std::result::Result<(), (OptionOutputTx, T)> {
412 let Some(status) = self.region_status.get_mut(®ion_id) else {
413 return Err((sender, request));
414 };
415 status.request_cancel();
416
417 let request = SenderDdlRequest {
418 region_id,
419 sender,
420 request: into_ddl_request(request),
421 };
422 debug!(
423 "Added pending DDL request for region: {}, ddl: {:?}",
424 request.region_id, request.request
425 );
426 status.pending_ddl_requests.push(request);
427 Ok(())
428 }
429}
430
431impl CompactionScheduler {
433 fn next_plan_id(counter: &mut u64) -> u64 {
438 let plan_id = *counter;
439 *counter = counter.wrapping_add(1);
440 plan_id
441 }
442
443 #[allow(clippy::too_many_arguments)]
444 fn schedule_compaction(
445 &mut self,
446 trigger: CompactionTrigger,
447 compact_options: compact_request::Options,
448 version_control: &VersionControlRef,
449 access_layer: &AccessLayerRef,
450 waiter: OptionOutputTx,
451 manifest_ctx: &ManifestContextRef,
452 schema_metadata_manager: SchemaMetadataManagerRef,
453 max_parallelism: usize,
454 time_range: Option<TimestampRange>,
455 ) -> Result<bool> {
456 let region_id = version_control.region_id();
457 let current_state = manifest_ctx.current_state();
458 if current_state == RegionRoleState::Leader(RegionLeaderState::Staging) {
459 info!(
460 "Skipping compaction for region {} in staging mode, options: {:?}",
461 region_id, compact_options
462 );
463 waiter.send(Ok(0));
464 return Ok(false);
465 }
466
467 if let Some(status) = self.region_status.get_mut(®ion_id) {
468 if !status.pending_ddl_requests.is_empty() {
471 waiter.send(CompactionCancelledSnafu.fail());
472 info!(
473 "Region {} has pending DDL requests, ignoring compaction: {:?}",
474 region_id, compact_options
475 );
476 return Ok(false);
477 }
478
479 match trigger {
480 CompactionTrigger::Automatic => status.mark_automatic_trigger(),
481 CompactionTrigger::Manual if status.is_manual_compaction() => {
482 waiter.send(ManualCompactionAlreadyRunningSnafu { region_id }.fail());
483 info!(
484 "Region {} already has a manually triggered compaction running",
485 region_id
486 );
487 }
488 CompactionTrigger::Manual => {
489 status.set_pending_request(PendingCompaction {
490 options: compact_options,
491 waiter,
492 max_parallelism,
493 time_range,
494 });
495 info!(
496 "Region {} is running an automatic compaction; manual compaction will be re-scheduled",
497 region_id
498 );
499 }
500 }
501 return Ok(false);
502 }
503
504 let plan_id = Self::next_plan_id(&mut self.next_plan_id);
506 let mut status = CompactionStatus::new(
507 region_id,
508 version_control.clone(),
509 access_layer.clone(),
510 plan_id,
511 trigger,
512 );
513 let request = status.new_compaction_request(
514 self.request_sender.clone(),
515 self.engine_config.clone(),
516 self.cache_manager.clone(),
517 manifest_ctx,
518 self.listener.clone(),
519 schema_metadata_manager,
520 max_parallelism,
521 );
522 status.merge_waiter(waiter);
523 self.region_status.insert(region_id, status);
524 self.dispatch_compaction_planning(plan_id, request, compact_options, time_range);
525 self.listener.on_compaction_scheduled(region_id);
526 Ok(true)
527 }
528
529 fn handle_pending_compaction_request(
533 &mut self,
534 region_id: RegionId,
535 manifest_ctx: &ManifestContextRef,
536 schema_metadata_manager: SchemaMetadataManagerRef,
537 ) -> bool {
538 let Some(status) = self.region_status.get_mut(®ion_id) else {
539 return true;
540 };
541
542 let Some(pending_request) = std::mem::take(&mut status.pending_request) else {
545 return false;
546 };
547
548 let PendingCompaction {
549 options,
550 waiter,
551 max_parallelism,
552 time_range,
553 } = pending_request;
554
555 let request = status.new_compaction_request(
556 self.request_sender.clone(),
557 self.engine_config.clone(),
558 self.cache_manager.clone(),
559 manifest_ctx,
560 self.listener.clone(),
561 schema_metadata_manager,
562 max_parallelism,
563 );
564 status.merge_waiter(waiter);
565 let plan_id = Self::next_plan_id(&mut self.next_plan_id);
569 status.start_picking_with_trigger(plan_id, CompactionTrigger::Manual);
570 self.dispatch_compaction_planning(plan_id, request, options, time_range);
571 debug!(
572 "Successfully scheduled manual compaction planning for region id: {}",
573 region_id
574 );
575 true
576 }
577
578 async fn on_compaction_finished(
580 &mut self,
581 region_id: RegionId,
582 manifest_ctx: &ManifestContextRef,
583 schema_metadata_manager: SchemaMetadataManagerRef,
584 made_progress: bool,
585 ) -> CompactionTransition {
586 if !self.region_status.contains_key(®ion_id) {
587 return CompactionTransition::NoAction;
588 }
589
590 if self.handle_pending_compaction_request(
591 region_id,
592 manifest_ctx,
593 schema_metadata_manager.clone(),
594 ) {
595 return CompactionTransition::NoAction;
596 }
597
598 let Some(status) = self.region_status.get_mut(®ion_id) else {
601 return CompactionTransition::NoAction;
602 };
603 for waiter in status.take_waiters() {
604 waiter.send(Ok(0));
605 }
606
607 let pending_ddl_requests = std::mem::take(&mut status.pending_ddl_requests);
611 if !pending_ddl_requests.is_empty() {
612 self.region_status.remove(®ion_id);
614 return CompactionTransition::DdlReady(pending_ddl_requests);
617 }
618
619 let should_continue = status.active.reset_automatic_followup() || made_progress;
623 if should_continue
624 && self.schedule_automatic_followup(region_id, manifest_ctx, schema_metadata_manager)
625 {
626 return CompactionTransition::AutomaticFollowupScheduled;
627 }
628 self.region_status.remove(®ion_id);
629 CompactionTransition::NoAction
630 }
631
632 fn schedule_automatic_followup(
633 &mut self,
634 region_id: RegionId,
635 manifest_ctx: &ManifestContextRef,
636 schema_metadata_manager: SchemaMetadataManagerRef,
637 ) -> bool {
638 let Some(status) = self.region_status.get_mut(®ion_id) else {
639 return false;
640 };
641 let request = status.new_compaction_request(
643 self.request_sender.clone(),
644 self.engine_config.clone(),
645 self.cache_manager.clone(),
646 manifest_ctx,
647 self.listener.clone(),
648 schema_metadata_manager,
649 MAX_PARALLEL_COMPACTION,
650 );
651 let plan_id = Self::next_plan_id(&mut self.next_plan_id);
655 status.start_regular_picking(plan_id);
656 self.dispatch_compaction_planning(
657 plan_id,
658 request,
659 compact_request::Options::Regular(Default::default()),
660 None,
661 );
662 debug!(
663 "Successfully scheduled next compaction planning for region id: {}",
664 region_id
665 );
666 true
667 }
668
669 async fn on_compaction_cancelled(&mut self, region_id: RegionId) -> Vec<SenderDdlRequest> {
671 self.remove_region_on_cancel(region_id)
672 }
673
674 fn on_compaction_failed(&mut self, region_id: RegionId, err: Arc<Error>) {
676 error!(err; "Region {} failed to compact, cancel all pending tasks", region_id);
677 self.remove_region_on_failure(region_id, err);
678 }
679
680 #[cfg(test)]
681 fn add_ddl_request_to_pending(&mut self, request: SenderDdlRequest) {
682 self.region_status
683 .get_mut(&request.region_id)
684 .unwrap()
685 .pending_ddl_requests
686 .push(request);
687 }
688
689 #[cfg(test)]
690 fn has_pending_ddls(&self, region_id: RegionId) -> bool {
691 let has_pending = self
692 .region_status
693 .get(®ion_id)
694 .map(|status| !status.pending_ddl_requests.is_empty())
695 .unwrap_or(false);
696 debug!(
697 "Checked pending DDL requests for region: {}, has_pending: {}",
698 region_id, has_pending
699 );
700 has_pending
701 }
702
703 #[cfg(test)]
704 fn request_cancel(&mut self, region_id: RegionId) -> RequestCancelResult {
705 self.region_status
706 .get_mut(®ion_id)
707 .unwrap()
708 .request_cancel()
709 }
710
711 fn remove_region_on_failure(&mut self, region_id: RegionId, err: Arc<Error>) {
712 let Some(status) = self.region_status.remove(®ion_id) else {
714 return;
715 };
716
717 status.on_failure(err);
719 }
720
721 fn remove_region_on_cancel(&mut self, region_id: RegionId) -> Vec<SenderDdlRequest> {
722 let Some(status) = self.region_status.remove(®ion_id) else {
723 return Vec::new();
724 };
725
726 status.on_cancel()
727 }
728}
729
730impl Drop for CompactionScheduler {
731 fn drop(&mut self) {
732 for (region_id, status) in self.region_status.drain() {
733 status.on_failure(Arc::new(RegionClosedSnafu { region_id }.build()));
735 }
736 }
737}
738
739#[cfg(test)]
740#[path = "scheduler_test.rs"]
741mod tests;