Skip to main content

mito2/compaction/
scheduler.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15mod 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
51/// Compaction scheduler tracks and manages compaction tasks.
52pub(crate) struct CompactionScheduler {
53    scheduler: SchedulerRef,
54    /// Compacting regions.
55    region_status: HashMap<RegionId, CompactionStatus>,
56    /// Request sender of the worker that this scheduler belongs to.
57    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 for the compaction scheduler.
64    plugins: Plugins,
65    /// Scheduler-wide generation counter for compaction plans and executions.
66    /// It outlives region statuses so close/reopen cannot reuse an old identity.
67    next_plan_id: u64,
68}
69
70/// Describes the immediate action produced by a compaction terminal transition.
71#[derive(Debug)]
72pub(crate) enum CompactionTransition {
73    /// No follow-up work was dispatched and no DDL became ready.
74    NoAction,
75    /// An automatic follow-up planning cycle was dispatched.
76    AutomaticFollowupScheduled,
77    /// DDL requests became ready after the compaction lifecycle terminated.
78    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
104// API used by callers outside the compaction scheduler module.
105impl CompactionScheduler {
106    /// Creates an empty scheduler bound to one region worker.
107    ///
108    /// # Effects
109    ///
110    /// Stores the worker request sender and shared scheduling resources. No
111    /// compaction is dispatched until a scheduling method is called.
112    ///
113    /// # Constraints
114    ///
115    /// All lifecycle methods on the returned scheduler must be driven by the
116    /// same worker so region state transitions remain serialized.
117    #[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    /// Accepts an automatic compaction trigger for a region.
143    ///
144    /// # Effects
145    ///
146    /// Starts planning when no scheduler status exists for the region.
147    /// Otherwise, an active status coalesces the trigger into one unrestricted
148    /// follow-up cycle. Returns `true` only when this call dispatches planning.
149    ///
150    /// # Constraints
151    ///
152    /// The owning region worker must call this method serially. Automatic
153    /// triggers have no waiter and may be ignored while a DDL fence is active.
154    /// The region identity comes from `version_control`; `access_layer` and
155    /// `manifest_ctx` must belong to the same region.
156    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, // Default for automatic compaction
173            None,
174        )
175    }
176
177    /// Accepts a manual compaction request for a region.
178    ///
179    /// # Effects
180    ///
181    /// Starts planning when no scheduler status exists for the region. During
182    /// an automatic compaction, queues the request and replaces any older
183    /// pending manual request; during a manual compaction, rejects the new
184    /// request through `waiter`. Returns `true` only when this call dispatches
185    /// planning.
186    ///
187    /// # Constraints
188    ///
189    /// The owning region worker must call this method serially. The supplied
190    /// waiter is completed exactly once by cycle completion, replacement,
191    /// cancellation, failure, or region teardown. The region identity comes
192    /// from `version_control`; `access_layer` and `manifest_ctx` must belong to
193    /// the same region.
194    #[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    /// Applies a background planning result to the current region state.
220    ///
221    /// # Effects
222    ///
223    /// Rejects stale plans, submits a prepared local or remote execution, or
224    /// completes the cycle when planning produced no executable plan. The
225    /// returned transition reports a dispatched automatic follow-up or DDL
226    /// requests released by the resulting terminal transition.
227    ///
228    /// # Constraints
229    ///
230    /// The owning worker must call this method for planning notifications and
231    /// must execute every returned DDL request. The notification may be stale;
232    /// stale notifications intentionally have no effect.
233    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    /// Returns whether a terminal notification belongs to the installed execution.
244    ///
245    /// # Effects
246    ///
247    /// Performs a read-only identity check against the region's current plan.
248    ///
249    /// # Constraints
250    ///
251    /// Callers must check this before applying a compaction edit. Matching only
252    /// the region id is insufficient because close/reopen can replace a status.
253    pub(crate) fn is_current_execution(
254        &self,
255        region_id: RegionId,
256        execution: &CompactionExecution,
257    ) -> bool {
258        self.region_status
259            .get(&region_id)
260            .is_some_and(|status| status.matches_execution(execution))
261    }
262
263    /// Completes the installed execution after its edit has been applied.
264    ///
265    /// # Effects
266    ///
267    /// Notifies waiters, schedules a pending manual or automatic follow-up, or
268    /// removes the region status. A follow-up is scheduled whenever the cycle
269    /// latched an automatic trigger, or when `made_progress` is true (the
270    /// execution removed more files than it added): successful compaction keeps
271    /// draining the region until the picker returns no plan. The returned
272    /// transition reports a dispatched automatic follow-up or DDLs that are now
273    /// safe to execute.
274    ///
275    /// # Constraints
276    ///
277    /// The owning worker must call this only after accepting and applying the
278    /// execution's result, and must execute every returned DDL request. Stale
279    /// executions are ignored.
280    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    /// Completes a cooperatively canceled execution.
301    ///
302    /// # Effects
303    ///
304    /// Removes the matching region status, notifies compaction waiters, and
305    /// returns DDLs that were waiting for cancellation.
306    ///
307    /// # Constraints
308    ///
309    /// The owning worker must execute every returned DDL request. A stale
310    /// cancellation is ignored.
311    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    /// Records failure of the installed execution.
323    ///
324    /// # Effects
325    ///
326    /// Removes the matching region status and fails all compaction waiters and
327    /// dependent DDL requests with the supplied error.
328    ///
329    /// # Constraints
330    ///
331    /// A stale execution failure is ignored so it cannot tear down replacement
332    /// state.
333    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    /// Removes compaction state because the region was dropped.
346    ///
347    /// # Effects
348    ///
349    /// Fails all compaction and DDL waiters owned by the status.
350    ///
351    /// # Constraints
352    ///
353    /// The owning worker must invoke this as part of serialized region teardown.
354    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    /// Removes compaction state because the region was closed.
362    ///
363    /// # Effects
364    ///
365    /// Fails all compaction and DDL waiters owned by the status.
366    ///
367    /// # Constraints
368    ///
369    /// The owning worker must invoke this as part of serialized region teardown.
370    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    /// Removes compaction state because the region was truncated.
375    ///
376    /// # Effects
377    ///
378    /// Fails all compaction and DDL waiters owned by the status.
379    ///
380    /// # Constraints
381    ///
382    /// The owning worker must invoke this after truncate completes.
383    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    /// Cancels a running compaction and queues its dependent DDL atomically.
391    ///
392    /// # Effects
393    ///
394    /// Requests cancellation when the active phase can still stop, then queues
395    /// the DDL. The queued DDL forms a scheduling fence for subsequent
396    /// compaction triggers. If no compaction is running, returns the sender and
397    /// typed request unchanged.
398    ///
399    /// # Constraints
400    ///
401    /// Production callers use this only for [`DdlRequest::Truncate`] and
402    /// [`DdlRequest::EnterStaging`]. `Ok(())` means the caller must wait for a
403    /// terminal callback to receive and execute the queued DDL; `Err` means the
404    /// caller must execute the returned request directly.
405    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(&region_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
431// Internal state-machine helpers.
432impl CompactionScheduler {
433    /// Returns the current plan id and advances the counter.
434    ///
435    /// Takes the counter instead of `&mut self` so callers can bump it while
436    /// holding a mutable borrow of a region status.
437    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(&region_id) {
468            // Pending Truncate/EnterStaging requests form a scheduling fence. Any later
469            // manual request receives CompactionCancelled; automatic triggers are ignored.
470            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        // Publish the picking phase before dispatching background planning.
505        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    // Handle pending manual compaction request for the region.
530    //
531    // Returns true if should early return, false otherwise.
532    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(&region_id) else {
539            return true;
540        };
541
542        // If there is a pending manual compaction request, schedule it.
543        // and defer returning the pending DDL requests to the caller.
544        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        // Bump the counter through a disjoint field borrow so the `status`
566        // borrow stays alive; nothing could have removed the status since it
567        // was fetched above.
568        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    /// Notifies the scheduler that the compaction job is finished successfully.
579    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(&region_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        // The region status might be removed by the previous steps.
599        // So we return empty DDL requests.
600        let Some(status) = self.region_status.get_mut(&region_id) else {
601            return CompactionTransition::NoAction;
602        };
603        for waiter in status.take_waiters() {
604            waiter.send(Ok(0));
605        }
606
607        // A queued DDL was waiting for the current task to terminate; chaining
608        // another compaction ahead of it would delay the DDL by a whole extra
609        // plan/execution cycle, so dispatch the DDLs first.
610        let pending_ddl_requests = std::mem::take(&mut status.pending_ddl_requests);
611        if !pending_ddl_requests.is_empty() {
612            // The DDL supersedes any retained automatic follow-up.
613            self.region_status.remove(&region_id);
614            // If there are pending DDL requests, we should return them to the caller.
615            // And skip try to schedule next compaction task.
616            return CompactionTransition::DdlReady(pending_ddl_requests);
617        }
618
619        // Keep draining when the cycle latched an automatic trigger, or when the
620        // execution reduced the file count. A no-progress rewrite stops here so a
621        // split-heavy output cannot loop forever; the next flush trigger resumes.
622        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(&region_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(&region_id) else {
639            return false;
640        };
641        // An external automatic trigger requires one unrestricted follow-up pick.
642        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        // Bump the counter through a disjoint field borrow so the `status`
652        // borrow stays alive; nothing could have removed the status since it
653        // was fetched above.
654        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    /// Notifies the scheduler that the compaction job is cancelled cooperatively.
670    async fn on_compaction_cancelled(&mut self, region_id: RegionId) -> Vec<SenderDdlRequest> {
671        self.remove_region_on_cancel(region_id)
672    }
673
674    /// Notifies the scheduler that the compaction job is failed.
675    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(&region_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(&region_id)
707            .unwrap()
708            .request_cancel()
709    }
710
711    fn remove_region_on_failure(&mut self, region_id: RegionId, err: Arc<Error>) {
712        // Remove this region.
713        let Some(status) = self.region_status.remove(&region_id) else {
714            return;
715        };
716
717        // Notifies all pending tasks.
718        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(&region_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            // We are shutting down so notify all pending tasks.
734            status.on_failure(Arc::new(RegionClosedSnafu { region_id }.build()));
735        }
736    }
737}
738
739#[cfg(test)]
740#[path = "scheduler_test.rs"]
741mod tests;