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 explicit automatic
268    /// follow-up, or removes the region status. The returned transition reports
269    /// a dispatched automatic follow-up or DDLs that are now safe to execute.
270    ///
271    /// # Constraints
272    ///
273    /// The owning worker must call this only after accepting and applying the
274    /// execution's result, and must execute every returned DDL request. Stale
275    /// executions are ignored.
276    pub(crate) async fn on_execution_finished(
277        &mut self,
278        region_id: RegionId,
279        execution: &CompactionExecution,
280        manifest_ctx: &ManifestContextRef,
281        schema_metadata_manager: SchemaMetadataManagerRef,
282    ) -> CompactionTransition {
283        if !self.is_current_execution(region_id, execution) {
284            return CompactionTransition::NoAction;
285        }
286        self.on_compaction_finished(region_id, manifest_ctx, schema_metadata_manager)
287            .await
288    }
289
290    /// Completes a cooperatively canceled execution.
291    ///
292    /// # Effects
293    ///
294    /// Removes the matching region status, notifies compaction waiters, and
295    /// returns DDLs that were waiting for cancellation.
296    ///
297    /// # Constraints
298    ///
299    /// The owning worker must execute every returned DDL request. A stale
300    /// cancellation is ignored.
301    pub(crate) async fn on_execution_cancelled(
302        &mut self,
303        region_id: RegionId,
304        execution: &CompactionExecution,
305    ) -> Vec<SenderDdlRequest> {
306        if !self.is_current_execution(region_id, execution) {
307            return Vec::new();
308        }
309        self.on_compaction_cancelled(region_id).await
310    }
311
312    /// Records failure of the installed execution.
313    ///
314    /// # Effects
315    ///
316    /// Removes the matching region status and fails all compaction waiters and
317    /// dependent DDL requests with the supplied error.
318    ///
319    /// # Constraints
320    ///
321    /// A stale execution failure is ignored so it cannot tear down replacement
322    /// state.
323    pub(crate) fn on_execution_failed(
324        &mut self,
325        region_id: RegionId,
326        execution: &CompactionExecution,
327        err: Arc<Error>,
328    ) {
329        if !self.is_current_execution(region_id, execution) {
330            return;
331        }
332        self.on_compaction_failed(region_id, err);
333    }
334
335    /// Removes compaction state because the region was dropped.
336    ///
337    /// # Effects
338    ///
339    /// Fails all compaction and DDL waiters owned by the status.
340    ///
341    /// # Constraints
342    ///
343    /// The owning worker must invoke this as part of serialized region teardown.
344    pub(crate) fn on_region_dropped(&mut self, region_id: RegionId) {
345        self.remove_region_on_failure(
346            region_id,
347            Arc::new(RegionDroppedSnafu { region_id }.build()),
348        );
349    }
350
351    /// Removes compaction state because the region was closed.
352    ///
353    /// # Effects
354    ///
355    /// Fails all compaction and DDL waiters owned by the status.
356    ///
357    /// # Constraints
358    ///
359    /// The owning worker must invoke this as part of serialized region teardown.
360    pub(crate) fn on_region_closed(&mut self, region_id: RegionId) {
361        self.remove_region_on_failure(region_id, Arc::new(RegionClosedSnafu { region_id }.build()));
362    }
363
364    /// Removes compaction state because the region was truncated.
365    ///
366    /// # Effects
367    ///
368    /// Fails all compaction and DDL waiters owned by the status.
369    ///
370    /// # Constraints
371    ///
372    /// The owning worker must invoke this after truncate completes.
373    pub(crate) fn on_region_truncated(&mut self, region_id: RegionId) {
374        self.remove_region_on_failure(
375            region_id,
376            Arc::new(RegionTruncatedSnafu { region_id }.build()),
377        );
378    }
379
380    /// Cancels a running compaction and queues its dependent DDL atomically.
381    ///
382    /// # Effects
383    ///
384    /// Requests cancellation when the active phase can still stop, then queues
385    /// the DDL. The queued DDL forms a scheduling fence for subsequent
386    /// compaction triggers. If no compaction is running, returns the sender and
387    /// typed request unchanged.
388    ///
389    /// # Constraints
390    ///
391    /// Production callers use this only for [`DdlRequest::Truncate`] and
392    /// [`DdlRequest::EnterStaging`]. `Ok(())` means the caller must wait for a
393    /// terminal callback to receive and execute the queued DDL; `Err` means the
394    /// caller must execute the returned request directly.
395    pub(crate) fn try_cancel_and_add_ddl<T>(
396        &mut self,
397        region_id: RegionId,
398        sender: OptionOutputTx,
399        request: T,
400        into_ddl_request: impl FnOnce(T) -> DdlRequest,
401    ) -> std::result::Result<(), (OptionOutputTx, T)> {
402        let Some(status) = self.region_status.get_mut(&region_id) else {
403            return Err((sender, request));
404        };
405        status.request_cancel();
406
407        let request = SenderDdlRequest {
408            region_id,
409            sender,
410            request: into_ddl_request(request),
411        };
412        debug!(
413            "Added pending DDL request for region: {}, ddl: {:?}",
414            request.region_id, request.request
415        );
416        status.pending_ddl_requests.push(request);
417        Ok(())
418    }
419}
420
421// Internal state-machine helpers.
422impl CompactionScheduler {
423    /// Returns the current plan id and advances the counter.
424    ///
425    /// Takes the counter instead of `&mut self` so callers can bump it while
426    /// holding a mutable borrow of a region status.
427    fn next_plan_id(counter: &mut u64) -> u64 {
428        let plan_id = *counter;
429        *counter = counter.wrapping_add(1);
430        plan_id
431    }
432
433    #[allow(clippy::too_many_arguments)]
434    fn schedule_compaction(
435        &mut self,
436        trigger: CompactionTrigger,
437        compact_options: compact_request::Options,
438        version_control: &VersionControlRef,
439        access_layer: &AccessLayerRef,
440        waiter: OptionOutputTx,
441        manifest_ctx: &ManifestContextRef,
442        schema_metadata_manager: SchemaMetadataManagerRef,
443        max_parallelism: usize,
444        time_range: Option<TimestampRange>,
445    ) -> Result<bool> {
446        let region_id = version_control.region_id();
447        let current_state = manifest_ctx.current_state();
448        if current_state == RegionRoleState::Leader(RegionLeaderState::Staging) {
449            info!(
450                "Skipping compaction for region {} in staging mode, options: {:?}",
451                region_id, compact_options
452            );
453            waiter.send(Ok(0));
454            return Ok(false);
455        }
456
457        if let Some(status) = self.region_status.get_mut(&region_id) {
458            // Pending Truncate/EnterStaging requests form a scheduling fence. Any later
459            // manual request receives CompactionCancelled; automatic triggers are ignored.
460            if !status.pending_ddl_requests.is_empty() {
461                waiter.send(CompactionCancelledSnafu.fail());
462                info!(
463                    "Region {} has pending DDL requests, ignoring compaction: {:?}",
464                    region_id, compact_options
465                );
466                return Ok(false);
467            }
468
469            match trigger {
470                CompactionTrigger::Automatic => status.mark_automatic_trigger(),
471                CompactionTrigger::Manual if status.is_manual_compaction() => {
472                    waiter.send(ManualCompactionAlreadyRunningSnafu { region_id }.fail());
473                    info!(
474                        "Region {} already has a manually triggered compaction running",
475                        region_id
476                    );
477                }
478                CompactionTrigger::Manual => {
479                    status.set_pending_request(PendingCompaction {
480                        options: compact_options,
481                        waiter,
482                        max_parallelism,
483                        time_range,
484                    });
485                    info!(
486                        "Region {} is running an automatic compaction; manual compaction will be re-scheduled",
487                        region_id
488                    );
489                }
490            }
491            return Ok(false);
492        }
493
494        // Publish the picking phase before dispatching background planning.
495        let plan_id = Self::next_plan_id(&mut self.next_plan_id);
496        let mut status = CompactionStatus::new(
497            region_id,
498            version_control.clone(),
499            access_layer.clone(),
500            plan_id,
501            trigger,
502        );
503        let request = status.new_compaction_request(
504            self.request_sender.clone(),
505            self.engine_config.clone(),
506            self.cache_manager.clone(),
507            manifest_ctx,
508            self.listener.clone(),
509            schema_metadata_manager,
510            max_parallelism,
511        );
512        status.merge_waiter(waiter);
513        self.region_status.insert(region_id, status);
514        self.dispatch_compaction_planning(plan_id, request, compact_options, time_range);
515        self.listener.on_compaction_scheduled(region_id);
516        Ok(true)
517    }
518
519    // Handle pending manual compaction request for the region.
520    //
521    // Returns true if should early return, false otherwise.
522    fn handle_pending_compaction_request(
523        &mut self,
524        region_id: RegionId,
525        manifest_ctx: &ManifestContextRef,
526        schema_metadata_manager: SchemaMetadataManagerRef,
527    ) -> bool {
528        let Some(status) = self.region_status.get_mut(&region_id) else {
529            return true;
530        };
531
532        // If there is a pending manual compaction request, schedule it.
533        // and defer returning the pending DDL requests to the caller.
534        let Some(pending_request) = std::mem::take(&mut status.pending_request) else {
535            return false;
536        };
537
538        let PendingCompaction {
539            options,
540            waiter,
541            max_parallelism,
542            time_range,
543        } = pending_request;
544
545        let request = status.new_compaction_request(
546            self.request_sender.clone(),
547            self.engine_config.clone(),
548            self.cache_manager.clone(),
549            manifest_ctx,
550            self.listener.clone(),
551            schema_metadata_manager,
552            max_parallelism,
553        );
554        status.merge_waiter(waiter);
555        // Bump the counter through a disjoint field borrow so the `status`
556        // borrow stays alive; nothing could have removed the status since it
557        // was fetched above.
558        let plan_id = Self::next_plan_id(&mut self.next_plan_id);
559        status.start_picking_with_trigger(plan_id, CompactionTrigger::Manual);
560        self.dispatch_compaction_planning(plan_id, request, options, time_range);
561        debug!(
562            "Successfully scheduled manual compaction planning for region id: {}",
563            region_id
564        );
565        true
566    }
567
568    /// Notifies the scheduler that the compaction job is finished successfully.
569    async fn on_compaction_finished(
570        &mut self,
571        region_id: RegionId,
572        manifest_ctx: &ManifestContextRef,
573        schema_metadata_manager: SchemaMetadataManagerRef,
574    ) -> CompactionTransition {
575        if !self.region_status.contains_key(&region_id) {
576            return CompactionTransition::NoAction;
577        }
578
579        if self.handle_pending_compaction_request(
580            region_id,
581            manifest_ctx,
582            schema_metadata_manager.clone(),
583        ) {
584            return CompactionTransition::NoAction;
585        }
586
587        // The region status might be removed by the previous steps.
588        // So we return empty DDL requests.
589        let Some(status) = self.region_status.get_mut(&region_id) else {
590            return CompactionTransition::NoAction;
591        };
592        for waiter in status.take_waiters() {
593            waiter.send(Ok(0));
594        }
595
596        // A queued DDL was waiting for the current task to terminate; chaining
597        // another compaction ahead of it would delay the DDL by a whole extra
598        // plan/execution cycle, so dispatch the DDLs first.
599        let pending_ddl_requests = std::mem::take(&mut status.pending_ddl_requests);
600        if !pending_ddl_requests.is_empty() {
601            // The DDL supersedes any retained automatic follow-up.
602            self.region_status.remove(&region_id);
603            // If there are pending DDL requests, we should return them to the caller.
604            // And skip try to schedule next compaction task.
605            return CompactionTransition::DdlReady(pending_ddl_requests);
606        }
607
608        if status.active.reset_automatic_followup()
609            && self.schedule_automatic_followup(region_id, manifest_ctx, schema_metadata_manager)
610        {
611            return CompactionTransition::AutomaticFollowupScheduled;
612        }
613        self.region_status.remove(&region_id);
614        CompactionTransition::NoAction
615    }
616
617    fn schedule_automatic_followup(
618        &mut self,
619        region_id: RegionId,
620        manifest_ctx: &ManifestContextRef,
621        schema_metadata_manager: SchemaMetadataManagerRef,
622    ) -> bool {
623        let Some(status) = self.region_status.get_mut(&region_id) else {
624            return false;
625        };
626        // An external automatic trigger requires one unrestricted follow-up pick.
627        let request = status.new_compaction_request(
628            self.request_sender.clone(),
629            self.engine_config.clone(),
630            self.cache_manager.clone(),
631            manifest_ctx,
632            self.listener.clone(),
633            schema_metadata_manager,
634            MAX_PARALLEL_COMPACTION,
635        );
636        // Bump the counter through a disjoint field borrow so the `status`
637        // borrow stays alive; nothing could have removed the status since it
638        // was fetched above.
639        let plan_id = Self::next_plan_id(&mut self.next_plan_id);
640        status.start_regular_picking(plan_id);
641        self.dispatch_compaction_planning(
642            plan_id,
643            request,
644            compact_request::Options::Regular(Default::default()),
645            None,
646        );
647        debug!(
648            "Successfully scheduled next compaction planning for region id: {}",
649            region_id
650        );
651        true
652    }
653
654    /// Notifies the scheduler that the compaction job is cancelled cooperatively.
655    async fn on_compaction_cancelled(&mut self, region_id: RegionId) -> Vec<SenderDdlRequest> {
656        self.remove_region_on_cancel(region_id)
657    }
658
659    /// Notifies the scheduler that the compaction job is failed.
660    fn on_compaction_failed(&mut self, region_id: RegionId, err: Arc<Error>) {
661        error!(err; "Region {} failed to compact, cancel all pending tasks", region_id);
662        self.remove_region_on_failure(region_id, err);
663    }
664
665    #[cfg(test)]
666    fn add_ddl_request_to_pending(&mut self, request: SenderDdlRequest) {
667        self.region_status
668            .get_mut(&request.region_id)
669            .unwrap()
670            .pending_ddl_requests
671            .push(request);
672    }
673
674    #[cfg(test)]
675    fn has_pending_ddls(&self, region_id: RegionId) -> bool {
676        let has_pending = self
677            .region_status
678            .get(&region_id)
679            .map(|status| !status.pending_ddl_requests.is_empty())
680            .unwrap_or(false);
681        debug!(
682            "Checked pending DDL requests for region: {}, has_pending: {}",
683            region_id, has_pending
684        );
685        has_pending
686    }
687
688    #[cfg(test)]
689    fn request_cancel(&mut self, region_id: RegionId) -> RequestCancelResult {
690        self.region_status
691            .get_mut(&region_id)
692            .unwrap()
693            .request_cancel()
694    }
695
696    fn remove_region_on_failure(&mut self, region_id: RegionId, err: Arc<Error>) {
697        // Remove this region.
698        let Some(status) = self.region_status.remove(&region_id) else {
699            return;
700        };
701
702        // Notifies all pending tasks.
703        status.on_failure(err);
704    }
705
706    fn remove_region_on_cancel(&mut self, region_id: RegionId) -> Vec<SenderDdlRequest> {
707        let Some(status) = self.region_status.remove(&region_id) else {
708            return Vec::new();
709        };
710
711        status.on_cancel()
712    }
713}
714
715impl Drop for CompactionScheduler {
716    fn drop(&mut self) {
717        for (region_id, status) in self.region_status.drain() {
718            // We are shutting down so notify all pending tasks.
719            status.on_failure(Arc::new(RegionClosedSnafu { region_id }.build()));
720        }
721    }
722}
723
724#[cfg(test)]
725#[path = "scheduler_test.rs"]
726mod tests;