Skip to main content

mito2/compaction/scheduler/
state.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
15use std::collections::HashSet;
16use std::sync::Arc;
17use std::time::Instant;
18
19use api::v1::region::compact_request;
20use common_meta::key::SchemaMetadataManagerRef;
21use common_telemetry::debug;
22use common_time::range::TimestampRange;
23use snafu::ResultExt;
24use store_api::storage::RegionId;
25use tokio::sync::mpsc::Sender;
26
27use crate::access_layer::AccessLayerRef;
28use crate::cache::CacheManagerRef;
29use crate::compaction::compactor::CompactionVersion;
30use crate::compaction::picker::PickerOutput;
31use crate::compaction::scheduler::planning::CompactionRequest;
32use crate::config::MitoConfig;
33use crate::error::{
34    CompactRegionSnafu, CompactionCancelledSnafu, Error, ManualCompactionOverrideSnafu,
35};
36use crate::region::ManifestContextRef;
37use crate::region::version::VersionControlRef;
38use crate::request::{OptionOutputTx, OutputTx, SenderDdlRequest, WorkerRequestWithTime};
39use crate::schedule::{CancellableTaskState, RequestCancelResult};
40use crate::sst::file::FileHandle;
41use crate::worker::WorkerListener;
42
43/// Identifies an accepted compaction attempt and keeps its SST reservations alive.
44/// The plan id fences terminal notifications from superseded attempts.
45#[derive(Debug, Clone)]
46pub(crate) struct CompactionExecution {
47    plan_id: u64,
48    _files: CompactingFiles,
49}
50
51impl CompactionExecution {
52    pub(super) fn new(plan_id: u64, files: CompactingFiles) -> Self {
53        Self {
54            plan_id,
55            _files: files,
56        }
57    }
58
59    pub(crate) fn matches(&self, other: &Self) -> bool {
60        self.plan_id == other.plan_id
61    }
62
63    #[cfg(test)]
64    pub(crate) fn for_test(plan_id: u64) -> Self {
65        Self::new(plan_id, CompactingFiles::empty())
66    }
67}
68
69#[derive(Debug)]
70pub(super) enum CompactionPhase {
71    Picking {
72        plan_id: u64,
73        cancelled: bool,
74    },
75    Local {
76        state: CancellableTaskState,
77        execution: CompactionExecution,
78    },
79    Remote {
80        execution: CompactionExecution,
81    },
82}
83
84/// Describes how the current compaction cycle was triggered.
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub(super) enum CompactionTrigger {
87    Automatic,
88    Manual,
89}
90
91#[derive(Debug)]
92pub(super) struct ActiveCompaction {
93    pub(super) phase: CompactionPhase,
94    trigger: CompactionTrigger,
95    /// Waiters satisfied by the current planning or execution cycle. Picking waiters move into
96    /// the submitted task; regular triggers coalesced during execution accumulate here.
97    pub(super) waiters: Vec<OutputTx>,
98    /// An automatic trigger arrived during this cycle and requires one unrestricted regular
99    /// picking cycle after the current cycle finishes. Recorded in every phase so an external
100    /// trigger always resets the continuation scope, even during local/remote execution.
101    pub(super) automatic_followup_required: bool,
102}
103
104impl ActiveCompaction {
105    pub(super) fn picking(
106        plan_id: u64,
107        waiters: Vec<OutputTx>,
108        trigger: CompactionTrigger,
109    ) -> Self {
110        Self {
111            phase: CompactionPhase::Picking {
112                plan_id,
113                cancelled: false,
114            },
115            trigger,
116            waiters,
117            automatic_followup_required: false,
118        }
119    }
120
121    pub(super) fn start_picking(&mut self, plan_id: u64, trigger: CompactionTrigger) {
122        self.phase = CompactionPhase::Picking {
123            plan_id,
124            cancelled: false,
125        };
126        self.trigger = trigger;
127    }
128
129    pub(super) fn start_regular_picking(&mut self, plan_id: u64) {
130        self.start_picking(plan_id, CompactionTrigger::Automatic);
131    }
132
133    /// Marks an automatic trigger. The trigger is coalesced into a single unrestricted
134    /// follow-up cycle regardless of the current phase.
135    pub(super) fn mark_automatic_trigger(&mut self) {
136        self.automatic_followup_required = true;
137    }
138
139    /// Resets whether an unrestricted follow-up cycle is required and
140    /// return the previous value.
141    pub(super) fn reset_automatic_followup(&mut self) -> bool {
142        std::mem::take(&mut self.automatic_followup_required)
143    }
144
145    pub(super) fn is_manual(&self) -> bool {
146        self.trigger == CompactionTrigger::Manual
147    }
148
149    pub(super) fn is_picking(&self, expected_plan_id: u64) -> bool {
150        matches!(
151            self.phase,
152            CompactionPhase::Picking { plan_id, .. } if plan_id == expected_plan_id
153        )
154    }
155
156    pub(super) fn accept_plan(&self, expected_plan_id: u64) -> bool {
157        matches!(
158            self.phase,
159            CompactionPhase::Picking {
160                plan_id,
161                cancelled: false,
162            } if plan_id == expected_plan_id
163        )
164    }
165
166    pub(super) fn matches_execution(&self, execution: &CompactionExecution) -> bool {
167        match &self.phase {
168            CompactionPhase::Picking { .. } => None,
169            CompactionPhase::Local { execution, .. } | CompactionPhase::Remote { execution } => {
170                Some(execution)
171            }
172        }
173        .is_some_and(|current| current.matches(execution))
174    }
175
176    pub(super) fn request_cancel(&mut self) -> RequestCancelResult {
177        match &mut self.phase {
178            CompactionPhase::Picking { cancelled, .. } => {
179                if *cancelled {
180                    RequestCancelResult::AlreadyCancelling
181                } else {
182                    *cancelled = true;
183                    RequestCancelResult::CancelIssued
184                }
185            }
186            CompactionPhase::Local { state, .. } => state.request_cancel(),
187            CompactionPhase::Remote { .. } => RequestCancelResult::TooLateToCancel,
188        }
189    }
190
191    pub(super) fn merge_waiter(&mut self, mut waiter: OptionOutputTx) {
192        if let Some(waiter) = waiter.take_inner() {
193            self.waiters.push(waiter);
194        }
195    }
196}
197
198/// Owns atomic reservations for every SST selected by a compaction plan.
199#[derive(Debug, Clone)]
200pub(super) struct CompactingFiles {
201    _inner: Arc<CompactingFilesInner>,
202}
203
204#[derive(Debug)]
205struct CompactingFilesInner {
206    files: Vec<FileHandle>,
207}
208
209impl CompactingFiles {
210    pub(super) fn try_new(output: &PickerOutput) -> Option<Self> {
211        let mut seen = HashSet::new();
212        let mut files: Vec<FileHandle> = Vec::new();
213        let selected_files = output
214            .outputs
215            .iter()
216            .flat_map(|output| output.inputs.iter())
217            .chain(output.expired_ssts.iter());
218
219        for file in selected_files {
220            if !seen.insert(file.file_id()) {
221                continue;
222            }
223            if !file.try_set_compacting() {
224                for reserved in &files {
225                    reserved.set_compacting(false);
226                }
227                return None;
228            }
229            files.push(file.clone());
230        }
231
232        Some(Self {
233            _inner: Arc::new(CompactingFilesInner { files }),
234        })
235    }
236
237    #[cfg(test)]
238    pub(super) fn empty() -> Self {
239        Self {
240            _inner: Arc::new(CompactingFilesInner { files: Vec::new() }),
241        }
242    }
243}
244
245impl Drop for CompactingFilesInner {
246    fn drop(&mut self) {
247        for file in &self.files {
248            file.set_compacting(false);
249        }
250    }
251}
252
253/// Status of running and pending region compaction tasks.
254pub(super) struct CompactionStatus {
255    /// Id of the region.
256    pub(super) region_id: RegionId,
257    /// Version control of the region.
258    pub(super) version_control: VersionControlRef,
259    /// Access layer of the region.
260    pub(super) access_layer: AccessLayerRef,
261    /// Current compaction lifecycle.
262    pub(super) active: ActiveCompaction,
263    /// A manual compaction waiting for the current automatic compaction to finish.
264    ///
265    /// A manual request is rejected if the current compaction is also manual. Automatic requests
266    /// are merged into the active compaction instead of using this slot.
267    pub(super) pending_request: Option<PendingCompaction>,
268    /// Pending DDL requests that should run when compaction is done.
269    ///
270    /// Although [`SenderDdlRequest`] can wrap any DDL variant, production code only queues
271    /// [`crate::request::DdlRequest::Truncate`] and [`crate::request::DdlRequest::EnterStaging`] here. Both must serialize with
272    /// compaction so they observe the version after compaction terminates.
273    pub(super) pending_ddl_requests: Vec<SenderDdlRequest>,
274}
275
276impl CompactionStatus {
277    /// Creates a new picking [CompactionStatus].
278    pub(super) fn new(
279        region_id: RegionId,
280        version_control: VersionControlRef,
281        access_layer: AccessLayerRef,
282        plan_id: u64,
283        trigger: CompactionTrigger,
284    ) -> CompactionStatus {
285        CompactionStatus {
286            region_id,
287            version_control,
288            access_layer,
289            active: ActiveCompaction::picking(plan_id, Vec::new(), trigger),
290            pending_request: None,
291            pending_ddl_requests: Vec::new(),
292        }
293    }
294
295    #[cfg(test)]
296    pub(super) fn for_test(
297        region_id: RegionId,
298        version_control: VersionControlRef,
299        access_layer: AccessLayerRef,
300    ) -> Self {
301        Self::new(
302            region_id,
303            version_control,
304            access_layer,
305            0,
306            CompactionTrigger::Automatic,
307        )
308    }
309
310    #[cfg(test)]
311    pub(super) fn start_picking(&mut self, plan_id: u64) {
312        self.start_picking_with_trigger(plan_id, CompactionTrigger::Automatic);
313    }
314
315    pub(super) fn start_picking_with_trigger(&mut self, plan_id: u64, trigger: CompactionTrigger) {
316        self.active.start_picking(plan_id, trigger);
317    }
318
319    pub(super) fn start_regular_picking(&mut self, plan_id: u64) {
320        self.active.start_regular_picking(plan_id);
321    }
322
323    pub(super) fn is_picking(&self, expected_plan_id: u64) -> bool {
324        self.active.is_picking(expected_plan_id)
325    }
326
327    pub(super) fn accept_plan(&self, expected_plan_id: u64) -> bool {
328        self.active.accept_plan(expected_plan_id)
329    }
330
331    pub(super) fn is_manual_compaction(&self) -> bool {
332        self.active.is_manual()
333    }
334
335    pub(super) fn matches_execution(&self, execution: &CompactionExecution) -> bool {
336        self.active.matches_execution(execution)
337    }
338
339    #[cfg(test)]
340    pub(super) fn start_local_task(&mut self) -> CancellableTaskState {
341        let state = CancellableTaskState::new();
342        let execution = CompactionExecution::new(0, CompactingFiles::empty());
343        let phase = CompactionPhase::Local {
344            state: state.clone(),
345            execution,
346        };
347        self.active.phase = phase;
348        state
349    }
350
351    #[cfg(test)]
352    pub(super) fn start_remote_task(&mut self) {
353        let execution = CompactionExecution::new(0, CompactingFiles::empty());
354        let phase = CompactionPhase::Remote { execution };
355        self.active.phase = phase;
356    }
357
358    pub(super) fn request_cancel(&mut self) -> RequestCancelResult {
359        self.active.request_cancel()
360    }
361
362    pub(super) fn mark_automatic_trigger(&mut self) {
363        self.active.mark_automatic_trigger();
364    }
365
366    /// Merge the waiter to the pending compaction.
367    pub(super) fn merge_waiter(&mut self, waiter: OptionOutputTx) {
368        self.active.merge_waiter(waiter);
369    }
370
371    pub(super) fn take_waiters(&mut self) -> Vec<OutputTx> {
372        std::mem::take(&mut self.active.waiters)
373    }
374
375    pub(super) fn extend_waiters(&mut self, waiters: Vec<OutputTx>) {
376        self.active.waiters.extend(waiters);
377    }
378
379    pub(super) fn append_waiters(&mut self, waiters: &mut Vec<OutputTx>) {
380        self.active.waiters.append(waiters);
381    }
382
383    pub(super) fn set_phase(&mut self, phase: CompactionPhase) {
384        self.active.phase = phase;
385    }
386
387    /// Sets a pending manual compaction request, replacing an older pending request.
388    pub(super) fn set_pending_request(&mut self, pending: PendingCompaction) {
389        if let Some(prev) = self.pending_request.replace(pending) {
390            debug!(
391                "Replace pending compaction options with new request {:?} for region: {}",
392                prev.options, self.region_id
393            );
394            prev.waiter.send(ManualCompactionOverrideSnafu.fail());
395        }
396    }
397
398    pub(super) fn on_failure(mut self, err: Arc<Error>) {
399        for waiter in self.active.waiters.drain(..) {
400            waiter.send(Err(err.clone()).context(CompactRegionSnafu {
401                region_id: self.region_id,
402            }));
403        }
404
405        if let Some(pending_compaction) = self.pending_request {
406            pending_compaction
407                .waiter
408                .send(Err(err.clone()).context(CompactRegionSnafu {
409                    region_id: self.region_id,
410                }));
411        }
412
413        for pending_ddl in self.pending_ddl_requests {
414            pending_ddl
415                .sender
416                .send(Err(err.clone()).context(CompactRegionSnafu {
417                    region_id: self.region_id,
418                }));
419        }
420    }
421
422    #[must_use]
423    pub(super) fn on_cancel(mut self) -> Vec<SenderDdlRequest> {
424        for waiter in self.active.waiters.drain(..) {
425            waiter.send(CompactionCancelledSnafu.fail());
426        }
427
428        if let Some(pending_compaction) = self.pending_request {
429            pending_compaction.waiter.send(
430                Err(Arc::new(CompactionCancelledSnafu.build())).context(CompactRegionSnafu {
431                    region_id: self.region_id,
432                }),
433            );
434        }
435
436        std::mem::take(&mut self.pending_ddl_requests)
437    }
438
439    /// Creates an immutable request for background compaction planning.
440    #[allow(clippy::too_many_arguments)]
441    pub(super) fn new_compaction_request(
442        &self,
443        request_sender: Sender<WorkerRequestWithTime>,
444        engine_config: Arc<MitoConfig>,
445        cache_manager: CacheManagerRef,
446        manifest_ctx: &ManifestContextRef,
447        listener: WorkerListener,
448        schema_metadata_manager: SchemaMetadataManagerRef,
449        max_parallelism: usize,
450    ) -> CompactionRequest {
451        let current_version = CompactionVersion::from(self.version_control.current().version);
452        let start_time = Instant::now();
453
454        CompactionRequest {
455            engine_config,
456            current_version,
457            access_layer: self.access_layer.clone(),
458            request_sender: request_sender.clone(),
459            start_time,
460            cache_manager,
461            manifest_ctx: manifest_ctx.clone(),
462            listener,
463            schema_metadata_manager,
464            max_parallelism,
465        }
466    }
467}
468
469/// A manual compaction request waiting for an automatic compaction to finish.
470pub(super) struct PendingCompaction {
471    /// Compaction options.
472    pub(crate) options: compact_request::Options,
473    /// Waiters of pending requests.
474    pub(crate) waiter: OptionOutputTx,
475    /// Max parallelism for pending compaction.
476    pub(crate) max_parallelism: usize,
477    /// Optional time range that constrains candidate compaction windows.
478    pub(crate) time_range: Option<TimestampRange>,
479}