Skip to main content

mito2/
compaction.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 buckets;
16pub mod compactor;
17pub mod memory_manager;
18pub mod picker;
19pub mod run;
20mod task;
21#[cfg(test)]
22mod test_util;
23mod twcs;
24mod window;
25
26use std::collections::HashMap;
27use std::sync::{Arc, Mutex};
28use std::time::Instant;
29
30use api::v1::region::compact_request;
31use api::v1::region::compact_request::Options;
32use arrow_schema::Schema;
33use common_base::Plugins;
34use common_base::cancellation::CancellationHandle;
35use common_memory_manager::OnExhaustedPolicy;
36use common_meta::key::SchemaMetadataManagerRef;
37use common_telemetry::{debug, error, info, warn};
38use common_time::range::TimestampRange;
39use common_time::timestamp::TimeUnit;
40use common_time::{TimeToLive, Timestamp};
41use datafusion_common::ScalarValue;
42use datafusion_expr::Expr;
43use datatypes::extension::json::is_json_extension_type;
44use datatypes::schema::ext::ArrowSchemaExt;
45use datatypes::types::json_type::JsonNativeType;
46use parquet::arrow::parquet_to_arrow_schema;
47use parquet::file::metadata::PageIndexPolicy;
48use serde::{Deserialize, Serialize};
49use snafu::{OptionExt, ResultExt};
50use store_api::metadata::RegionMetadataRef;
51use store_api::storage::RegionId;
52use task::MAX_PARALLEL_COMPACTION;
53use tokio::sync::mpsc::{self, Sender};
54
55use crate::access_layer::AccessLayerRef;
56use crate::cache::{CacheManagerRef, CacheStrategy};
57use crate::compaction::compactor::{CompactionRegion, CompactionVersion, DefaultCompactor};
58use crate::compaction::memory_manager::CompactionMemoryManager;
59use crate::compaction::picker::{CompactionTask, PickerOutput, new_picker};
60use crate::compaction::task::CompactionTaskImpl;
61use crate::config::MitoConfig;
62use crate::error::{
63    CompactRegionSnafu, CompactionCancelledSnafu, DataTypeMismatchSnafu, Error,
64    GetSchemaMetadataSnafu, ManualCompactionOverrideSnafu, ParquetToArrowSchemaSnafu,
65    RegionClosedSnafu, RegionDroppedSnafu, RegionTruncatedSnafu, RemoteCompactionSnafu, Result,
66    TimeRangePredicateOverflowSnafu, TimeoutSnafu,
67};
68use crate::metrics::{COMPACTION_STAGE_ELAPSED, INFLIGHT_COMPACTION_COUNT};
69use crate::read::FlatSource;
70use crate::read::flat_projection::FlatProjectionMapper;
71use crate::read::read_columns::ReadColumns;
72use crate::read::scan_region::{PredicateGroup, ScanInput};
73use crate::read::seq_scan::SeqScan;
74use crate::region::options::{MergeMode, RegionOptions};
75use crate::region::version::VersionControlRef;
76use crate::region::{ManifestContextRef, RegionLeaderState, RegionRoleState};
77use crate::request::{OptionOutputTx, OutputTx, SenderDdlRequest, WorkerRequestWithTime};
78use crate::schedule::remote_job_scheduler::{
79    CompactionJob, DefaultNotifier, RemoteJob, RemoteJobSchedulerRef,
80};
81use crate::schedule::scheduler::SchedulerRef;
82use crate::sst::file::{FileHandle, FileMeta, Level};
83use crate::sst::parquet::reader::MetadataCacheMetrics;
84use crate::sst::version::LevelMeta;
85use crate::worker::WorkerListener;
86
87/// Region compaction request.
88pub struct CompactionRequest {
89    pub(crate) engine_config: Arc<MitoConfig>,
90    pub(crate) current_version: CompactionVersion,
91    pub(crate) access_layer: AccessLayerRef,
92    /// Sender to send notification to the region worker.
93    pub(crate) request_sender: mpsc::Sender<WorkerRequestWithTime>,
94    /// Waiters of the compaction request.
95    pub(crate) waiters: Vec<OutputTx>,
96    /// Start time of compaction task.
97    pub(crate) start_time: Instant,
98    pub(crate) cache_manager: CacheManagerRef,
99    pub(crate) manifest_ctx: ManifestContextRef,
100    pub(crate) listener: WorkerListener,
101    pub(crate) schema_metadata_manager: SchemaMetadataManagerRef,
102    pub(crate) max_parallelism: usize,
103}
104
105impl CompactionRequest {
106    pub(crate) fn region_id(&self) -> RegionId {
107        self.current_version.metadata.region_id
108    }
109}
110
111/// Compaction scheduler tracks and manages compaction tasks.
112pub(crate) struct CompactionScheduler {
113    scheduler: SchedulerRef,
114    /// Compacting regions.
115    region_status: HashMap<RegionId, CompactionStatus>,
116    /// Request sender of the worker that this scheduler belongs to.
117    request_sender: Sender<WorkerRequestWithTime>,
118    cache_manager: CacheManagerRef,
119    engine_config: Arc<MitoConfig>,
120    memory_manager: Arc<CompactionMemoryManager>,
121    memory_policy: OnExhaustedPolicy,
122    listener: WorkerListener,
123    /// Plugins for the compaction scheduler.
124    plugins: Plugins,
125}
126
127impl CompactionScheduler {
128    #[allow(clippy::too_many_arguments)]
129    pub(crate) fn new(
130        scheduler: SchedulerRef,
131        request_sender: Sender<WorkerRequestWithTime>,
132        cache_manager: CacheManagerRef,
133        engine_config: Arc<MitoConfig>,
134        listener: WorkerListener,
135        plugins: Plugins,
136        memory_manager: Arc<CompactionMemoryManager>,
137        memory_policy: OnExhaustedPolicy,
138    ) -> Self {
139        Self {
140            scheduler,
141            region_status: HashMap::new(),
142            request_sender,
143            cache_manager,
144            engine_config,
145            memory_manager,
146            memory_policy,
147            listener,
148            plugins,
149        }
150    }
151
152    /// Schedules a compaction for the region.
153    /// Returns whether a compaction is scheduled.
154    #[allow(clippy::too_many_arguments)]
155    pub(crate) async fn schedule_compaction(
156        &mut self,
157        region_id: RegionId,
158        compact_options: compact_request::Options,
159        version_control: &VersionControlRef,
160        access_layer: &AccessLayerRef,
161        waiter: OptionOutputTx,
162        manifest_ctx: &ManifestContextRef,
163        schema_metadata_manager: SchemaMetadataManagerRef,
164        max_parallelism: usize,
165    ) -> Result<bool> {
166        // skip compaction if region is in staging state
167        let current_state = manifest_ctx.current_state();
168        if current_state == RegionRoleState::Leader(RegionLeaderState::Staging) {
169            info!(
170                "Skipping compaction for region {} in staging mode, options: {:?}",
171                region_id, compact_options
172            );
173            waiter.send(Ok(0));
174            return Ok(false);
175        }
176
177        if let Some(status) = self.region_status.get_mut(&region_id) {
178            match compact_options {
179                Options::Regular(_) => {
180                    // Region is compacting. Add the waiter to pending list.
181                    status.merge_waiter(waiter);
182                }
183                options @ Options::StrictWindow(_) => {
184                    // Incoming compaction request is manually triggered.
185                    status.set_pending_request(PendingCompaction {
186                        options,
187                        waiter,
188                        max_parallelism,
189                    });
190                    info!(
191                        "Region {} is compacting, manually compaction will be re-scheduled.",
192                        region_id
193                    );
194                }
195            }
196            return Ok(false);
197        }
198
199        // The region can compact directly.
200        let mut status =
201            CompactionStatus::new(region_id, version_control.clone(), access_layer.clone());
202        let request = status.new_compaction_request(
203            self.request_sender.clone(),
204            waiter,
205            self.engine_config.clone(),
206            self.cache_manager.clone(),
207            manifest_ctx,
208            self.listener.clone(),
209            schema_metadata_manager,
210            max_parallelism,
211        );
212
213        match self
214            .schedule_compaction_request(request, compact_options)
215            .await
216        {
217            Ok(Some(active_compaction)) => {
218                // Publish CompactionStatus only after a task has been accepted by the scheduler.
219                // This avoids exposing a half-initialized region status that could collect pending
220                // DDL/compaction state even though no compaction is actually running.
221                status.active_compaction = Some(active_compaction);
222                self.region_status.insert(region_id, status);
223
224                self.listener.on_compaction_scheduled(region_id);
225                Ok(true)
226            }
227            Ok(None) => Ok(false),
228            Err(e) => Err(e),
229        }
230    }
231
232    // Handle pending manual compaction request for the region.
233    //
234    // Returns true if should early return, false otherwise.
235    pub(crate) async fn handle_pending_compaction_request(
236        &mut self,
237        region_id: RegionId,
238        manifest_ctx: &ManifestContextRef,
239        schema_metadata_manager: SchemaMetadataManagerRef,
240    ) -> bool {
241        let Some(status) = self.region_status.get_mut(&region_id) else {
242            return true;
243        };
244
245        // If there is a pending manual compaction request, schedule it.
246        // and defer returning the pending DDL requests to the caller.
247        let Some(pending_request) = std::mem::take(&mut status.pending_request) else {
248            return false;
249        };
250
251        let PendingCompaction {
252            options,
253            waiter,
254            max_parallelism,
255        } = pending_request;
256
257        let request = {
258            status.new_compaction_request(
259                self.request_sender.clone(),
260                waiter,
261                self.engine_config.clone(),
262                self.cache_manager.clone(),
263                manifest_ctx,
264                self.listener.clone(),
265                schema_metadata_manager,
266                max_parallelism,
267            )
268        };
269
270        match self.schedule_compaction_request(request, options).await {
271            Ok(Some(active_compaction)) => {
272                let status = self.region_status.get_mut(&region_id).unwrap();
273                status.active_compaction = Some(active_compaction);
274                debug!(
275                    "Successfully scheduled manual compaction for region id: {}",
276                    region_id
277                );
278                true
279            }
280            Ok(None) => {
281                // We still need to handle the pending DDL requests.
282                // So we can't return early here.
283                false
284            }
285            Err(e) => {
286                error!(e; "Failed to continue pending manual compaction for region id: {}", region_id);
287                self.remove_region_on_failure(region_id, Arc::new(e));
288                true
289            }
290        }
291    }
292
293    /// Notifies the scheduler that the compaction job is finished successfully.
294    pub(crate) async fn on_compaction_finished(
295        &mut self,
296        region_id: RegionId,
297        manifest_ctx: &ManifestContextRef,
298        schema_metadata_manager: SchemaMetadataManagerRef,
299    ) -> Vec<SenderDdlRequest> {
300        let Some(status) = self.region_status.get_mut(&region_id) else {
301            return Vec::new();
302        };
303        status.clear_running_task();
304
305        // If there a pending compaction request, handle it first
306        // and defer returning the pending DDL requests to the caller.
307        if self
308            .handle_pending_compaction_request(
309                region_id,
310                manifest_ctx,
311                schema_metadata_manager.clone(),
312            )
313            .await
314        {
315            return Vec::new();
316        }
317
318        let Some(status) = self.region_status.get_mut(&region_id) else {
319            // The region status might be removed by the previous steps.
320            // So we return empty DDL requests.
321            return Vec::new();
322        };
323
324        for waiter in std::mem::take(&mut status.waiters) {
325            waiter.send(Ok(0));
326        }
327
328        // If there are pending DDL requests, run them.
329        let pending_ddl_requests = std::mem::take(&mut status.pending_ddl_requests);
330        if !pending_ddl_requests.is_empty() {
331            self.region_status.remove(&region_id);
332            // If there are pending DDL requests, we should return them to the caller.
333            // And skip try to schedule next compaction task.
334            return pending_ddl_requests;
335        }
336        Vec::new()
337    }
338
339    pub(crate) fn is_compacting(&self, region_id: RegionId) -> bool {
340        self.region_status
341            .get(&region_id)
342            .map(|status| status.active_compaction.is_some())
343            .unwrap_or(false)
344    }
345
346    /// Schedules next compaction upon a finished compaction.
347    /// Returns whether the compaction is scheduled.
348    pub(crate) async fn schedule_next_compaction(
349        &mut self,
350        region_id: RegionId,
351        manifest_ctx: &ManifestContextRef,
352        schema_metadata_manager: SchemaMetadataManagerRef,
353    ) -> bool {
354        let Some(status) = self.region_status.get_mut(&region_id) else {
355            return false;
356        };
357
358        // We should always try to compact the region until picker returns None.
359        let request = status.new_compaction_request(
360            self.request_sender.clone(),
361            OptionOutputTx::none(),
362            self.engine_config.clone(),
363            self.cache_manager.clone(),
364            manifest_ctx,
365            self.listener.clone(),
366            schema_metadata_manager,
367            MAX_PARALLEL_COMPACTION,
368        );
369
370        // Try to schedule next compaction task for this region.
371        match self
372            .schedule_compaction_request(
373                request,
374                compact_request::Options::Regular(Default::default()),
375            )
376            .await
377        {
378            Ok(Some(active_compaction)) => {
379                self.region_status
380                    .get_mut(&region_id)
381                    .unwrap()
382                    .active_compaction = Some(active_compaction);
383                debug!(
384                    "Successfully scheduled next compaction for region id: {}",
385                    region_id
386                );
387                true
388            }
389            Ok(None) => {
390                // No further compaction tasks can be scheduled; cleanup the `CompactionStatus` for this region.
391                // All DDL requests and pending compaction requests have already been processed.
392                // Safe to remove the region from status tracking.
393                self.region_status.remove(&region_id);
394                false
395            }
396            Err(e) => {
397                error!(e; "Failed to schedule next compaction for region {}", region_id);
398                self.remove_region_on_failure(region_id, Arc::new(e));
399                false
400            }
401        }
402    }
403
404    /// Notifies the scheduler that the compaction job is cancelled cooperatively.
405    pub(crate) async fn on_compaction_cancelled(
406        &mut self,
407        region_id: RegionId,
408    ) -> Vec<SenderDdlRequest> {
409        self.remove_region_on_cancel(region_id)
410    }
411
412    /// Notifies the scheduler that the compaction job is failed.
413    pub(crate) fn on_compaction_failed(&mut self, region_id: RegionId, err: Arc<Error>) {
414        error!(err; "Region {} failed to compact, cancel all pending tasks", region_id);
415        self.remove_region_on_failure(region_id, err);
416    }
417
418    /// Notifies the scheduler that the region is dropped.
419    pub(crate) fn on_region_dropped(&mut self, region_id: RegionId) {
420        self.remove_region_on_failure(
421            region_id,
422            Arc::new(RegionDroppedSnafu { region_id }.build()),
423        );
424    }
425
426    /// Notifies the scheduler that the region is closed.
427    pub(crate) fn on_region_closed(&mut self, region_id: RegionId) {
428        self.remove_region_on_failure(region_id, Arc::new(RegionClosedSnafu { region_id }.build()));
429    }
430
431    /// Notifies the scheduler that the region is truncated.
432    pub(crate) fn on_region_truncated(&mut self, region_id: RegionId) {
433        self.remove_region_on_failure(
434            region_id,
435            Arc::new(RegionTruncatedSnafu { region_id }.build()),
436        );
437    }
438
439    /// Add ddl request to pending queue.
440    ///
441    /// # Panics
442    /// Panics if region didn't request compaction.
443    pub(crate) fn add_ddl_request_to_pending(&mut self, request: SenderDdlRequest) {
444        debug!(
445            "Added pending DDL request for region: {}, ddl: {:?}",
446            request.region_id, request.request
447        );
448        let status = self.region_status.get_mut(&request.region_id).unwrap();
449        status.pending_ddl_requests.push(request);
450    }
451
452    #[cfg(test)]
453    pub(crate) fn has_pending_ddls(&self, region_id: RegionId) -> bool {
454        let has_pending = self
455            .region_status
456            .get(&region_id)
457            .map(|status| !status.pending_ddl_requests.is_empty())
458            .unwrap_or(false);
459        debug!(
460            "Checked pending DDL requests for region: {}, has_pending: {}",
461            region_id, has_pending
462        );
463        has_pending
464    }
465
466    pub(crate) fn request_cancel(&mut self, region_id: RegionId) -> RequestCancelResult {
467        let Some(status) = self.region_status.get_mut(&region_id) else {
468            return RequestCancelResult::NotRunning;
469        };
470
471        status.request_cancel()
472    }
473
474    /// Schedules a compaction request.
475    ///
476    /// Returns the active compaction state if the request is scheduled successfully.
477    /// Returns `None` if no compaction task can be scheduled for this region.
478    async fn schedule_compaction_request(
479        &mut self,
480        request: CompactionRequest,
481        options: compact_request::Options,
482    ) -> Result<Option<ActiveCompaction>> {
483        let region_id = request.region_id();
484        let (dynamic_compaction_opts, ttl) = find_dynamic_options(
485            region_id,
486            &request.current_version.options,
487            &request.schema_metadata_manager,
488        )
489        .await
490        .unwrap_or_else(|e| {
491            warn!(e; "Failed to find dynamic options for region: {}", region_id);
492            (
493                request.current_version.options.compaction.clone(),
494                request.current_version.options.ttl.unwrap_or_default(),
495            )
496        });
497
498        let picker = new_picker(
499            &options,
500            &dynamic_compaction_opts,
501            request.current_version.options.append_mode,
502            Some(self.engine_config.max_background_compactions),
503        );
504        let region_id = request.region_id();
505        let CompactionRequest {
506            engine_config,
507            current_version,
508            access_layer,
509            request_sender,
510            waiters,
511            start_time,
512            cache_manager,
513            manifest_ctx,
514            listener,
515            schema_metadata_manager: _,
516            max_parallelism,
517        } = request;
518
519        debug!(
520            "Pick compaction strategy {:?} for region: {}, ttl: {:?}",
521            picker, region_id, ttl
522        );
523
524        let compaction_region = CompactionRegion {
525            region_id,
526            current_version: current_version.clone(),
527            region_options: RegionOptions {
528                compaction: dynamic_compaction_opts.clone(),
529                ..current_version.options.clone()
530            },
531            engine_config: engine_config.clone(),
532            region_metadata: current_version.metadata.clone(),
533            cache_manager: cache_manager.clone(),
534            access_layer: access_layer.clone(),
535            manifest_ctx: manifest_ctx.clone(),
536            file_purger: None,
537            ttl: Some(ttl),
538            max_parallelism,
539            plugins: self.plugins.clone(),
540        };
541
542        let picker_output = {
543            let _pick_timer = COMPACTION_STAGE_ELAPSED
544                .with_label_values(&["pick"])
545                .start_timer();
546            picker.pick(&compaction_region)
547        };
548
549        let picker_output = if let Some(picker_output) = picker_output {
550            picker_output
551        } else {
552            // Nothing to compact, we are done. Notifies all waiters as we consume the compaction request.
553            for waiter in waiters {
554                waiter.send(Ok(0));
555            }
556            return Ok(None);
557        };
558
559        // If specified to run compaction remotely, we schedule the compaction job remotely.
560        // It will fall back to local compaction if there is no remote job scheduler.
561        let waiters = if dynamic_compaction_opts.remote_compaction() {
562            if let Some(remote_job_scheduler) = &self.plugins.get::<RemoteJobSchedulerRef>() {
563                let remote_compaction_job = CompactionJob {
564                    compaction_region: compaction_region.clone(),
565                    picker_output: picker_output.clone(),
566                    start_time,
567                    waiters,
568                    ttl,
569                };
570
571                let result = remote_job_scheduler
572                    .schedule(
573                        RemoteJob::CompactionJob(remote_compaction_job),
574                        Box::new(DefaultNotifier {
575                            request_sender: request_sender.clone(),
576                        }),
577                    )
578                    .await;
579
580                match result {
581                    Ok(job_id) => {
582                        info!(
583                            "Scheduled remote compaction job {} for region {}",
584                            job_id, region_id
585                        );
586                        INFLIGHT_COMPACTION_COUNT.inc();
587                        return Ok(Some(ActiveCompaction::Remote));
588                    }
589                    Err(e) => {
590                        if !dynamic_compaction_opts.fallback_to_local() {
591                            error!(e; "Failed to schedule remote compaction job for region {}", region_id);
592                            return RemoteCompactionSnafu {
593                                region_id,
594                                job_id: None,
595                                reason: e.reason,
596                            }
597                            .fail();
598                        }
599
600                        error!(e; "Failed to schedule remote compaction job for region {}, fallback to local compaction", region_id);
601
602                        // Return the waiters back to the caller for local compaction.
603                        e.waiters
604                    }
605                }
606            } else {
607                debug!(
608                    "Remote compaction is not enabled, fallback to local compaction for region {}",
609                    region_id
610                );
611                waiters
612            }
613        } else {
614            waiters
615        };
616
617        // Create a local compaction task.
618        let estimated_bytes = estimate_compaction_bytes(&picker_output);
619
620        let cancel_handle = Arc::new(CancellationHandle::default());
621        let state = LocalCompactionState::new(cancel_handle.clone());
622        let local_compaction_task = Box::new(CompactionTaskImpl {
623            state: state.clone(),
624            request_sender,
625            waiters,
626            start_time,
627            listener,
628            picker_output,
629            compaction_region,
630            compactor: Arc::new(DefaultCompactor::with_cancel_handle(cancel_handle.clone())),
631            memory_manager: self.memory_manager.clone(),
632            memory_policy: self.memory_policy,
633            estimated_memory_bytes: estimated_bytes,
634        });
635
636        self.submit_compaction_task(local_compaction_task, region_id)
637            .map(|_| Some(ActiveCompaction::Local { state }))
638    }
639
640    fn submit_compaction_task(
641        &mut self,
642        mut task: Box<CompactionTaskImpl>,
643        region_id: RegionId,
644    ) -> Result<()> {
645        self.scheduler
646            .schedule(Box::pin(async move {
647                INFLIGHT_COMPACTION_COUNT.inc();
648                task.run().await;
649                INFLIGHT_COMPACTION_COUNT.dec();
650            }))
651            .inspect_err(
652                |e| error!(e; "Failed to submit compaction request for region {}", region_id),
653            )
654    }
655
656    fn remove_region_on_failure(&mut self, region_id: RegionId, err: Arc<Error>) {
657        // Remove this region.
658        let Some(status) = self.region_status.remove(&region_id) else {
659            return;
660        };
661
662        // Notifies all pending tasks.
663        status.on_failure(err);
664    }
665
666    fn remove_region_on_cancel(&mut self, region_id: RegionId) -> Vec<SenderDdlRequest> {
667        let Some(status) = self.region_status.remove(&region_id) else {
668            return Vec::new();
669        };
670
671        status.on_cancel()
672    }
673}
674
675#[derive(Debug, Clone)]
676pub(crate) struct LocalCompactionState {
677    cancel_handle: Arc<CancellationHandle>,
678    commit_started: Arc<Mutex<bool>>,
679}
680
681#[derive(Debug)]
682enum ActiveCompaction {
683    Local { state: LocalCompactionState },
684    Remote,
685}
686
687impl LocalCompactionState {
688    fn new(cancel_handle: Arc<CancellationHandle>) -> Self {
689        Self {
690            cancel_handle,
691            commit_started: Arc::new(Mutex::new(false)),
692        }
693    }
694
695    /// Returns the cancellation handle for this compaction task.
696    pub(crate) fn cancel_handle(&self) -> Arc<CancellationHandle> {
697        self.cancel_handle.clone()
698    }
699
700    /// Marks the compaction task as started to commit,
701    /// which means the compaction task is in the final stage and is about to update region version and manifest.
702    /// It will reject cancellation request after this method is called.
703    ///
704    /// Returns true if this is the first time to mark commit started, false otherwise.
705    pub(crate) fn mark_commit_started(&self) -> bool {
706        let mut commit_started = self.commit_started.lock().unwrap();
707        if self.cancel_handle.is_cancelled() {
708            return false;
709        }
710        *commit_started = true;
711        true
712    }
713
714    /// Request cancellation for this compaction task.
715    pub(crate) fn request_cancel(&self) -> RequestCancelResult {
716        // The cancel handle must under the lock of `commit_started` to avoid racing between cancellation and commit.
717        let commit_started = self.commit_started.lock().unwrap();
718        if *commit_started {
719            return RequestCancelResult::TooLateToCancel;
720        }
721        if self.cancel_handle.is_cancelled() {
722            return RequestCancelResult::AlreadyCancelling;
723        }
724
725        self.cancel_handle.cancel();
726        RequestCancelResult::CancelIssued
727    }
728}
729
730#[derive(Debug, Clone, Copy, PartialEq, Eq)]
731pub(crate) enum RequestCancelResult {
732    CancelIssued,
733    AlreadyCancelling,
734    TooLateToCancel,
735    NotRunning,
736}
737
738impl Drop for CompactionScheduler {
739    fn drop(&mut self) {
740        for (region_id, status) in self.region_status.drain() {
741            // We are shutting down so notify all pending tasks.
742            status.on_failure(Arc::new(RegionClosedSnafu { region_id }.build()));
743        }
744    }
745}
746
747/// Finds compaction options and TTL together with a single metadata fetch to reduce RTT.
748async fn find_dynamic_options(
749    region_id: RegionId,
750    region_options: &crate::region::options::RegionOptions,
751    schema_metadata_manager: &SchemaMetadataManagerRef,
752) -> Result<(crate::region::options::CompactionOptions, TimeToLive)> {
753    let table_id = region_id.table_id();
754    if let (true, Some(ttl)) = (region_options.compaction_override, region_options.ttl) {
755        debug!(
756            "Use region options directly for table {}: compaction={:?}, ttl={:?}",
757            table_id, region_options.compaction, region_options.ttl
758        );
759        return Ok((region_options.compaction.clone(), ttl));
760    }
761
762    let db_options = tokio::time::timeout(
763        crate::config::FETCH_OPTION_TIMEOUT,
764        schema_metadata_manager.get_schema_options_by_table_id(table_id),
765    )
766    .await
767    .context(TimeoutSnafu)?
768    .context(GetSchemaMetadataSnafu)?;
769
770    let ttl = if let Some(ttl) = region_options.ttl {
771        debug!(
772            "Use region TTL directly for table {}: ttl={:?}",
773            table_id, region_options.ttl
774        );
775        ttl
776    } else {
777        db_options
778            .as_ref()
779            .and_then(|options| options.ttl)
780            .unwrap_or_default()
781            .into()
782    };
783
784    let compaction = if !region_options.compaction_override {
785        if let Some(schema_opts) = db_options {
786            let map: HashMap<String, String> = schema_opts
787                .extra_options
788                .iter()
789                .filter_map(|(k, v)| {
790                    if k.starts_with("compaction.") {
791                        Some((k.clone(), v.clone()))
792                    } else {
793                        None
794                    }
795                })
796                .collect();
797            if map.is_empty() {
798                region_options.compaction.clone()
799            } else {
800                crate::region::options::RegionOptions::try_from_options(region_id, &map)
801                    .map(|o| o.compaction)
802                    .unwrap_or_else(|e| {
803                        error!(e; "Failed to create RegionOptions from map");
804                        region_options.compaction.clone()
805                    })
806            }
807        } else {
808            debug!(
809                "DB options is None for table {}, use region compaction: compaction={:?}",
810                table_id, region_options.compaction
811            );
812            region_options.compaction.clone()
813        }
814    } else {
815        debug!(
816            "No schema options for table {}, use region compaction: compaction={:?}",
817            table_id, region_options.compaction
818        );
819        region_options.compaction.clone()
820    };
821
822    debug!(
823        "Resolved dynamic options for table {}: compaction={:?}, ttl={:?}",
824        table_id, compaction, ttl
825    );
826    Ok((compaction, ttl))
827}
828
829/// Status of running and pending region compaction tasks.
830struct CompactionStatus {
831    /// Id of the region.
832    region_id: RegionId,
833    /// Version control of the region.
834    version_control: VersionControlRef,
835    /// Access layer of the region.
836    access_layer: AccessLayerRef,
837    /// Pending waiters for compaction.
838    waiters: Vec<OutputTx>,
839    /// Pending compactions that are supposed to run as soon as current compaction task finished.
840    pending_request: Option<PendingCompaction>,
841    /// Pending DDL requests that should run when compaction is done.
842    pending_ddl_requests: Vec<SenderDdlRequest>,
843    /// Active compaction state.
844    active_compaction: Option<ActiveCompaction>,
845}
846
847impl CompactionStatus {
848    /// Creates a new [CompactionStatus]
849    fn new(
850        region_id: RegionId,
851        version_control: VersionControlRef,
852        access_layer: AccessLayerRef,
853    ) -> CompactionStatus {
854        CompactionStatus {
855            region_id,
856            version_control,
857            access_layer,
858            waiters: Vec::new(),
859            pending_request: None,
860            pending_ddl_requests: Vec::new(),
861            active_compaction: None,
862        }
863    }
864
865    #[cfg(test)]
866    fn start_local_task(&mut self) -> LocalCompactionState {
867        let state = LocalCompactionState::new(Arc::new(CancellationHandle::default()));
868        self.active_compaction = Some(ActiveCompaction::Local {
869            state: state.clone(),
870        });
871        state
872    }
873
874    #[cfg(test)]
875    fn start_remote_task(&mut self) {
876        self.active_compaction = Some(ActiveCompaction::Remote);
877    }
878
879    fn request_cancel(&mut self) -> RequestCancelResult {
880        let Some(active_compaction) = &self.active_compaction else {
881            return RequestCancelResult::NotRunning;
882        };
883
884        match active_compaction {
885            ActiveCompaction::Local { state, .. } => state.request_cancel(),
886            ActiveCompaction::Remote => RequestCancelResult::TooLateToCancel,
887        }
888    }
889
890    fn clear_running_task(&mut self) -> bool {
891        self.active_compaction.take().is_some()
892    }
893
894    /// Merge the waiter to the pending compaction.
895    fn merge_waiter(&mut self, mut waiter: OptionOutputTx) {
896        if let Some(waiter) = waiter.take_inner() {
897            self.waiters.push(waiter);
898        }
899    }
900
901    /// Set pending compaction request or replace current value if already exist.
902    fn set_pending_request(&mut self, pending: PendingCompaction) {
903        if let Some(prev) = self.pending_request.replace(pending) {
904            debug!(
905                "Replace pending compaction options with new request {:?} for region: {}",
906                prev.options, self.region_id
907            );
908            prev.waiter.send(ManualCompactionOverrideSnafu.fail());
909        }
910    }
911
912    fn on_failure(mut self, err: Arc<Error>) {
913        for waiter in self.waiters.drain(..) {
914            waiter.send(Err(err.clone()).context(CompactRegionSnafu {
915                region_id: self.region_id,
916            }));
917        }
918
919        if let Some(pending_compaction) = self.pending_request {
920            pending_compaction
921                .waiter
922                .send(Err(err.clone()).context(CompactRegionSnafu {
923                    region_id: self.region_id,
924                }));
925        }
926
927        for pending_ddl in self.pending_ddl_requests {
928            pending_ddl
929                .sender
930                .send(Err(err.clone()).context(CompactRegionSnafu {
931                    region_id: self.region_id,
932                }));
933        }
934    }
935
936    #[must_use]
937    fn on_cancel(mut self) -> Vec<SenderDdlRequest> {
938        for waiter in self.waiters.drain(..) {
939            waiter.send(CompactionCancelledSnafu.fail());
940        }
941
942        if let Some(pending_compaction) = self.pending_request {
943            pending_compaction.waiter.send(
944                Err(Arc::new(CompactionCancelledSnafu.build())).context(CompactRegionSnafu {
945                    region_id: self.region_id,
946                }),
947            );
948        }
949
950        std::mem::take(&mut self.pending_ddl_requests)
951    }
952
953    /// Creates a new compaction request for compaction picker.
954    ///
955    /// It consumes all pending compaction waiters.
956    #[allow(clippy::too_many_arguments)]
957    fn new_compaction_request(
958        &mut self,
959        request_sender: Sender<WorkerRequestWithTime>,
960        mut waiter: OptionOutputTx,
961        engine_config: Arc<MitoConfig>,
962        cache_manager: CacheManagerRef,
963        manifest_ctx: &ManifestContextRef,
964        listener: WorkerListener,
965        schema_metadata_manager: SchemaMetadataManagerRef,
966        max_parallelism: usize,
967    ) -> CompactionRequest {
968        let current_version = CompactionVersion::from(self.version_control.current().version);
969        let start_time = Instant::now();
970        let mut waiters = Vec::with_capacity(self.waiters.len() + 1);
971        waiters.extend(std::mem::take(&mut self.waiters));
972
973        if let Some(waiter) = waiter.take_inner() {
974            waiters.push(waiter);
975        }
976
977        CompactionRequest {
978            engine_config,
979            current_version,
980            access_layer: self.access_layer.clone(),
981            request_sender: request_sender.clone(),
982            waiters,
983            start_time,
984            cache_manager,
985            manifest_ctx: manifest_ctx.clone(),
986            listener,
987            schema_metadata_manager,
988            max_parallelism,
989        }
990    }
991}
992
993#[derive(Debug, Clone)]
994pub struct CompactionOutput {
995    /// Compaction output file level.
996    pub output_level: Level,
997    /// Compaction input files.
998    pub inputs: Vec<FileHandle>,
999    /// Whether to remove deletion markers.
1000    pub filter_deleted: bool,
1001    /// Compaction output time range. Only windowed compaction specifies output time range.
1002    pub output_time_range: Option<TimestampRange>,
1003}
1004
1005/// SerializedCompactionOutput is a serialized version of [CompactionOutput] by replacing [FileHandle] with [FileMeta].
1006#[derive(Debug, Clone, Serialize, Deserialize)]
1007pub struct SerializedCompactionOutput {
1008    output_level: Level,
1009    inputs: Vec<FileMeta>,
1010    filter_deleted: bool,
1011    output_time_range: Option<TimestampRange>,
1012}
1013
1014/// Builders to create [BoxedRecordBatchStream] for compaction.
1015struct CompactionSstReaderBuilder<'a> {
1016    metadata: RegionMetadataRef,
1017    sst_layer: AccessLayerRef,
1018    cache: CacheManagerRef,
1019    inputs: &'a [FileHandle],
1020    append_mode: bool,
1021    filter_deleted: bool,
1022    time_range: Option<TimestampRange>,
1023    merge_mode: MergeMode,
1024}
1025
1026impl CompactionSstReaderBuilder<'_> {
1027    /// Build a [FlatSource] that yields Arrow `RecordBatch`s from reading all the input SST files,
1028    /// for compaction. The schema of the [FlatSource] is unified.
1029    async fn build_flat_sst_reader(self) -> Result<FlatSource> {
1030        let scan_input = self.build_scan_input().await?.with_compaction(true);
1031
1032        let schema = scan_input.mapper.output_schema();
1033        let schema = schema.arrow_schema();
1034
1035        let stream = SeqScan::new(scan_input)
1036            .build_flat_reader_for_compaction()
1037            .await?;
1038        Ok(FlatSource::new_stream(schema.clone(), stream))
1039    }
1040
1041    async fn build_scan_input(self) -> Result<ScanInput> {
1042        let schema = self.metadata.schema.arrow_schema();
1043        let json_type_hint = if schema.has_json_extension_field() {
1044            let mut json_type_hint = schema
1045                .fields()
1046                .iter()
1047                .filter(|&field| is_json_extension_type(field))
1048                .map(|field| (field.name().clone(), JsonNativeType::Null))
1049                .collect::<HashMap<_, _>>();
1050
1051            let schemas = self.collect_arrow_schemas_from_parquet().await?;
1052            for schema in schemas {
1053                for field in schema.fields() {
1054                    let Some(merged) = json_type_hint.get_mut(field.name()) else {
1055                        continue;
1056                    };
1057
1058                    let json_type = JsonNativeType::try_from(field.data_type())
1059                        .context(DataTypeMismatchSnafu)?;
1060                    merged.merge(&json_type);
1061                }
1062            }
1063
1064            Some(json_type_hint)
1065        } else {
1066            None
1067        };
1068
1069        let projection = (0..self.metadata.column_metadatas.len()).collect();
1070        let read_columns = ReadColumns::from_deduped_column_ids(
1071            self.metadata.column_metadatas.iter().map(|x| x.column_id),
1072        );
1073        let mapper = FlatProjectionMapper::new_with_read_columns(
1074            &self.metadata,
1075            projection,
1076            read_columns,
1077            json_type_hint.as_ref(),
1078        )?;
1079
1080        let mut scan_input = ScanInput::new(self.sst_layer, mapper)
1081            .with_files(self.inputs.to_vec())
1082            .with_append_mode(self.append_mode)
1083            // We use special cache strategy for compaction.
1084            .with_cache(CacheStrategy::Compaction(self.cache))
1085            .with_filter_deleted(self.filter_deleted)
1086            // We ignore file not found error during compaction.
1087            .with_ignore_file_not_found(true)
1088            .with_merge_mode(self.merge_mode);
1089
1090        // This serves as a workaround of https://github.com/GreptimeTeam/greptimedb/issues/3944
1091        // by converting time ranges into predicate.
1092        if let Some(time_range) = self.time_range {
1093            scan_input =
1094                scan_input.with_predicate(time_range_to_predicate(time_range, &self.metadata)?);
1095        }
1096
1097        Ok(scan_input)
1098    }
1099
1100    async fn collect_arrow_schemas_from_parquet(&self) -> Result<Vec<Schema>> {
1101        let mut schemas = Vec::with_capacity(self.inputs.len());
1102
1103        for file_handle in self.inputs {
1104            let file_path =
1105                file_handle.file_path(self.sst_layer.table_dir(), self.sst_layer.path_type());
1106            let file_size = file_handle.meta_ref().file_size;
1107            let parquet_metadata = match self
1108                .sst_layer
1109                .read_sst(file_handle.clone())
1110                .cache(CacheStrategy::Compaction(self.cache.clone()))
1111                .read_parquet_metadata(
1112                    &file_path,
1113                    file_size,
1114                    &mut MetadataCacheMetrics::default(),
1115                    PageIndexPolicy::default(),
1116                )
1117                .await
1118                .map(|x| x.0.parquet_metadata())
1119            {
1120                Ok(x) => x,
1121                Err(e) if e.is_object_not_found() => continue,
1122                Err(e) => return Err(e),
1123            };
1124            let file_metadata = parquet_metadata.file_metadata();
1125
1126            let schema = parquet_to_arrow_schema(
1127                file_metadata.schema_descr(),
1128                file_metadata.key_value_metadata(),
1129            )
1130            .context(ParquetToArrowSchemaSnafu { file: file_path })?;
1131
1132            schemas.push(schema);
1133        }
1134        Ok(schemas)
1135    }
1136}
1137
1138/// Converts time range to predicates so that rows outside the range will be filtered.
1139fn time_range_to_predicate(
1140    range: TimestampRange,
1141    metadata: &RegionMetadataRef,
1142) -> Result<PredicateGroup> {
1143    let ts_col = metadata.time_index_column();
1144
1145    // safety: time index column's type must be a valid timestamp type.
1146    let ts_col_unit = ts_col
1147        .column_schema
1148        .data_type
1149        .as_timestamp()
1150        .unwrap()
1151        .unit();
1152
1153    let exprs = match (range.start(), range.end()) {
1154        (Some(start), Some(end)) => {
1155            vec![
1156                datafusion_expr::col(ts_col.column_schema.name.clone())
1157                    .gt_eq(ts_to_lit(*start, ts_col_unit)?),
1158                datafusion_expr::col(ts_col.column_schema.name.clone())
1159                    .lt(ts_to_lit(*end, ts_col_unit)?),
1160            ]
1161        }
1162        (Some(start), None) => {
1163            vec![
1164                datafusion_expr::col(ts_col.column_schema.name.clone())
1165                    .gt_eq(ts_to_lit(*start, ts_col_unit)?),
1166            ]
1167        }
1168
1169        (None, Some(end)) => {
1170            vec![
1171                datafusion_expr::col(ts_col.column_schema.name.clone())
1172                    .lt(ts_to_lit(*end, ts_col_unit)?),
1173            ]
1174        }
1175        (None, None) => {
1176            return Ok(PredicateGroup::default());
1177        }
1178    };
1179
1180    let predicate = PredicateGroup::new(metadata, &exprs)?;
1181    Ok(predicate)
1182}
1183
1184fn ts_to_lit(ts: Timestamp, ts_col_unit: TimeUnit) -> Result<Expr> {
1185    let ts = ts
1186        .convert_to(ts_col_unit)
1187        .context(TimeRangePredicateOverflowSnafu {
1188            timestamp: ts,
1189            unit: ts_col_unit,
1190        })?;
1191    let val = ts.value();
1192    let scalar_value = match ts_col_unit {
1193        TimeUnit::Second => ScalarValue::TimestampSecond(Some(val), None),
1194        TimeUnit::Millisecond => ScalarValue::TimestampMillisecond(Some(val), None),
1195        TimeUnit::Microsecond => ScalarValue::TimestampMicrosecond(Some(val), None),
1196        TimeUnit::Nanosecond => ScalarValue::TimestampNanosecond(Some(val), None),
1197    };
1198    Ok(datafusion_expr::lit(scalar_value))
1199}
1200
1201/// Finds all expired SSTs across levels.
1202fn get_expired_ssts(
1203    levels: &[LevelMeta],
1204    ttl: Option<TimeToLive>,
1205    now: Timestamp,
1206) -> Vec<FileHandle> {
1207    let Some(ttl) = ttl else {
1208        return vec![];
1209    };
1210
1211    levels
1212        .iter()
1213        .flat_map(|l| l.get_expired_files(&now, &ttl).into_iter())
1214        .collect()
1215}
1216
1217/// Estimates compaction memory as the sum of all input files' maximum row-group
1218/// uncompressed sizes.
1219fn estimate_compaction_bytes(picker_output: &PickerOutput) -> u64 {
1220    picker_output
1221        .outputs
1222        .iter()
1223        .flat_map(|output| output.inputs.iter())
1224        .map(|file: &FileHandle| {
1225            let meta = file.meta_ref();
1226            meta.max_row_group_uncompressed_size
1227        })
1228        .sum()
1229}
1230
1231/// Pending compaction request that is supposed to run after current task is finished,
1232/// typically used for manual compactions.
1233struct PendingCompaction {
1234    /// Compaction options. Currently, it can only be [StrictWindow].
1235    pub(crate) options: compact_request::Options,
1236    /// Waiters of pending requests.
1237    pub(crate) waiter: OptionOutputTx,
1238    /// Max parallelism for pending compaction.
1239    pub(crate) max_parallelism: usize,
1240}
1241
1242#[cfg(test)]
1243mod tests {
1244    use std::assert_matches;
1245    use std::time::Duration;
1246
1247    use api::v1::region::StrictWindow;
1248    use common_datasource::compression::CompressionType;
1249    use common_meta::key::schema_name::SchemaNameValue;
1250    use common_time::DatabaseTimeToLive;
1251    use tokio::sync::{Barrier, oneshot};
1252
1253    use super::*;
1254    use crate::compaction::memory_manager::{CompactionMemoryGuard, new_compaction_memory_manager};
1255    use crate::error::InvalidSchedulerStateSnafu;
1256    use crate::manifest::manager::{RegionManifestManager, RegionManifestOptions};
1257    use crate::region::ManifestContext;
1258    use crate::schedule::scheduler::{Job, Scheduler};
1259    use crate::sst::FormatType;
1260    use crate::test_util::mock_schema_metadata_manager;
1261    use crate::test_util::scheduler_util::{SchedulerEnv, VecScheduler};
1262    use crate::test_util::version_util::{VersionControlBuilder, apply_edit};
1263
1264    struct FailingScheduler;
1265
1266    #[async_trait::async_trait]
1267    impl Scheduler for FailingScheduler {
1268        fn schedule(&self, _job: Job) -> Result<()> {
1269            InvalidSchedulerStateSnafu.fail()
1270        }
1271
1272        async fn stop(&self, _await_termination: bool) -> Result<()> {
1273            Ok(())
1274        }
1275    }
1276
1277    #[tokio::test]
1278    async fn test_find_compaction_options_db_level() {
1279        let env = SchedulerEnv::new().await;
1280        let builder = VersionControlBuilder::new();
1281        let (schema_metadata_manager, kv_backend) = mock_schema_metadata_manager();
1282        let region_id = builder.region_id();
1283        let table_id = region_id.table_id();
1284        // Register table without ttl but with db-level compaction options
1285        let mut schema_value = SchemaNameValue {
1286            ttl: Some(DatabaseTimeToLive::default()),
1287            ..Default::default()
1288        };
1289        schema_value
1290            .extra_options
1291            .insert("compaction.type".to_string(), "twcs".to_string());
1292        schema_value
1293            .extra_options
1294            .insert("compaction.twcs.time_window".to_string(), "2h".to_string());
1295        schema_metadata_manager
1296            .register_region_table_info(
1297                table_id,
1298                "t",
1299                "c",
1300                "s",
1301                Some(schema_value),
1302                kv_backend.clone(),
1303            )
1304            .await;
1305
1306        let version_control = Arc::new(builder.build());
1307        let region_opts = version_control.current().version.options.clone();
1308        let (opts, _) = find_dynamic_options(region_id, &region_opts, &schema_metadata_manager)
1309            .await
1310            .unwrap();
1311        match opts {
1312            crate::region::options::CompactionOptions::Twcs(t) => {
1313                assert_eq!(t.time_window_seconds(), Some(2 * 3600));
1314            }
1315        }
1316        let manifest_ctx = env
1317            .mock_manifest_context(version_control.current().version.metadata.clone())
1318            .await;
1319        let (tx, _rx) = mpsc::channel(4);
1320        let mut scheduler = env.mock_compaction_scheduler(tx);
1321        let (otx, _orx) = oneshot::channel();
1322        let request = scheduler
1323            .region_status
1324            .entry(region_id)
1325            .or_insert_with(|| {
1326                crate::compaction::CompactionStatus::new(
1327                    region_id,
1328                    version_control.clone(),
1329                    env.access_layer.clone(),
1330                )
1331            })
1332            .new_compaction_request(
1333                scheduler.request_sender.clone(),
1334                OptionOutputTx::new(Some(OutputTx::new(otx))),
1335                scheduler.engine_config.clone(),
1336                scheduler.cache_manager.clone(),
1337                &manifest_ctx,
1338                scheduler.listener.clone(),
1339                schema_metadata_manager.clone(),
1340                1,
1341            );
1342        scheduler
1343            .schedule_compaction_request(
1344                request,
1345                compact_request::Options::Regular(Default::default()),
1346            )
1347            .await
1348            .unwrap();
1349    }
1350
1351    #[tokio::test]
1352    async fn test_find_compaction_options_priority() {
1353        fn schema_value_with_twcs(time_window: &str) -> SchemaNameValue {
1354            let mut schema_value = SchemaNameValue {
1355                ttl: Some(DatabaseTimeToLive::default()),
1356                ..Default::default()
1357            };
1358            schema_value
1359                .extra_options
1360                .insert("compaction.type".to_string(), "twcs".to_string());
1361            schema_value.extra_options.insert(
1362                "compaction.twcs.time_window".to_string(),
1363                time_window.to_string(),
1364            );
1365            schema_value
1366        }
1367
1368        let cases = [
1369            (
1370                "db options set and table override set",
1371                Some(schema_value_with_twcs("2h")),
1372                true,
1373                Some(Duration::from_secs(5 * 3600)),
1374                Some(5 * 3600),
1375            ),
1376            (
1377                "db options set and table override not set",
1378                Some(schema_value_with_twcs("2h")),
1379                false,
1380                None,
1381                Some(2 * 3600),
1382            ),
1383            (
1384                "db options not set and table override set",
1385                None,
1386                true,
1387                Some(Duration::from_secs(4 * 3600)),
1388                Some(4 * 3600),
1389            ),
1390            (
1391                "db options not set and table override not set",
1392                None,
1393                false,
1394                None,
1395                None,
1396            ),
1397        ];
1398
1399        for (case_name, schema_value, override_set, table_window, expected_window) in cases {
1400            let builder = VersionControlBuilder::new();
1401            let (schema_metadata_manager, kv_backend) = mock_schema_metadata_manager();
1402            let region_id = builder.region_id();
1403            let table_id = region_id.table_id();
1404            schema_metadata_manager
1405                .register_region_table_info(
1406                    table_id,
1407                    "t",
1408                    "c",
1409                    "s",
1410                    schema_value,
1411                    kv_backend.clone(),
1412                )
1413                .await;
1414
1415            let version_control = Arc::new(builder.build());
1416            let mut region_opts = version_control.current().version.options.clone();
1417            region_opts.compaction_override = override_set;
1418            if let Some(window) = table_window {
1419                let crate::region::options::CompactionOptions::Twcs(twcs) =
1420                    &mut region_opts.compaction;
1421                twcs.time_window = Some(window);
1422            }
1423
1424            let (opts, _) = find_dynamic_options(region_id, &region_opts, &schema_metadata_manager)
1425                .await
1426                .unwrap();
1427            match opts {
1428                crate::region::options::CompactionOptions::Twcs(t) => {
1429                    assert_eq!(t.time_window_seconds(), expected_window, "{case_name}");
1430                }
1431            }
1432        }
1433    }
1434
1435    #[tokio::test]
1436    async fn test_schedule_empty() {
1437        let env = SchedulerEnv::new().await;
1438        let (tx, _rx) = mpsc::channel(4);
1439        let mut scheduler = env.mock_compaction_scheduler(tx);
1440        let mut builder = VersionControlBuilder::new();
1441        let (schema_metadata_manager, kv_backend) = mock_schema_metadata_manager();
1442        schema_metadata_manager
1443            .register_region_table_info(
1444                builder.region_id().table_id(),
1445                "test_table",
1446                "test_catalog",
1447                "test_schema",
1448                None,
1449                kv_backend,
1450            )
1451            .await;
1452        // Nothing to compact.
1453        let version_control = Arc::new(builder.build());
1454        let (output_tx, output_rx) = oneshot::channel();
1455        let waiter = OptionOutputTx::from(output_tx);
1456        let manifest_ctx = env
1457            .mock_manifest_context(version_control.current().version.metadata.clone())
1458            .await;
1459        let scheduled = scheduler
1460            .schedule_compaction(
1461                builder.region_id(),
1462                compact_request::Options::Regular(Default::default()),
1463                &version_control,
1464                &env.access_layer,
1465                waiter,
1466                &manifest_ctx,
1467                schema_metadata_manager.clone(),
1468                1,
1469            )
1470            .await
1471            .unwrap();
1472        assert!(!scheduled);
1473        let output = output_rx.await.unwrap().unwrap();
1474        assert_eq!(output, 0);
1475        assert!(scheduler.region_status.is_empty());
1476
1477        // Only one file, picker won't compact it.
1478        let version_control = Arc::new(builder.push_l0_file(0, 1000).build());
1479        let (output_tx, output_rx) = oneshot::channel();
1480        let waiter = OptionOutputTx::from(output_tx);
1481        let scheduled = scheduler
1482            .schedule_compaction(
1483                builder.region_id(),
1484                compact_request::Options::Regular(Default::default()),
1485                &version_control,
1486                &env.access_layer,
1487                waiter,
1488                &manifest_ctx,
1489                schema_metadata_manager,
1490                1,
1491            )
1492            .await
1493            .unwrap();
1494        assert!(!scheduled);
1495        let output = output_rx.await.unwrap().unwrap();
1496        assert_eq!(output, 0);
1497        assert!(scheduler.region_status.is_empty());
1498    }
1499
1500    #[tokio::test]
1501    async fn test_schedule_compaction_returns_true_when_task_scheduled() {
1502        let job_scheduler = Arc::new(VecScheduler::default());
1503        let env = SchedulerEnv::new().await.scheduler(job_scheduler.clone());
1504        let (tx, _rx) = mpsc::channel(4);
1505        let mut scheduler = env.mock_compaction_scheduler(tx);
1506        let mut builder = VersionControlBuilder::new();
1507        let region_id = builder.region_id();
1508        let end = 1000 * 1000;
1509        // Five overlapping L0 files are enough for the regular picker to create a task.
1510        let version_control = Arc::new(
1511            builder
1512                .push_l0_file(0, end)
1513                .push_l0_file(10, end)
1514                .push_l0_file(50, end)
1515                .push_l0_file(80, end)
1516                .push_l0_file(90, end)
1517                .build(),
1518        );
1519        let manifest_ctx = env
1520            .mock_manifest_context(version_control.current().version.metadata.clone())
1521            .await;
1522        let (schema_metadata_manager, kv_backend) = mock_schema_metadata_manager();
1523        schema_metadata_manager
1524            .register_region_table_info(
1525                region_id.table_id(),
1526                "test_table",
1527                "test_catalog",
1528                "test_schema",
1529                None,
1530                kv_backend,
1531            )
1532            .await;
1533
1534        let scheduled = scheduler
1535            .schedule_compaction(
1536                region_id,
1537                Options::Regular(Default::default()),
1538                &version_control,
1539                &env.access_layer,
1540                OptionOutputTx::none(),
1541                &manifest_ctx,
1542                schema_metadata_manager,
1543                1,
1544            )
1545            .await
1546            .unwrap();
1547
1548        // The boolean result is what the worker uses to decide whether to update
1549        // last_schedule_compaction_millis.
1550        assert!(scheduled);
1551        assert_eq!(1, job_scheduler.num_jobs());
1552        assert!(scheduler.region_status.contains_key(&region_id));
1553    }
1554
1555    #[tokio::test]
1556    async fn test_schedule_on_finished() {
1557        common_telemetry::init_default_ut_logging();
1558        let job_scheduler = Arc::new(VecScheduler::default());
1559        let env = SchedulerEnv::new().await.scheduler(job_scheduler.clone());
1560        let (tx, _rx) = mpsc::channel(4);
1561        let mut scheduler = env.mock_compaction_scheduler(tx);
1562        let mut builder = VersionControlBuilder::new();
1563        let purger = builder.file_purger();
1564        let region_id = builder.region_id();
1565
1566        let (schema_metadata_manager, kv_backend) = mock_schema_metadata_manager();
1567        schema_metadata_manager
1568            .register_region_table_info(
1569                builder.region_id().table_id(),
1570                "test_table",
1571                "test_catalog",
1572                "test_schema",
1573                None,
1574                kv_backend,
1575            )
1576            .await;
1577
1578        // 5 files to compact.
1579        let end = 1000 * 1000;
1580        let version_control = Arc::new(
1581            builder
1582                .push_l0_file(0, end)
1583                .push_l0_file(10, end)
1584                .push_l0_file(50, end)
1585                .push_l0_file(80, end)
1586                .push_l0_file(90, end)
1587                .build(),
1588        );
1589        let manifest_ctx = env
1590            .mock_manifest_context(version_control.current().version.metadata.clone())
1591            .await;
1592        let scheduled = scheduler
1593            .schedule_compaction(
1594                region_id,
1595                compact_request::Options::Regular(Default::default()),
1596                &version_control,
1597                &env.access_layer,
1598                OptionOutputTx::none(),
1599                &manifest_ctx,
1600                schema_metadata_manager.clone(),
1601                1,
1602            )
1603            .await
1604            .unwrap();
1605        // Should schedule 1 compaction.
1606        assert!(scheduled);
1607        assert_eq!(1, scheduler.region_status.len());
1608        assert_eq!(1, job_scheduler.num_jobs());
1609        let data = version_control.current();
1610        let file_metas: Vec<_> = data.version.ssts.levels()[0]
1611            .files
1612            .values()
1613            .map(|file| file.meta_ref().clone())
1614            .collect();
1615
1616        // 5 files for next compaction and removes old files.
1617        apply_edit(
1618            &version_control,
1619            &[(0, end), (20, end), (40, end), (60, end), (80, end)],
1620            &file_metas,
1621            purger.clone(),
1622        );
1623        // The task is pending.
1624        let (tx, _rx) = oneshot::channel();
1625        let scheduled = scheduler
1626            .schedule_compaction(
1627                region_id,
1628                compact_request::Options::Regular(Default::default()),
1629                &version_control,
1630                &env.access_layer,
1631                OptionOutputTx::new(Some(OutputTx::new(tx))),
1632                &manifest_ctx,
1633                schema_metadata_manager.clone(),
1634                1,
1635            )
1636            .await
1637            .unwrap();
1638        assert!(!scheduled);
1639        assert_eq!(1, scheduler.region_status.len());
1640        assert_eq!(1, job_scheduler.num_jobs());
1641        assert!(
1642            !scheduler
1643                .region_status
1644                .get(&builder.region_id())
1645                .unwrap()
1646                .waiters
1647                .is_empty()
1648        );
1649
1650        // On compaction finished and schedule next compaction.
1651        scheduler
1652            .on_compaction_finished(region_id, &manifest_ctx, schema_metadata_manager.clone())
1653            .await;
1654        let scheduled = scheduler
1655            .schedule_next_compaction(region_id, &manifest_ctx, schema_metadata_manager.clone())
1656            .await;
1657        assert!(scheduled);
1658        assert_eq!(1, scheduler.region_status.len());
1659        assert_eq!(2, job_scheduler.num_jobs());
1660
1661        // 5 files for next compaction.
1662        apply_edit(
1663            &version_control,
1664            &[(0, end), (20, end), (40, end), (60, end), (80, end)],
1665            &[],
1666            purger.clone(),
1667        );
1668        let (tx, _rx) = oneshot::channel();
1669        // The task is pending.
1670        let scheduled = scheduler
1671            .schedule_compaction(
1672                region_id,
1673                compact_request::Options::Regular(Default::default()),
1674                &version_control,
1675                &env.access_layer,
1676                OptionOutputTx::new(Some(OutputTx::new(tx))),
1677                &manifest_ctx,
1678                schema_metadata_manager,
1679                1,
1680            )
1681            .await
1682            .unwrap();
1683        assert!(!scheduled);
1684        assert_eq!(2, job_scheduler.num_jobs());
1685        assert!(
1686            !scheduler
1687                .region_status
1688                .get(&builder.region_id())
1689                .unwrap()
1690                .waiters
1691                .is_empty()
1692        );
1693    }
1694
1695    #[tokio::test]
1696    async fn test_schedule_compaction_does_not_publish_status_when_schedule_fails() {
1697        common_telemetry::init_default_ut_logging();
1698        let env = SchedulerEnv::new()
1699            .await
1700            .scheduler(Arc::new(FailingScheduler));
1701        let (tx, _rx) = mpsc::channel(4);
1702        let mut scheduler = env.mock_compaction_scheduler(tx);
1703        let mut builder = VersionControlBuilder::new();
1704        let end = 1000 * 1000;
1705        let version_control = Arc::new(
1706            builder
1707                .push_l0_file(0, end)
1708                .push_l0_file(10, end)
1709                .push_l0_file(50, end)
1710                .push_l0_file(80, end)
1711                .push_l0_file(90, end)
1712                .build(),
1713        );
1714        let region_id = builder.region_id();
1715        let manifest_ctx = env
1716            .mock_manifest_context(version_control.current().version.metadata.clone())
1717            .await;
1718        let (schema_metadata_manager, kv_backend) = mock_schema_metadata_manager();
1719        schema_metadata_manager
1720            .register_region_table_info(
1721                builder.region_id().table_id(),
1722                "test_table",
1723                "test_catalog",
1724                "test_schema",
1725                None,
1726                kv_backend,
1727            )
1728            .await;
1729
1730        let result = scheduler
1731            .schedule_compaction(
1732                region_id,
1733                compact_request::Options::Regular(Default::default()),
1734                &version_control,
1735                &env.access_layer,
1736                OptionOutputTx::none(),
1737                &manifest_ctx,
1738                schema_metadata_manager,
1739                1,
1740            )
1741            .await;
1742
1743        assert!(result.is_err());
1744        assert!(!scheduler.region_status.contains_key(&region_id));
1745    }
1746
1747    #[tokio::test]
1748    async fn test_manual_compaction_when_compaction_in_progress() {
1749        common_telemetry::init_default_ut_logging();
1750        let job_scheduler = Arc::new(VecScheduler::default());
1751        let env = SchedulerEnv::new().await.scheduler(job_scheduler.clone());
1752        let (tx, _rx) = mpsc::channel(4);
1753        let mut scheduler = env.mock_compaction_scheduler(tx);
1754        let mut builder = VersionControlBuilder::new();
1755        let purger = builder.file_purger();
1756        let region_id = builder.region_id();
1757
1758        let (schema_metadata_manager, kv_backend) = mock_schema_metadata_manager();
1759        schema_metadata_manager
1760            .register_region_table_info(
1761                builder.region_id().table_id(),
1762                "test_table",
1763                "test_catalog",
1764                "test_schema",
1765                None,
1766                kv_backend,
1767            )
1768            .await;
1769
1770        // 5 files to compact.
1771        let end = 1000 * 1000;
1772        let version_control = Arc::new(
1773            builder
1774                .push_l0_file(0, end)
1775                .push_l0_file(10, end)
1776                .push_l0_file(50, end)
1777                .push_l0_file(80, end)
1778                .push_l0_file(90, end)
1779                .build(),
1780        );
1781        let manifest_ctx = env
1782            .mock_manifest_context(version_control.current().version.metadata.clone())
1783            .await;
1784
1785        let file_metas: Vec<_> = version_control.current().version.ssts.levels()[0]
1786            .files
1787            .values()
1788            .map(|file| file.meta_ref().clone())
1789            .collect();
1790
1791        // 5 files for next compaction and removes old files.
1792        apply_edit(
1793            &version_control,
1794            &[(0, end), (20, end), (40, end), (60, end), (80, end)],
1795            &file_metas,
1796            purger.clone(),
1797        );
1798
1799        scheduler
1800            .schedule_compaction(
1801                region_id,
1802                compact_request::Options::Regular(Default::default()),
1803                &version_control,
1804                &env.access_layer,
1805                OptionOutputTx::none(),
1806                &manifest_ctx,
1807                schema_metadata_manager.clone(),
1808                1,
1809            )
1810            .await
1811            .unwrap();
1812        // Should schedule 1 compaction.
1813        assert_eq!(1, scheduler.region_status.len());
1814        assert_eq!(1, job_scheduler.num_jobs());
1815        assert!(
1816            scheduler
1817                .region_status
1818                .get(&region_id)
1819                .unwrap()
1820                .pending_request
1821                .is_none()
1822        );
1823
1824        // Schedule another manual compaction.
1825        let (tx, _rx) = oneshot::channel();
1826        scheduler
1827            .schedule_compaction(
1828                region_id,
1829                compact_request::Options::StrictWindow(StrictWindow { window_seconds: 60 }),
1830                &version_control,
1831                &env.access_layer,
1832                OptionOutputTx::new(Some(OutputTx::new(tx))),
1833                &manifest_ctx,
1834                schema_metadata_manager.clone(),
1835                1,
1836            )
1837            .await
1838            .unwrap();
1839        assert_eq!(1, scheduler.region_status.len());
1840        // Current job num should be 1 since compaction is in progress.
1841        assert_eq!(1, job_scheduler.num_jobs());
1842        let status = scheduler.region_status.get(&builder.region_id()).unwrap();
1843        assert!(status.pending_request.is_some());
1844
1845        // On compaction finished and schedule next compaction.
1846        scheduler
1847            .on_compaction_finished(region_id, &manifest_ctx, schema_metadata_manager.clone())
1848            .await;
1849        assert_eq!(1, scheduler.region_status.len());
1850        assert_eq!(2, job_scheduler.num_jobs());
1851
1852        let status = scheduler.region_status.get(&builder.region_id()).unwrap();
1853        assert!(status.pending_request.is_none());
1854    }
1855
1856    #[tokio::test]
1857    async fn test_compaction_bypass_in_staging_mode() {
1858        let env = SchedulerEnv::new().await;
1859        let (tx, _rx) = mpsc::channel(4);
1860        let mut scheduler = env.mock_compaction_scheduler(tx);
1861
1862        // Create version control and manifest context for staging mode
1863        let builder = VersionControlBuilder::new();
1864        let version_control = Arc::new(builder.build());
1865        let region_id = version_control.current().version.metadata.region_id;
1866
1867        // Create staging manifest context using the same pattern as SchedulerEnv
1868        let staging_manifest_ctx = {
1869            let manager = RegionManifestManager::new(
1870                version_control.current().version.metadata.clone(),
1871                0,
1872                RegionManifestOptions {
1873                    manifest_dir: "".to_string(),
1874                    object_store: env.access_layer.object_store().clone(),
1875                    compress_type: CompressionType::Uncompressed,
1876                    checkpoint_distance: 10,
1877                    remove_file_options: Default::default(),
1878                    manifest_cache: None,
1879                },
1880                FormatType::PrimaryKey,
1881                &Default::default(),
1882            )
1883            .await
1884            .unwrap();
1885            Arc::new(ManifestContext::new(
1886                manager,
1887                RegionRoleState::Leader(RegionLeaderState::Staging),
1888                None,
1889            ))
1890        };
1891
1892        let (schema_metadata_manager, _kv_backend) = mock_schema_metadata_manager();
1893
1894        // Test regular compaction bypass in staging mode
1895        let (tx, rx) = oneshot::channel();
1896        scheduler
1897            .schedule_compaction(
1898                region_id,
1899                compact_request::Options::Regular(Default::default()),
1900                &version_control,
1901                &env.access_layer,
1902                OptionOutputTx::new(Some(OutputTx::new(tx))),
1903                &staging_manifest_ctx,
1904                schema_metadata_manager,
1905                1,
1906            )
1907            .await
1908            .unwrap();
1909
1910        let result = rx.await.unwrap();
1911        assert_eq!(result.unwrap(), 0); // is there a better way to check this?
1912        assert_eq!(0, scheduler.region_status.len());
1913    }
1914
1915    #[tokio::test]
1916    async fn test_add_ddl_request_to_pending() {
1917        let env = SchedulerEnv::new().await;
1918        let (tx, _rx) = mpsc::channel(4);
1919        let mut scheduler = env.mock_compaction_scheduler(tx);
1920        let builder = VersionControlBuilder::new();
1921        let version_control = Arc::new(builder.build());
1922        let region_id = builder.region_id();
1923
1924        scheduler.region_status.insert(
1925            region_id,
1926            CompactionStatus::new(region_id, version_control, env.access_layer.clone()),
1927        );
1928        scheduler
1929            .region_status
1930            .get_mut(&region_id)
1931            .unwrap()
1932            .start_local_task();
1933
1934        let (output_tx, _output_rx) = oneshot::channel();
1935        scheduler.add_ddl_request_to_pending(SenderDdlRequest {
1936            region_id,
1937            sender: OptionOutputTx::from(output_tx),
1938            request: crate::request::DdlRequest::EnterStaging(
1939                store_api::region_request::EnterStagingRequest {
1940                    partition_directive:
1941                        store_api::region_request::StagingPartitionDirective::RejectAllWrites,
1942                },
1943            ),
1944        });
1945
1946        assert!(scheduler.has_pending_ddls(region_id));
1947    }
1948
1949    #[tokio::test]
1950    async fn test_request_cancel_state_transitions() {
1951        let env = SchedulerEnv::new().await;
1952        let builder = VersionControlBuilder::new();
1953        let region_id = builder.region_id();
1954        let version_control = Arc::new(builder.build());
1955        let mut status =
1956            CompactionStatus::new(region_id, version_control, env.access_layer.clone());
1957        let state = status.start_local_task();
1958
1959        assert_eq!(status.request_cancel(), RequestCancelResult::CancelIssued);
1960        assert!(state.cancel_handle().is_cancelled());
1961        assert_eq!(
1962            status.request_cancel(),
1963            RequestCancelResult::AlreadyCancelling
1964        );
1965
1966        assert!(!state.mark_commit_started());
1967        assert_eq!(
1968            status.request_cancel(),
1969            RequestCancelResult::AlreadyCancelling
1970        );
1971
1972        assert!(status.clear_running_task());
1973        assert_eq!(status.request_cancel(), RequestCancelResult::NotRunning);
1974    }
1975
1976    #[tokio::test]
1977    async fn test_request_cancel_remote_compaction_is_too_late() {
1978        let env = SchedulerEnv::new().await;
1979        let builder = VersionControlBuilder::new();
1980        let region_id = builder.region_id();
1981        let version_control = Arc::new(builder.build());
1982        let mut status =
1983            CompactionStatus::new(region_id, version_control, env.access_layer.clone());
1984
1985        status.start_remote_task();
1986
1987        assert_eq!(
1988            status.request_cancel(),
1989            RequestCancelResult::TooLateToCancel
1990        );
1991        assert!(status.active_compaction.is_some());
1992    }
1993
1994    #[tokio::test]
1995    async fn test_on_compaction_cancelled_returns_pending_ddl_requests() {
1996        let job_scheduler = Arc::new(VecScheduler::default());
1997        let env = SchedulerEnv::new().await.scheduler(job_scheduler.clone());
1998        let (tx, _rx) = mpsc::channel(4);
1999        let mut scheduler = env.mock_compaction_scheduler(tx);
2000        let builder = VersionControlBuilder::new();
2001        let version_control = Arc::new(builder.build());
2002        let region_id = builder.region_id();
2003        let _manifest_ctx = env
2004            .mock_manifest_context(version_control.current().version.metadata.clone())
2005            .await;
2006        let (_schema_metadata_manager, _kv_backend) = mock_schema_metadata_manager();
2007
2008        scheduler.region_status.insert(
2009            region_id,
2010            CompactionStatus::new(region_id, version_control, env.access_layer.clone()),
2011        );
2012        scheduler
2013            .region_status
2014            .get_mut(&region_id)
2015            .unwrap()
2016            .start_local_task();
2017
2018        let (output_tx, _output_rx) = oneshot::channel();
2019        scheduler.add_ddl_request_to_pending(SenderDdlRequest {
2020            region_id,
2021            sender: OptionOutputTx::from(output_tx),
2022            request: crate::request::DdlRequest::EnterStaging(
2023                store_api::region_request::EnterStagingRequest {
2024                    partition_directive:
2025                        store_api::region_request::StagingPartitionDirective::RejectAllWrites,
2026                },
2027            ),
2028        });
2029
2030        let pending_ddls = scheduler.on_compaction_cancelled(region_id).await;
2031
2032        assert_eq!(pending_ddls.len(), 1);
2033        assert!(!scheduler.has_pending_ddls(region_id));
2034        assert!(!scheduler.region_status.contains_key(&region_id));
2035        assert_eq!(job_scheduler.num_jobs(), 0);
2036    }
2037
2038    #[tokio::test]
2039    async fn test_on_compaction_cancelled_prioritizes_pending_ddls_over_pending_compaction() {
2040        let job_scheduler = Arc::new(VecScheduler::default());
2041        let env = SchedulerEnv::new().await.scheduler(job_scheduler.clone());
2042        let (tx, _rx) = mpsc::channel(4);
2043        let mut scheduler = env.mock_compaction_scheduler(tx);
2044        let builder = VersionControlBuilder::new();
2045        let version_control = Arc::new(builder.build());
2046        let region_id = builder.region_id();
2047        let _manifest_ctx = env
2048            .mock_manifest_context(version_control.current().version.metadata.clone())
2049            .await;
2050        let (_schema_metadata_manager, _kv_backend) = mock_schema_metadata_manager();
2051
2052        scheduler.region_status.insert(
2053            region_id,
2054            CompactionStatus::new(region_id, version_control, env.access_layer.clone()),
2055        );
2056        let status = scheduler.region_status.get_mut(&region_id).unwrap();
2057        status.start_local_task();
2058        let (manual_tx, manual_rx) = oneshot::channel();
2059        status.set_pending_request(PendingCompaction {
2060            options: compact_request::Options::StrictWindow(StrictWindow { window_seconds: 60 }),
2061            waiter: OptionOutputTx::from(manual_tx),
2062            max_parallelism: 1,
2063        });
2064
2065        let (output_tx, _output_rx) = oneshot::channel();
2066        scheduler.add_ddl_request_to_pending(SenderDdlRequest {
2067            region_id,
2068            sender: OptionOutputTx::from(output_tx),
2069            request: crate::request::DdlRequest::EnterStaging(
2070                store_api::region_request::EnterStagingRequest {
2071                    partition_directive:
2072                        store_api::region_request::StagingPartitionDirective::RejectAllWrites,
2073                },
2074            ),
2075        });
2076
2077        let pending_ddls = scheduler.on_compaction_cancelled(region_id).await;
2078
2079        assert_eq!(pending_ddls.len(), 1);
2080        assert!(!scheduler.region_status.contains_key(&region_id));
2081        assert_eq!(job_scheduler.num_jobs(), 0);
2082        assert_matches!(manual_rx.await.unwrap(), Err(_));
2083    }
2084
2085    #[tokio::test]
2086    async fn test_pending_ddl_request_failed_on_compaction_failed() {
2087        let env = SchedulerEnv::new().await;
2088        let (tx, _rx) = mpsc::channel(4);
2089        let mut scheduler = env.mock_compaction_scheduler(tx);
2090        let builder = VersionControlBuilder::new();
2091        let version_control = Arc::new(builder.build());
2092        let region_id = builder.region_id();
2093
2094        scheduler.region_status.insert(
2095            region_id,
2096            CompactionStatus::new(region_id, version_control, env.access_layer.clone()),
2097        );
2098
2099        let (output_tx, output_rx) = oneshot::channel();
2100        scheduler.add_ddl_request_to_pending(SenderDdlRequest {
2101            region_id,
2102            sender: OptionOutputTx::from(output_tx),
2103            request: crate::request::DdlRequest::EnterStaging(
2104                store_api::region_request::EnterStagingRequest {
2105                    partition_directive:
2106                        store_api::region_request::StagingPartitionDirective::RejectAllWrites,
2107                },
2108            ),
2109        });
2110
2111        assert!(scheduler.has_pending_ddls(region_id));
2112        scheduler
2113            .on_compaction_failed(region_id, Arc::new(RegionClosedSnafu { region_id }.build()));
2114
2115        assert!(!scheduler.has_pending_ddls(region_id));
2116        let result = output_rx.await.unwrap();
2117        assert_matches!(result, Err(_));
2118    }
2119
2120    #[tokio::test]
2121    async fn test_pending_ddl_request_failed_on_region_closed() {
2122        let env = SchedulerEnv::new().await;
2123        let (tx, _rx) = mpsc::channel(4);
2124        let mut scheduler = env.mock_compaction_scheduler(tx);
2125        let builder = VersionControlBuilder::new();
2126        let version_control = Arc::new(builder.build());
2127        let region_id = builder.region_id();
2128
2129        scheduler.region_status.insert(
2130            region_id,
2131            CompactionStatus::new(region_id, version_control, env.access_layer.clone()),
2132        );
2133
2134        let (output_tx, output_rx) = oneshot::channel();
2135        scheduler.add_ddl_request_to_pending(SenderDdlRequest {
2136            region_id,
2137            sender: OptionOutputTx::from(output_tx),
2138            request: crate::request::DdlRequest::EnterStaging(
2139                store_api::region_request::EnterStagingRequest {
2140                    partition_directive:
2141                        store_api::region_request::StagingPartitionDirective::RejectAllWrites,
2142                },
2143            ),
2144        });
2145
2146        assert!(scheduler.has_pending_ddls(region_id));
2147        scheduler.on_region_closed(region_id);
2148
2149        assert!(!scheduler.has_pending_ddls(region_id));
2150        let result = output_rx.await.unwrap();
2151        assert_matches!(result, Err(_));
2152    }
2153
2154    #[tokio::test]
2155    async fn test_pending_ddl_request_failed_on_region_dropped() {
2156        let env = SchedulerEnv::new().await;
2157        let (tx, _rx) = mpsc::channel(4);
2158        let mut scheduler = env.mock_compaction_scheduler(tx);
2159        let builder = VersionControlBuilder::new();
2160        let version_control = Arc::new(builder.build());
2161        let region_id = builder.region_id();
2162
2163        scheduler.region_status.insert(
2164            region_id,
2165            CompactionStatus::new(region_id, version_control, env.access_layer.clone()),
2166        );
2167
2168        let (output_tx, output_rx) = oneshot::channel();
2169        scheduler.add_ddl_request_to_pending(SenderDdlRequest {
2170            region_id,
2171            sender: OptionOutputTx::from(output_tx),
2172            request: crate::request::DdlRequest::EnterStaging(
2173                store_api::region_request::EnterStagingRequest {
2174                    partition_directive:
2175                        store_api::region_request::StagingPartitionDirective::RejectAllWrites,
2176                },
2177            ),
2178        });
2179
2180        assert!(scheduler.has_pending_ddls(region_id));
2181        scheduler.on_region_dropped(region_id);
2182
2183        assert!(!scheduler.has_pending_ddls(region_id));
2184        let result = output_rx.await.unwrap();
2185        assert_matches!(result, Err(_));
2186    }
2187
2188    #[tokio::test]
2189    async fn test_pending_ddl_request_failed_on_region_truncated() {
2190        let env = SchedulerEnv::new().await;
2191        let (tx, _rx) = mpsc::channel(4);
2192        let mut scheduler = env.mock_compaction_scheduler(tx);
2193        let builder = VersionControlBuilder::new();
2194        let version_control = Arc::new(builder.build());
2195        let region_id = builder.region_id();
2196
2197        scheduler.region_status.insert(
2198            region_id,
2199            CompactionStatus::new(region_id, version_control, env.access_layer.clone()),
2200        );
2201
2202        let (output_tx, output_rx) = oneshot::channel();
2203        scheduler.add_ddl_request_to_pending(SenderDdlRequest {
2204            region_id,
2205            sender: OptionOutputTx::from(output_tx),
2206            request: crate::request::DdlRequest::EnterStaging(
2207                store_api::region_request::EnterStagingRequest {
2208                    partition_directive:
2209                        store_api::region_request::StagingPartitionDirective::RejectAllWrites,
2210                },
2211            ),
2212        });
2213
2214        assert!(scheduler.has_pending_ddls(region_id));
2215        scheduler.on_region_truncated(region_id);
2216
2217        assert!(!scheduler.has_pending_ddls(region_id));
2218        let result = output_rx.await.unwrap();
2219        assert_matches!(result, Err(_));
2220    }
2221
2222    #[tokio::test]
2223    async fn test_on_compaction_finished_returns_pending_ddl_requests() {
2224        let job_scheduler = Arc::new(VecScheduler::default());
2225        let env = SchedulerEnv::new().await.scheduler(job_scheduler.clone());
2226        let (tx, _rx) = mpsc::channel(4);
2227        let mut scheduler = env.mock_compaction_scheduler(tx);
2228        let builder = VersionControlBuilder::new();
2229        let version_control = Arc::new(builder.build());
2230        let region_id = builder.region_id();
2231        let manifest_ctx = env
2232            .mock_manifest_context(version_control.current().version.metadata.clone())
2233            .await;
2234        let (schema_metadata_manager, _kv_backend) = mock_schema_metadata_manager();
2235
2236        scheduler.region_status.insert(
2237            region_id,
2238            CompactionStatus::new(region_id, version_control, env.access_layer.clone()),
2239        );
2240        scheduler
2241            .region_status
2242            .get_mut(&region_id)
2243            .unwrap()
2244            .start_local_task();
2245
2246        let (output_tx, _output_rx) = oneshot::channel();
2247        scheduler.add_ddl_request_to_pending(SenderDdlRequest {
2248            region_id,
2249            sender: OptionOutputTx::from(output_tx),
2250            request: crate::request::DdlRequest::EnterStaging(
2251                store_api::region_request::EnterStagingRequest {
2252                    partition_directive:
2253                        store_api::region_request::StagingPartitionDirective::RejectAllWrites,
2254                },
2255            ),
2256        });
2257
2258        let pending_ddls = scheduler
2259            .on_compaction_finished(region_id, &manifest_ctx, schema_metadata_manager)
2260            .await;
2261
2262        assert_eq!(pending_ddls.len(), 1);
2263        assert!(!scheduler.has_pending_ddls(region_id));
2264        assert!(!scheduler.region_status.contains_key(&region_id));
2265        assert_eq!(job_scheduler.num_jobs(), 0);
2266    }
2267
2268    #[tokio::test]
2269    async fn test_on_compaction_finished_replays_pending_ddl_after_manual_noop() {
2270        let env = SchedulerEnv::new().await;
2271        let (tx, _rx) = mpsc::channel(4);
2272        let mut scheduler = env.mock_compaction_scheduler(tx);
2273        let builder = VersionControlBuilder::new();
2274        let version_control = Arc::new(builder.build());
2275        let region_id = builder.region_id();
2276        let manifest_ctx = env
2277            .mock_manifest_context(version_control.current().version.metadata.clone())
2278            .await;
2279        let (schema_metadata_manager, _kv_backend) = mock_schema_metadata_manager();
2280
2281        let (manual_tx, manual_rx) = oneshot::channel();
2282        let mut status =
2283            CompactionStatus::new(region_id, version_control.clone(), env.access_layer.clone());
2284        status.start_local_task();
2285        status.set_pending_request(PendingCompaction {
2286            options: compact_request::Options::Regular(Default::default()),
2287            waiter: OptionOutputTx::from(manual_tx),
2288            max_parallelism: 1,
2289        });
2290        scheduler.region_status.insert(region_id, status);
2291
2292        let (ddl_tx, _ddl_rx) = oneshot::channel();
2293        scheduler.add_ddl_request_to_pending(SenderDdlRequest {
2294            region_id,
2295            sender: OptionOutputTx::from(ddl_tx),
2296            request: crate::request::DdlRequest::EnterStaging(
2297                store_api::region_request::EnterStagingRequest {
2298                    partition_directive:
2299                        store_api::region_request::StagingPartitionDirective::RejectAllWrites,
2300                },
2301            ),
2302        });
2303
2304        let pending_ddls = scheduler
2305            .on_compaction_finished(region_id, &manifest_ctx, schema_metadata_manager)
2306            .await;
2307
2308        assert_eq!(pending_ddls.len(), 1);
2309        assert!(!scheduler.region_status.contains_key(&region_id));
2310        assert_eq!(manual_rx.await.unwrap().unwrap(), 0);
2311    }
2312
2313    #[tokio::test]
2314    async fn test_on_compaction_finished_returns_empty_when_region_absent() {
2315        let env = SchedulerEnv::new().await;
2316        let (tx, _rx) = mpsc::channel(4);
2317        let mut scheduler = env.mock_compaction_scheduler(tx);
2318        let builder = VersionControlBuilder::new();
2319        let region_id = builder.region_id();
2320        let version_control = Arc::new(builder.build());
2321        let manifest_ctx = env
2322            .mock_manifest_context(version_control.current().version.metadata.clone())
2323            .await;
2324        let (schema_metadata_manager, _kv_backend) = mock_schema_metadata_manager();
2325
2326        let pending_ddls = scheduler
2327            .on_compaction_finished(region_id, &manifest_ctx, schema_metadata_manager)
2328            .await;
2329
2330        assert!(pending_ddls.is_empty());
2331    }
2332
2333    #[tokio::test]
2334    async fn test_on_compaction_finished_manual_schedule_error_cleans_status() {
2335        let env = SchedulerEnv::new()
2336            .await
2337            .scheduler(Arc::new(FailingScheduler));
2338        let (tx, _rx) = mpsc::channel(4);
2339        let mut scheduler = env.mock_compaction_scheduler(tx);
2340        let mut builder = VersionControlBuilder::new();
2341        let end = 1000 * 1000;
2342        let version_control = Arc::new(
2343            builder
2344                .push_l0_file(0, end)
2345                .push_l0_file(10, end)
2346                .push_l0_file(50, end)
2347                .push_l0_file(80, end)
2348                .push_l0_file(90, end)
2349                .build(),
2350        );
2351        let region_id = builder.region_id();
2352        let manifest_ctx = env
2353            .mock_manifest_context(version_control.current().version.metadata.clone())
2354            .await;
2355        let (schema_metadata_manager, _kv_backend) = mock_schema_metadata_manager();
2356
2357        let (manual_tx, manual_rx) = oneshot::channel();
2358        let mut status =
2359            CompactionStatus::new(region_id, version_control.clone(), env.access_layer.clone());
2360        status.start_local_task();
2361        status.set_pending_request(PendingCompaction {
2362            options: compact_request::Options::Regular(Default::default()),
2363            waiter: OptionOutputTx::from(manual_tx),
2364            max_parallelism: 1,
2365        });
2366        scheduler.region_status.insert(region_id, status);
2367
2368        let (ddl_tx, ddl_rx) = oneshot::channel();
2369        scheduler.add_ddl_request_to_pending(SenderDdlRequest {
2370            region_id,
2371            sender: OptionOutputTx::from(ddl_tx),
2372            request: crate::request::DdlRequest::EnterStaging(
2373                store_api::region_request::EnterStagingRequest {
2374                    partition_directive:
2375                        store_api::region_request::StagingPartitionDirective::RejectAllWrites,
2376                },
2377            ),
2378        });
2379
2380        let pending_ddls = scheduler
2381            .on_compaction_finished(region_id, &manifest_ctx, schema_metadata_manager)
2382            .await;
2383
2384        assert!(pending_ddls.is_empty());
2385        assert!(!scheduler.region_status.contains_key(&region_id));
2386        assert!(manual_rx.await.is_err());
2387        assert_matches!(ddl_rx.await.unwrap(), Err(_));
2388    }
2389
2390    #[tokio::test]
2391    async fn test_on_compaction_finished_next_schedule_noop_removes_status() {
2392        let env = SchedulerEnv::new().await;
2393        let (tx, _rx) = mpsc::channel(4);
2394        let mut scheduler = env.mock_compaction_scheduler(tx);
2395        let builder = VersionControlBuilder::new();
2396        let version_control = Arc::new(builder.build());
2397        let region_id = builder.region_id();
2398        let manifest_ctx = env
2399            .mock_manifest_context(version_control.current().version.metadata.clone())
2400            .await;
2401        let (schema_metadata_manager, _kv_backend) = mock_schema_metadata_manager();
2402
2403        scheduler.region_status.insert(
2404            region_id,
2405            CompactionStatus::new(region_id, version_control, env.access_layer.clone()),
2406        );
2407        scheduler
2408            .region_status
2409            .get_mut(&region_id)
2410            .unwrap()
2411            .start_local_task();
2412
2413        let pending_ddls = scheduler
2414            .on_compaction_finished(region_id, &manifest_ctx, schema_metadata_manager)
2415            .await;
2416
2417        assert!(pending_ddls.is_empty());
2418        assert!(scheduler.region_status.contains_key(&region_id));
2419
2420        let (schema_metadata_manager, _kv_backend) = mock_schema_metadata_manager();
2421        // With no compactable files, next scheduling returns false and removes
2422        // the status without creating a background task.
2423        let scheduled = scheduler
2424            .schedule_next_compaction(region_id, &manifest_ctx, schema_metadata_manager)
2425            .await;
2426        assert!(!scheduled);
2427        assert!(!scheduler.region_status.contains_key(&region_id));
2428    }
2429
2430    #[tokio::test]
2431    async fn test_on_compaction_finished_next_schedule_error_cleans_status() {
2432        let env = SchedulerEnv::new()
2433            .await
2434            .scheduler(Arc::new(FailingScheduler));
2435        let (tx, _rx) = mpsc::channel(4);
2436        let mut scheduler = env.mock_compaction_scheduler(tx);
2437        let mut builder = VersionControlBuilder::new();
2438        let end = 1000 * 1000;
2439        let version_control = Arc::new(
2440            builder
2441                .push_l0_file(0, end)
2442                .push_l0_file(10, end)
2443                .push_l0_file(50, end)
2444                .push_l0_file(80, end)
2445                .push_l0_file(90, end)
2446                .build(),
2447        );
2448        let region_id = builder.region_id();
2449        let manifest_ctx = env
2450            .mock_manifest_context(version_control.current().version.metadata.clone())
2451            .await;
2452        let (schema_metadata_manager, _kv_backend) = mock_schema_metadata_manager();
2453
2454        scheduler.region_status.insert(
2455            region_id,
2456            CompactionStatus::new(region_id, version_control, env.access_layer.clone()),
2457        );
2458        scheduler
2459            .region_status
2460            .get_mut(&region_id)
2461            .unwrap()
2462            .start_local_task();
2463
2464        let pending_ddls = scheduler
2465            .on_compaction_finished(region_id, &manifest_ctx, schema_metadata_manager)
2466            .await;
2467
2468        assert!(pending_ddls.is_empty());
2469        assert!(scheduler.region_status.contains_key(&region_id));
2470
2471        let (schema_metadata_manager, _kv_backend) = mock_schema_metadata_manager();
2472        // The failing scheduler simulates a submit error; callers must see false.
2473        let scheduled = scheduler
2474            .schedule_next_compaction(region_id, &manifest_ctx, schema_metadata_manager)
2475            .await;
2476        assert!(!scheduled);
2477        assert!(!scheduler.region_status.contains_key(&region_id));
2478    }
2479
2480    #[tokio::test]
2481    async fn test_concurrent_memory_competition() {
2482        let manager = Arc::new(new_compaction_memory_manager(3 * 1024 * 1024)); // 3MB
2483        let barrier = Arc::new(Barrier::new(3));
2484        let mut handles = vec![];
2485
2486        // Spawn 3 tasks competing for memory, each trying to acquire 2MB
2487        for _i in 0..3 {
2488            let mgr = manager.clone();
2489            let bar = barrier.clone();
2490            let handle = tokio::spawn(async move {
2491                bar.wait().await; // Synchronize start
2492                mgr.try_acquire(2 * 1024 * 1024)
2493            });
2494            handles.push(handle);
2495        }
2496
2497        let results: Vec<Option<CompactionMemoryGuard>> = futures::future::join_all(handles)
2498            .await
2499            .into_iter()
2500            .map(|r| r.unwrap())
2501            .collect();
2502
2503        // Only 1 should succeed (3MB limit, 2MB request, can only fit one)
2504        let succeeded = results.iter().filter(|r| r.is_some()).count();
2505        let failed = results.iter().filter(|r| r.is_none()).count();
2506
2507        assert_eq!(succeeded, 1, "Expected exactly 1 task to acquire memory");
2508        assert_eq!(failed, 2, "Expected 2 tasks to fail");
2509
2510        // Clean up
2511        drop(results);
2512        assert_eq!(manager.used_bytes(), 0);
2513    }
2514}