1mod coordinator;
16pub(crate) mod history;
17mod local;
18mod planning;
19mod state;
20
21use std::collections::HashMap;
22use std::sync::{Arc, LazyLock};
23
24use api::v1::region::compact_request;
25use common_base::Plugins;
26use common_memory_manager::OnExhaustedPolicy;
27use common_meta::key::SchemaMetadataManagerRef;
28#[cfg(test)]
29use common_recordbatch::filter::MetadataFilter;
30use common_telemetry::{debug, error, info, warn};
31use common_time::range::TimestampRange;
32pub(crate) use coordinator::{CompactionCoordinator, ReadyUnit};
33#[cfg(test)]
34use datafusion_common::ScalarValue;
35pub(crate) use planning::CompactionPickFinished;
36pub use planning::CompactionRequest;
37#[cfg(test)]
38pub(crate) use planning::pick_automatic_for_test;
39pub(crate) use state::{CompactionExecution, CompactionState, CompactionStateRef};
40use state::{CompactionStatus, CompactionTrigger, DdlExecution, PendingCompaction};
41#[cfg(test)]
42use store_api::compaction_task::{self, CompactionTaskEntry};
43use store_api::storage::RegionId;
44use tokio::sync::mpsc::Sender;
45
46use crate::access_layer::AccessLayerRef;
47use crate::cache::CacheManagerRef;
48use crate::compaction::memory_manager::CompactionMemoryManager;
49use crate::config::MitoConfig;
50use crate::error::{
51 CompactionCancelledSnafu, Error, ManualCompactionAlreadyRunningSnafu, RegionClosedSnafu,
52 RegionDroppedSnafu, RegionTruncatedSnafu, Result, UnsupportedOperationSnafu,
53};
54use crate::region::version::VersionControlRef;
55use crate::region::{ManifestContextRef, RegionLeaderState, RegionRoleState};
56use crate::request::{DdlRequest, OptionOutputTx, SenderDdlRequest, WorkerRequestWithTime};
57#[cfg(test)]
58use crate::schedule::RequestCancelResult;
59use crate::worker::WorkerListener;
60
61const DISABLE_COMPACTION_ENV: &str = "GREPTIME_DISABLE_COMPACTION";
64
65static DISABLE_COMPACTION: LazyLock<bool> = LazyLock::new(|| {
68 let value = std::env::var(DISABLE_COMPACTION_ENV).ok();
69 let disabled = parse_disable_compaction(value.as_deref());
70 if disabled {
71 warn!(
72 "SST compaction is disabled by {DISABLE_COMPACTION_ENV}; restart without it to resume"
73 );
74 }
75 disabled
76});
77
78fn parse_disable_compaction(value: Option<&str>) -> bool {
79 match value {
80 None | Some("false") => false,
81 Some("true") => true,
82 Some(_) => {
83 warn!(
84 "Invalid {DISABLE_COMPACTION_ENV}: expected true or false; compaction remains enabled"
85 );
86 false
87 }
88 }
89}
90
91pub(crate) struct CompactionScheduler {
93 disable_compaction: bool,
97 coordinator: Arc<CompactionCoordinator>,
98 region_status: HashMap<RegionId, CompactionStatus>,
100 request_sender: Sender<WorkerRequestWithTime>,
102 cache_manager: CacheManagerRef,
103 engine_config: Arc<MitoConfig>,
104 memory_manager: Arc<CompactionMemoryManager>,
105 memory_policy: OnExhaustedPolicy,
106 listener: WorkerListener,
107 plugins: Plugins,
109 next_plan_id: u64,
112}
113
114#[derive(Debug)]
116pub(crate) enum CompactionTransition {
117 NoAction,
119 AutomaticFollowupScheduled,
121 DdlReady(Vec<SenderDdlRequest>),
123}
124
125impl CompactionTransition {
126 fn from_pending_ddls(pending_ddls: Vec<SenderDdlRequest>) -> Self {
127 if pending_ddls.is_empty() {
128 Self::NoAction
129 } else {
130 Self::DdlReady(pending_ddls)
131 }
132 }
133
134 #[cfg(test)]
135 fn is_empty(&self) -> bool {
136 !matches!(self, Self::DdlReady(pending_ddls) if !pending_ddls.is_empty())
137 }
138
139 #[cfg(test)]
140 fn len(&self) -> usize {
141 match self {
142 Self::DdlReady(pending_ddls) => pending_ddls.len(),
143 Self::NoAction | Self::AutomaticFollowupScheduled => 0,
144 }
145 }
146}
147
148impl CompactionScheduler {
150 #[allow(clippy::too_many_arguments)]
162 pub(crate) fn new(
163 coordinator: Arc<CompactionCoordinator>,
164 request_sender: Sender<WorkerRequestWithTime>,
165 cache_manager: CacheManagerRef,
166 engine_config: Arc<MitoConfig>,
167 listener: WorkerListener,
168 plugins: Plugins,
169 memory_manager: Arc<CompactionMemoryManager>,
170 memory_policy: OnExhaustedPolicy,
171 ) -> Self {
172 Self {
173 disable_compaction: *DISABLE_COMPACTION,
174 coordinator,
175 region_status: HashMap::new(),
176 request_sender,
177 cache_manager,
178 engine_config,
179 memory_manager,
180 memory_policy,
181 listener,
182 plugins,
183 next_plan_id: 0,
184 }
185 }
186
187 pub(crate) fn register_region(
189 &mut self,
190 version_control: &VersionControlRef,
191 access_layer: &AccessLayerRef,
192 shared_state: &CompactionStateRef,
193 ) {
194 let region_id = version_control.region_id();
195 if let Some(status) = self.region_status.get_mut(®ion_id) {
196 if Arc::ptr_eq(&status.version_control, version_control) {
197 return;
198 }
199 status.on_failure(Arc::new(RegionClosedSnafu { region_id }.build()));
200 }
201 let mut status =
202 CompactionStatus::new(region_id, version_control.clone(), access_layer.clone());
203 status.shared_state = shared_state.clone();
204 self.region_status.insert(region_id, status);
205 }
206
207 #[cfg(test)]
209 fn is_compacting(&self, region_id: RegionId) -> bool {
210 self.region_status
211 .get(®ion_id)
212 .is_some_and(CompactionStatus::is_compacting)
213 }
214
215 pub(crate) fn schedule_automatic_compaction(
230 &mut self,
231 compact_options: compact_request::Options,
232 version_control: &VersionControlRef,
233 access_layer: &AccessLayerRef,
234 manifest_ctx: &ManifestContextRef,
235 schema_metadata_manager: SchemaMetadataManagerRef,
236 ) -> Result<bool> {
237 self.schedule_compaction(
238 CompactionTrigger::Automatic,
239 compact_options,
240 version_control,
241 access_layer,
242 OptionOutputTx::none(),
243 manifest_ctx,
244 schema_metadata_manager,
245 self.engine_config.max_background_compactions,
246 None,
247 )
248 }
249
250 #[allow(clippy::too_many_arguments)]
268 pub(crate) fn schedule_manual_compaction(
269 &mut self,
270 compact_options: compact_request::Options,
271 version_control: &VersionControlRef,
272 access_layer: &AccessLayerRef,
273 waiter: OptionOutputTx,
274 manifest_ctx: &ManifestContextRef,
275 schema_metadata_manager: SchemaMetadataManagerRef,
276 max_parallelism: usize,
277 time_range: Option<TimestampRange>,
278 ) -> Result<bool> {
279 self.schedule_compaction(
280 CompactionTrigger::Manual,
281 compact_options,
282 version_control,
283 access_layer,
284 waiter,
285 manifest_ctx,
286 schema_metadata_manager,
287 max_parallelism,
288 time_range,
289 )
290 }
291
292 pub(crate) async fn handle_compaction_pick_finished(
307 &mut self,
308 finished: CompactionPickFinished,
309 manifest_ctx: &ManifestContextRef,
310 schema_metadata_manager: SchemaMetadataManagerRef,
311 ) -> CompactionTransition {
312 self.handle_compaction_pick_finished_inner(finished, manifest_ctx, schema_metadata_manager)
313 .await
314 }
315
316 pub(crate) fn is_current_execution(
327 &self,
328 region_id: RegionId,
329 execution: &CompactionExecution,
330 ) -> bool {
331 self.region_status
332 .get(®ion_id)
333 .is_some_and(|status| status.matches_execution(execution))
334 }
335
336 pub(crate) async fn on_execution_finished(
354 &mut self,
355 region_id: RegionId,
356 execution: &CompactionExecution,
357 manifest_ctx: &ManifestContextRef,
358 schema_metadata_manager: SchemaMetadataManagerRef,
359 made_progress: bool,
360 ) -> CompactionTransition {
361 if !self.is_current_execution(region_id, execution) {
362 return CompactionTransition::NoAction;
363 }
364 self.on_compaction_finished(
365 region_id,
366 manifest_ctx,
367 schema_metadata_manager,
368 made_progress,
369 )
370 .await
371 }
372
373 pub(crate) fn on_execution_failed(
385 &mut self,
386 region_id: RegionId,
387 execution: &CompactionExecution,
388 err: Arc<Error>,
389 ) {
390 if !self.is_current_execution(region_id, execution) {
391 return;
392 }
393 self.on_compaction_failed(region_id, err);
394 }
395
396 pub(crate) fn on_region_dropped(&mut self, region_id: RegionId) {
406 self.remove_region(
407 region_id,
408 Arc::new(RegionDroppedSnafu { region_id }.build()),
409 );
410 }
411
412 pub(crate) fn on_region_closed(&mut self, region_id: RegionId) {
422 self.remove_region(region_id, Arc::new(RegionClosedSnafu { region_id }.build()));
423 }
424
425 pub(crate) fn on_region_truncated(&mut self, region_id: RegionId) {
435 self.fail_compaction(
436 region_id,
437 Arc::new(RegionTruncatedSnafu { region_id }.build()),
438 );
439 }
440
441 pub(crate) fn try_cancel_and_add_ddl<T>(
457 &mut self,
458 region_id: RegionId,
459 sender: OptionOutputTx,
460 request: T,
461 into_ddl_request: impl FnOnce(T) -> DdlRequest,
462 ) -> std::result::Result<(), (OptionOutputTx, T)> {
463 let Some(status) = self.region_status.get_mut(®ion_id) else {
464 return Err((sender, request));
465 };
466 if !status.is_compacting() {
467 return Err((sender, request));
468 }
469 status.request_cancel();
470
471 let request = SenderDdlRequest {
472 region_id,
473 sender,
474 request: into_ddl_request(request),
475 };
476 debug!(
477 "Added pending DDL request for region: {}, ddl: {:?}",
478 request.region_id, request.request
479 );
480 status.pending_ddl_requests.push(request);
481 Ok(())
482 }
483}
484
485impl CompactionScheduler {
487 fn next_plan_id(counter: &mut u64) -> u64 {
492 let plan_id = *counter;
493 *counter = counter.wrapping_add(1);
494 plan_id
495 }
496
497 #[allow(clippy::too_many_arguments)]
498 fn schedule_compaction(
499 &mut self,
500 trigger: CompactionTrigger,
501 compact_options: compact_request::Options,
502 version_control: &VersionControlRef,
503 access_layer: &AccessLayerRef,
504 waiter: OptionOutputTx,
505 manifest_ctx: &ManifestContextRef,
506 schema_metadata_manager: SchemaMetadataManagerRef,
507 max_parallelism: usize,
508 time_range: Option<TimestampRange>,
509 ) -> Result<bool> {
510 if self.disable_compaction {
511 if matches!(trigger, CompactionTrigger::Manual) {
512 waiter.send(
513 UnsupportedOperationSnafu {
514 err_msg: format!("SST compaction is disabled by {DISABLE_COMPACTION_ENV}"),
515 }
516 .fail(),
517 );
518 }
519 return Ok(false);
520 }
521 let region_id = version_control.region_id();
522 let status = self.region_status.entry(region_id).or_insert_with(|| {
523 CompactionStatus::new(region_id, version_control.clone(), access_layer.clone())
524 });
525 if status.has_ddl() {
526 info!(
527 "Region {} has pending DDL requests, ignoring compaction: queued={}, executing={}",
528 region_id,
529 status.pending_ddl_requests.len(),
530 status.executing_ddl.is_some(),
531 );
532 waiter.send(CompactionCancelledSnafu.fail());
533 return Ok(false);
534 }
535 let current_state = manifest_ctx.current_state();
536 if current_state == RegionRoleState::Leader(RegionLeaderState::Staging) {
537 info!(
538 "Skipping compaction for region {} in staging mode, options: {:?}",
539 region_id, compact_options
540 );
541 waiter.send(Ok(0));
542 return Ok(false);
543 }
544
545 if status.is_compacting() {
546 match trigger {
547 CompactionTrigger::Automatic => status.mark_automatic_trigger(),
548 CompactionTrigger::Manual if status.is_manual_compaction() => {
549 waiter.send(ManualCompactionAlreadyRunningSnafu { region_id }.fail());
550 info!(
551 "Region {} already has a manually triggered compaction running",
552 region_id
553 );
554 }
555 CompactionTrigger::Manual => {
556 status.set_pending_request(PendingCompaction {
557 options: compact_options,
558 waiter,
559 max_parallelism,
560 time_range,
561 });
562 info!(
563 "Region {} is running an automatic compaction; manual compaction will be re-scheduled",
564 region_id
565 );
566 }
567 }
568 return Ok(false);
569 }
570
571 let plan_id = Self::next_plan_id(&mut self.next_plan_id);
573 status.start_picking_with_trigger(plan_id, trigger);
574 let pick_history = (trigger == CompactionTrigger::Automatic
575 && matches!(compact_options, compact_request::Options::Regular(_)))
576 .then(|| status.pick_history.clone());
577 let request = status.new_compaction_request(
578 self.request_sender.clone(),
579 self.engine_config.clone(),
580 self.cache_manager.clone(),
581 manifest_ctx,
582 self.listener.clone(),
583 schema_metadata_manager,
584 max_parallelism,
585 pick_history,
586 );
587 status.merge_waiter(waiter);
588 self.dispatch_compaction_planning(plan_id, request, compact_options, time_range, trigger);
589 self.listener.on_compaction_scheduled(region_id);
590 Ok(true)
591 }
592
593 fn handle_pending_compaction_request(
597 &mut self,
598 region_id: RegionId,
599 manifest_ctx: &ManifestContextRef,
600 schema_metadata_manager: SchemaMetadataManagerRef,
601 ) -> bool {
602 let Some(status) = self.region_status.get_mut(®ion_id) else {
603 return true;
604 };
605
606 let Some(pending_request) = std::mem::take(&mut status.pending_request) else {
609 return false;
610 };
611
612 let PendingCompaction {
613 options,
614 waiter,
615 max_parallelism,
616 time_range,
617 } = pending_request;
618
619 let request = status.new_compaction_request(
620 self.request_sender.clone(),
621 self.engine_config.clone(),
622 self.cache_manager.clone(),
623 manifest_ctx,
624 self.listener.clone(),
625 schema_metadata_manager,
626 max_parallelism,
627 None,
628 );
629 status.merge_waiter(waiter);
630 let plan_id = Self::next_plan_id(&mut self.next_plan_id);
634 status.start_picking_with_trigger(plan_id, CompactionTrigger::Manual);
635 status.active.task_state = status.pending_task_state.take();
636 self.dispatch_compaction_planning(
637 plan_id,
638 request,
639 options,
640 time_range,
641 CompactionTrigger::Manual,
642 );
643 debug!(
644 "Successfully scheduled manual compaction planning for region id: {}",
645 region_id
646 );
647 true
648 }
649
650 async fn on_compaction_finished(
652 &mut self,
653 region_id: RegionId,
654 manifest_ctx: &ManifestContextRef,
655 schema_metadata_manager: SchemaMetadataManagerRef,
656 made_progress: bool,
657 ) -> CompactionTransition {
658 if let Some(ddls) = self.take_ready_ddls(region_id) {
659 return CompactionTransition::DdlReady(ddls);
660 }
661
662 if self.handle_pending_compaction_request(
663 region_id,
664 manifest_ctx,
665 schema_metadata_manager.clone(),
666 ) {
667 return CompactionTransition::NoAction;
668 }
669
670 let Some(status) = self.region_status.get_mut(®ion_id) else {
671 return CompactionTransition::NoAction;
672 };
673 for waiter in status.take_waiters() {
674 waiter.send(Ok(0));
675 }
676
677 let should_continue = status.active.reset_automatic_followup() || made_progress;
681 if should_continue
682 && self.schedule_automatic_followup(region_id, manifest_ctx, schema_metadata_manager)
683 {
684 return CompactionTransition::AutomaticFollowupScheduled;
685 }
686 if let Some(status) = self.region_status.get_mut(®ion_id) {
687 status.become_idle();
688 }
689 CompactionTransition::NoAction
690 }
691
692 fn schedule_automatic_followup(
693 &mut self,
694 region_id: RegionId,
695 manifest_ctx: &ManifestContextRef,
696 schema_metadata_manager: SchemaMetadataManagerRef,
697 ) -> bool {
698 if self.disable_compaction {
699 return false;
700 }
701 let Some(status) = self.region_status.get_mut(®ion_id) else {
702 return false;
703 };
704 let request = status.new_compaction_request(
706 self.request_sender.clone(),
707 self.engine_config.clone(),
708 self.cache_manager.clone(),
709 manifest_ctx,
710 self.listener.clone(),
711 schema_metadata_manager,
712 self.engine_config.max_background_compactions,
713 Some(status.pick_history.clone()),
714 );
715 let plan_id = Self::next_plan_id(&mut self.next_plan_id);
719 status.start_regular_picking(plan_id);
720 self.dispatch_compaction_planning(
721 plan_id,
722 request,
723 compact_request::Options::Regular(Default::default()),
724 None,
725 CompactionTrigger::Automatic,
726 );
727 debug!(
728 "Successfully scheduled next compaction planning for region id: {}",
729 region_id
730 );
731 true
732 }
733
734 #[cfg(test)]
736 async fn on_compaction_cancelled(&mut self, region_id: RegionId) -> Vec<SenderDdlRequest> {
737 self.finish_compaction_on_cancel(region_id)
738 }
739
740 fn on_compaction_failed(&mut self, region_id: RegionId, err: Arc<Error>) {
742 error!(err; "Region {} failed to compact, cancel all pending tasks", region_id);
743 self.fail_compaction(region_id, err);
744 }
745
746 #[cfg(test)]
747 fn add_ddl_request_to_pending(&mut self, request: SenderDdlRequest) {
748 self.region_status
749 .get_mut(&request.region_id)
750 .unwrap()
751 .pending_ddl_requests
752 .push(request);
753 }
754
755 #[cfg(test)]
756 fn has_pending_ddls(&self, region_id: RegionId) -> bool {
757 let has_pending = self
758 .region_status
759 .get(®ion_id)
760 .map(|status| !status.pending_ddl_requests.is_empty())
761 .unwrap_or(false);
762 debug!(
763 "Checked pending DDL requests for region: {}, has_pending: {}",
764 region_id, has_pending
765 );
766 has_pending
767 }
768
769 #[cfg(test)]
770 fn request_cancel(&mut self, region_id: RegionId) -> RequestCancelResult {
771 self.region_status
772 .get_mut(®ion_id)
773 .unwrap()
774 .request_cancel()
775 }
776
777 fn remove_region(&mut self, region_id: RegionId, err: Arc<Error>) {
779 if let Some(mut status) = self.region_status.remove(®ion_id) {
780 status.on_failure(err);
781 }
782 self.coordinator.forget_region(region_id);
783 }
784
785 fn fail_compaction(&mut self, region_id: RegionId, err: Arc<Error>) {
787 if let Some(status) = self.region_status.get_mut(®ion_id) {
788 status.on_failure(err);
789 }
790 }
791
792 fn finish_compaction_on_cancel(&mut self, region_id: RegionId) -> Vec<SenderDdlRequest> {
794 let Some(status) = self.region_status.get_mut(®ion_id) else {
795 return Vec::new();
796 };
797
798 let ddls = status.on_cancel();
799 self.fence_ddl_replies(region_id, ddls)
800 }
801
802 fn take_ready_ddls(&mut self, region_id: RegionId) -> Option<Vec<SenderDdlRequest>> {
804 if self
805 .region_status
806 .get(®ion_id)?
807 .pending_ddl_requests
808 .is_empty()
809 {
810 return None;
811 }
812 Some(self.finish_compaction_on_cancel(region_id))
813 }
814
815 fn fence_ddl_replies(
824 &mut self,
825 region_id: RegionId,
826 mut ddls: Vec<SenderDdlRequest>,
827 ) -> Vec<SenderDdlRequest> {
828 if ddls.is_empty() {
829 return ddls;
830 }
831 let generation = Self::next_plan_id(&mut self.next_plan_id);
832 if let Some(status) = self.region_status.get_mut(®ion_id) {
833 status.executing_ddl = Some(DdlExecution::new(generation, ddls.len()));
835 }
836
837 for (request_index, ddl) in ddls.iter_mut().enumerate() {
838 let (tx, rx) = tokio::sync::oneshot::channel();
842 let reply = std::mem::replace(&mut ddl.sender, tx.into());
843 let sender = self.request_sender.clone();
844 common_runtime::spawn_global(async move {
845 use snafu::ResultExt;
846 let result = rx
847 .await
848 .context(crate::error::RecvSnafu)
849 .and_then(|result| result);
850 let _ = sender
861 .send(WorkerRequestWithTime::new(
862 crate::request::WorkerRequest::Background {
863 region_id,
864 notify: crate::request::BackgroundNotify::CompactionDdlComplete {
865 generation,
866 request_index,
867 },
868 },
869 ))
870 .await;
871 reply.send(result);
872 });
873 }
874 ddls
875 }
876
877 pub(crate) fn on_ddl_complete(
879 &mut self,
880 region_id: RegionId,
881 generation: u64,
882 request_index: usize,
883 ) {
884 let Some(status) = self.region_status.get_mut(®ion_id) else {
885 return;
886 };
887 if let Some(ddl) = &mut status.executing_ddl
891 && ddl.complete(generation, request_index)
892 {
893 status.executing_ddl = None;
894 }
895 }
896}
897
898impl Drop for CompactionScheduler {
899 fn drop(&mut self) {
900 for (region_id, mut status) in self.region_status.drain() {
901 status.on_failure(Arc::new(RegionClosedSnafu { region_id }.build()));
903 self.coordinator.forget_region(region_id);
904 }
905 }
906}
907
908impl CompactionScheduler {
909 #[cfg(test)]
911 pub(crate) fn inspect_tasks(
912 &self,
913 filters: &MetadataFilter,
914 node_id: Option<u64>,
915 ) -> Vec<CompactionTaskEntry> {
916 let mut entries = Vec::new();
917 for (region_id, status) in &self.region_status {
918 if !filters.matches(|name| match name {
919 compaction_task::REGION_ID => Some(ScalarValue::UInt64(Some(region_id.as_u64()))),
920 compaction_task::TABLE_ID => Some(ScalarValue::UInt32(Some(region_id.table_id()))),
921 compaction_task::NODE_ID => Some(ScalarValue::UInt64(node_id)),
922 _ => None,
923 }) {
924 continue;
925 }
926 let (active, pending) = CompactionState::current(&status.shared_state);
927 for task in active.into_iter().chain(pending) {
928 let entry = task.snapshot(node_id);
929 if entry.matches(filters) {
930 entries.push(entry);
931 }
932 }
933 }
934 entries
935 }
936}
937
938#[cfg(test)]
939#[path = "scheduler_test.rs"]
940mod tests;