Skip to main content

mito2/worker/
handle_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
15use api::v1::region::compact_request;
16use common_telemetry::{debug, error, info};
17use store_api::logstore::LogStore;
18use store_api::region_request::RegionCompactRequest;
19use store_api::storage::RegionId;
20
21use crate::compaction::{CompactionPickFinished, CompactionTransition};
22use crate::config::IndexBuildMode;
23use crate::error::{RegionNotFoundSnafu, StaleCompactionExecutionSnafu};
24use crate::metrics::COMPACTION_REQUEST_COUNT;
25use crate::region::MitoRegionRef;
26use crate::request::{
27    BuildIndexRequest, CompactionFailed, CompactionFinished, CompactionUnitNotification, OnFailure,
28    OptionOutputTx,
29};
30use crate::sst::index::IndexBuildType;
31use crate::worker::RegionWorkerLoop;
32
33fn made_progress(files_to_add: usize, files_to_remove: usize) -> bool {
34    files_to_add > 0 || files_to_remove > files_to_add
35}
36
37impl<S> RegionWorkerLoop<S> {
38    /// Drives local unit publication and applies successful edits before acknowledging completion.
39    pub(crate) async fn handle_compaction_unit(
40        &mut self,
41        region_id: RegionId,
42        notify: CompactionUnitNotification,
43    ) where
44        S: LogStore,
45    {
46        match notify {
47            CompactionUnitNotification::Finished {
48                plan_id,
49                result,
50                applied,
51            } => {
52                let Some(region) = self.regions.get_region(region_id) else {
53                    return;
54                };
55                match result {
56                    Ok(edit) => {
57                        if !self.compaction_scheduler.can_apply_unit(region_id, plan_id) {
58                            let _ = applied.send(());
59                            return;
60                        }
61                        let progress =
62                            made_progress(edit.files_to_add.len(), edit.files_to_remove.len());
63                        region.version_control.apply_edit(
64                            Some(edit.clone()),
65                            &[],
66                            region.file_purger.clone(),
67                        );
68                        self.listener.on_compaction_unit_applied(region_id, plan_id);
69                        self.compaction_scheduler.on_unit_finished(
70                            region_id,
71                            plan_id,
72                            Ok(progress),
73                        );
74                        if self.config.index.build_mode == IndexBuildMode::Async
75                            && !edit.files_to_add.is_empty()
76                        {
77                            self.handle_rebuild_index(
78                                BuildIndexRequest {
79                                    region_id,
80                                    build_type: IndexBuildType::Compact,
81                                    file_metas: edit.files_to_add,
82                                },
83                                OptionOutputTx::new(None),
84                            )
85                            .await;
86                        }
87                    }
88                    Err(err) => {
89                        self.compaction_scheduler
90                            .on_unit_finished(region_id, plan_id, Err(err));
91                    }
92                }
93                let _ = applied.send(());
94            }
95            CompactionUnitNotification::Released { plan_id } => {
96                let Some(region) = self.regions.get_region(region_id) else {
97                    return;
98                };
99                let transition = self
100                    .compaction_scheduler
101                    .on_unit_released(
102                        region_id,
103                        plan_id,
104                        &region.manifest_ctx,
105                        self.schema_metadata_manager.clone(),
106                    )
107                    .await;
108                self.listener.on_compaction_result_notified(region_id).await;
109                match transition {
110                    CompactionTransition::AutomaticFollowupScheduled => {
111                        region.update_schedule_compaction_millis()
112                    }
113                    CompactionTransition::DdlReady(mut ddls) => {
114                        self.handle_ddl_requests(&mut ddls).await
115                    }
116                    CompactionTransition::NoAction => {}
117                }
118            }
119        }
120    }
121
122    pub(crate) async fn handle_compaction_pick_finished(
123        &mut self,
124        region_id: RegionId,
125        request: CompactionPickFinished,
126    ) where
127        S: LogStore,
128    {
129        let Some(region) = self.regions.get_region(region_id) else {
130            return;
131        };
132        // A terminal pick (canceled, no plan, or failed) may return the region
133        // to idle and release DDLs fenced behind its picking phase.
134        // Such a pick never produces an execution callback, so the worker must
135        // execute the returned DDLs from this notification.
136        let transition = self
137            .compaction_scheduler
138            .handle_compaction_pick_finished(
139                request,
140                &region.manifest_ctx,
141                self.schema_metadata_manager.clone(),
142            )
143            .await;
144        match transition {
145            CompactionTransition::AutomaticFollowupScheduled => {
146                // There will be a followup compaction so we need to update the
147                // last schedule time to avoid frequent compactions.
148                region.update_schedule_compaction_millis();
149            }
150            CompactionTransition::NoAction => {}
151            CompactionTransition::DdlReady(mut pending_ddls) => {
152                if !pending_ddls.is_empty() {
153                    // Preserve the lifecycle order observed by listeners: compaction
154                    // termination is visible before its dependent DDLs are dispatched.
155                    self.listener.on_compaction_result_notified(region_id).await;
156                    self.handle_ddl_requests(&mut pending_ddls).await;
157                }
158            }
159        }
160    }
161
162    /// Handles compaction request submitted to region worker.
163    pub(crate) async fn handle_compaction_request(
164        &mut self,
165        region_id: RegionId,
166        req: RegionCompactRequest,
167        mut sender: OptionOutputTx,
168    ) {
169        let Some(region) = self.regions.writable_region_or(region_id, &mut sender) else {
170            return;
171        };
172        COMPACTION_REQUEST_COUNT.inc();
173        let parallelism = req.parallelism.unwrap_or(1) as usize;
174        match self.compaction_scheduler.schedule_manual_compaction(
175            req.options,
176            &region.version_control,
177            &region.access_layer,
178            sender,
179            &region.manifest_ctx,
180            self.schema_metadata_manager.clone(),
181            parallelism,
182            req.time_range,
183        ) {
184            // Ok(false) means the request was merged, queued or rejected; the
185            // waiter was already notified or will be completed by the queued
186            // cycle, so only a newly scheduled task is logged as success.
187            Ok(true) => info!(
188                "Successfully scheduled compaction task for region: {}",
189                region_id
190            ),
191            Ok(false) => {}
192            Err(e) => {
193                error!(e; "Failed to schedule compaction task for region: {}", region_id);
194            }
195        }
196    }
197
198    /// Handles compaction finished, update region version and manifest, deleted compacted files.
199    pub(crate) async fn handle_compaction_finished(
200        &mut self,
201        region_id: RegionId,
202        mut request: CompactionFinished,
203    ) where
204        S: LogStore,
205    {
206        let region = match self.regions.get_region(region_id) {
207            Some(region) => region,
208            None => {
209                request.on_failure(RegionNotFoundSnafu { region_id }.build());
210                return;
211            }
212        };
213        // Reject stale terminal results before applying their manifest edit.
214        if !self
215            .compaction_scheduler
216            .is_current_execution(region_id, &request.execution)
217        {
218            request.on_failure(StaleCompactionExecutionSnafu { region_id }.build());
219            return;
220        }
221        let execution = request.execution.clone();
222        let made_progress = made_progress(
223            request.edit.files_to_add.len(),
224            request.edit.files_to_remove.len(),
225        );
226
227        region.version_control.apply_edit(
228            Some(request.edit.clone()),
229            &[],
230            region.file_purger.clone(),
231        );
232
233        let index_build_file_metas = std::mem::take(&mut request.edit.files_to_add);
234
235        // compaction finished.
236        request.on_success();
237        self.listener.on_compaction_result_notified(region_id).await;
238
239        // In async mode, create indexes after compact if new files are created.
240        if self.config.index.build_mode == IndexBuildMode::Async
241            && !index_build_file_metas.is_empty()
242        {
243            self.handle_rebuild_index(
244                BuildIndexRequest {
245                    region_id,
246                    build_type: IndexBuildType::Compact,
247                    file_metas: index_build_file_metas,
248                },
249                OptionOutputTx::new(None),
250            )
251            .await;
252        }
253
254        // Schedule next compaction if necessary.
255        let transition = self
256            .compaction_scheduler
257            .on_execution_finished(
258                region_id,
259                &execution,
260                &region.manifest_ctx,
261                self.schema_metadata_manager.clone(),
262                made_progress,
263            )
264            .await;
265        match transition {
266            CompactionTransition::AutomaticFollowupScheduled => {
267                region.update_schedule_compaction_millis();
268            }
269            CompactionTransition::NoAction => {}
270            CompactionTransition::DdlReady(mut pending_ddls) => {
271                self.handle_ddl_requests(&mut pending_ddls).await;
272            }
273        }
274    }
275
276    /// When compaction fails, we simply log the error.
277    pub(crate) async fn handle_compaction_failure(&mut self, req: CompactionFailed) {
278        if self.regions.get_region(req.region_id).is_none() {
279            return;
280        }
281        if !self
282            .compaction_scheduler
283            .is_current_execution(req.region_id, &req.execution)
284        {
285            debug!(
286                "Ignores stale compaction failure for region {}: {:?}",
287                req.region_id, req.err
288            );
289            return;
290        }
291
292        error!(req.err; "Failed to compact region: {}", req.region_id);
293        self.compaction_scheduler
294            .on_execution_failed(req.region_id, &req.execution, req.err);
295    }
296
297    /// Schedule compaction for the region if necessary.
298    pub(crate) async fn schedule_compaction(&mut self, region: &MitoRegionRef) {
299        if region.is_staging() || region.is_enter_staging() || region.is_truncating() {
300            info!(
301                "Region {} is staging, entering staging, or truncating, skip compaction",
302                region.region_id
303            );
304            return;
305        }
306        let now = self.time_provider.current_time_millis();
307        if now - region.last_schedule_compaction_millis()
308            >= self.config.min_compaction_interval.as_millis() as i64
309        {
310            debug!(
311                "minimal compaction interval time {:?} has passed, scheduling next compaction",
312                self.config.min_compaction_interval
313            );
314            match self.compaction_scheduler.schedule_automatic_compaction(
315                compact_request::Options::Regular(Default::default()),
316                &region.version_control,
317                &region.access_layer,
318                &region.manifest_ctx,
319                self.schema_metadata_manager.clone(),
320            ) {
321                Ok(true) => region.update_schedule_compaction_millis(),
322                Ok(false) => {}
323                Err(e) => {
324                    error!(e; "Failed to schedule compaction for region: {}", region.region_id)
325                }
326            }
327        }
328    }
329}
330
331#[cfg(test)]
332mod tests {
333    use super::made_progress;
334
335    #[test]
336    fn test_nonempty_output_or_file_reduction_is_progress() {
337        for (files_to_add, files_to_remove, expected) in [
338            (3, 3, true),  // Equal-count rewrite.
339            (1, 3, true),  // Ordinary reduction.
340            (0, 3, true),  // Zero-output reduction.
341            (0, 0, false), // Empty edit.
342            (3, 2, true),  // Growth rewrite with output.
343        ] {
344            assert_eq!(
345                expected,
346                made_progress(files_to_add, files_to_remove),
347                "files_to_remove: {files_to_remove}, files_to_add: {files_to_add}"
348            );
349        }
350    }
351}