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