Skip to main content

mito2/compaction/
task.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::fmt::{Debug, Formatter};
16use std::sync::Arc;
17use std::time::Instant;
18
19use common_base::cancellation::CancellableFuture;
20use common_memory_manager::OnExhaustedPolicy;
21use common_telemetry::{error, info, warn};
22use itertools::Itertools;
23use snafu::ResultExt;
24use store_api::ManifestVersion;
25use tokio::sync::mpsc;
26
27use crate::compaction::LocalCompactionState;
28use crate::compaction::compactor::{CompactionRegion, Compactor, MergeOutput};
29use crate::compaction::memory_manager::{CompactionMemoryGuard, CompactionMemoryManager};
30use crate::compaction::picker::{CompactionTask, PickerOutput};
31use crate::error::{CompactRegionSnafu, CompactionMemoryExhaustedSnafu};
32use crate::manifest::action::{RegionEdit, RegionMetaAction, RegionMetaActionList};
33use crate::metrics::{COMPACTION_FAILURE_COUNT, COMPACTION_MEMORY_WAIT, COMPACTION_STAGE_ELAPSED};
34use crate::region::RegionRoleState;
35use crate::request::{
36    BackgroundNotify, CompactionCancelled, CompactionFailed, CompactionFinished, OutputTx,
37    RegionEditResult, Waiters, WorkerRequest, WorkerRequestWithTime,
38};
39use crate::sst::file::FileMeta;
40use crate::worker::WorkerListener;
41use crate::{error, metrics};
42
43/// Maximum number of compaction tasks in parallel.
44pub const MAX_PARALLEL_COMPACTION: usize = 1;
45
46pub(crate) struct CompactionTaskImpl {
47    /// Shared local-compaction state for cooperative cancellation.
48    pub(crate) state: LocalCompactionState,
49    pub compaction_region: CompactionRegion,
50    /// Request sender to notify the worker.
51    pub(crate) request_sender: mpsc::Sender<WorkerRequestWithTime>,
52    /// Senders that are used to notify waiters waiting for pending compaction tasks.
53    pub waiters: Vec<OutputTx>,
54    /// Start time of compaction task
55    pub start_time: Instant,
56    /// Event listener.
57    pub(crate) listener: WorkerListener,
58    /// Compactor to handle compaction.
59    pub(crate) compactor: Arc<dyn Compactor>,
60    /// Output of the picker.
61    pub(crate) picker_output: PickerOutput,
62    /// Memory manager to acquire memory budget.
63    pub(crate) memory_manager: Arc<CompactionMemoryManager>,
64    /// Policy when memory is exhausted.
65    pub(crate) memory_policy: OnExhaustedPolicy,
66    /// Estimated memory bytes needed for this compaction.
67    pub(crate) estimated_memory_bytes: u64,
68}
69
70impl Debug for CompactionTaskImpl {
71    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
72        f.debug_struct("TwcsCompactionTask")
73            .field("region_id", &self.compaction_region.region_id)
74            .field("picker_output", &self.picker_output)
75            .field(
76                "append_mode",
77                &self.compaction_region.region_options.append_mode,
78            )
79            .finish()
80    }
81}
82
83impl Drop for CompactionTaskImpl {
84    fn drop(&mut self) {
85        self.mark_files_compacting(false)
86    }
87}
88
89impl CompactionTaskImpl {
90    fn mark_files_compacting(&self, compacting: bool) {
91        self.picker_output
92            .outputs
93            .iter()
94            .for_each(|o| o.inputs.iter().for_each(|f| f.set_compacting(compacting)));
95    }
96
97    /// Acquires memory budget based on the configured policy.
98    ///
99    /// Returns an error if memory cannot be acquired according to the policy.
100    async fn acquire_memory_with_policy(&self) -> error::Result<CompactionMemoryGuard> {
101        let region_id = self.compaction_region.region_id;
102        let requested_bytes = self.estimated_memory_bytes;
103        let policy = self.memory_policy;
104
105        let _timer = COMPACTION_MEMORY_WAIT.start_timer();
106        self.memory_manager
107            .acquire_with_policy(requested_bytes, policy)
108            .await
109            .context(CompactionMemoryExhaustedSnafu {
110                region_id,
111                policy: format!("{policy:?}"),
112            })
113    }
114
115    /// Remove expired ssts files, update manifest immediately
116    /// and apply the edit to region version.
117    ///
118    /// This function logs errors but does not stop the compaction process if removal fails.
119    async fn remove_expired(
120        &self,
121        compaction_region: &CompactionRegion,
122        expired_files: Vec<FileMeta>,
123    ) {
124        let region_id = compaction_region.region_id;
125        let expired_files_str = expired_files.iter().map(|f| f.file_id).join(",");
126        let (expire_delete_sender, expire_delete_listener) = tokio::sync::oneshot::channel();
127        // Update manifest to remove expired SSTs
128        let edit = RegionEdit {
129            files_to_add: Vec::new(),
130            files_to_remove: expired_files,
131            timestamp_ms: Some(chrono::Utc::now().timestamp_millis()),
132            compaction_time_window: None,
133            flushed_entry_id: None,
134            flushed_sequence: None,
135            committed_sequence: None,
136        };
137
138        // 1. Update manifest
139        let action_list = RegionMetaActionList::with_action(RegionMetaAction::Edit(edit.clone()));
140        let RegionRoleState::Leader(current_region_state) =
141            compaction_region.manifest_ctx.current_state()
142        else {
143            warn!(
144                "Region {} not in leader state, skip removing expired files",
145                region_id
146            );
147            return;
148        };
149        if let Err(e) = compaction_region
150            .manifest_ctx
151            .update_manifest(current_region_state, action_list, false)
152            .await
153        {
154            warn!(
155                e;
156                "Failed to update manifest for expired files removal, region: {region_id}, files: [{expired_files_str}]. Compaction will continue."
157            );
158            return;
159        }
160
161        // 2. Notify region worker loop to remove expired files from region version.
162        self.send_to_worker(WorkerRequest::Background {
163            region_id,
164            notify: BackgroundNotify::RegionEdit(RegionEditResult {
165                region_id,
166                waiters: Waiters::one(expire_delete_sender),
167                edit,
168                result: Ok(()),
169                update_region_state: false,
170                is_staging: false,
171            }),
172        })
173        .await;
174
175        if let Err(e) = expire_delete_listener
176            .await
177            .context(error::RecvSnafu)
178            .flatten()
179        {
180            warn!(
181                e;
182                "Failed to remove expired files from region version, region: {region_id}, files: [{expired_files_str}]. Compaction will continue."
183            );
184            return;
185        }
186
187        info!(
188            "Successfully removed expired files, region: {region_id}, files: [{expired_files_str}]"
189        );
190    }
191
192    async fn handle_expiration(&mut self) {
193        // 1. In case of local compaction, we can delete expired ssts in advance.
194        if !self.picker_output.expired_ssts.is_empty() {
195            let remove_timer = COMPACTION_STAGE_ELAPSED
196                .with_label_values(&["remove_expired"])
197                .start_timer();
198            let expired_ssts = self
199                .picker_output
200                .expired_ssts
201                .drain(..)
202                .map(|f| f.meta_ref().clone())
203                .collect();
204            // remove_expired logs errors but doesn't stop compaction
205            self.remove_expired(&self.compaction_region, expired_ssts)
206                .await;
207            remove_timer.observe_duration();
208        }
209    }
210
211    async fn handle_compaction(&mut self) -> error::Result<MergeOutput> {
212        // 2. Merge inputs
213        let merge_timer = COMPACTION_STAGE_ELAPSED
214            .with_label_values(&["merge"])
215            .start_timer();
216
217        let compaction_result = match self
218            .compactor
219            .merge_ssts(&self.compaction_region, self.picker_output.clone())
220            .await
221        {
222            Ok(v) => v,
223            Err(e) => {
224                error!(e; "Failed to compact region: {}", self.compaction_region.region_id);
225                merge_timer.stop_and_discard();
226                return Err(e);
227            }
228        };
229        let merge_time = merge_timer.stop_and_record();
230
231        metrics::COMPACTION_INPUT_BYTES.inc_by(compaction_result.input_file_size() as f64);
232        metrics::COMPACTION_OUTPUT_BYTES.inc_by(compaction_result.output_file_size() as f64);
233        info!(
234            "Compacted SST files, region_id: {}, input: {:?}, output: {:?}, window: {:?}, waiter_num: {}, merge_time: {}s",
235            self.compaction_region.region_id,
236            compaction_result.files_to_remove,
237            compaction_result.files_to_add,
238            compaction_result.compaction_time_window,
239            self.waiters.len(),
240            merge_time,
241        );
242
243        self.listener
244            .on_merge_ssts_finished(self.compaction_region.region_id)
245            .await;
246
247        Ok(compaction_result)
248    }
249
250    async fn update_manifest(
251        &self,
252        compaction_result: crate::compaction::compactor::MergeOutput,
253    ) -> error::Result<(RegionEdit, ManifestVersion)> {
254        let _manifest_timer = COMPACTION_STAGE_ELAPSED
255            .with_label_values(&["write_manifest"])
256            .start_timer();
257
258        self.compactor
259            .update_manifest(&self.compaction_region, compaction_result)
260            .await
261    }
262
263    /// Handles compaction failure, notifies all waiters.
264    pub(crate) fn on_failure(&mut self, err: Arc<error::Error>) {
265        COMPACTION_FAILURE_COUNT.inc();
266        for waiter in self.waiters.drain(..) {
267            waiter.send(Err(err.clone()).context(CompactRegionSnafu {
268                region_id: self.compaction_region.region_id,
269            }));
270        }
271    }
272
273    /// Notifies region worker to handle post-compaction tasks.
274    async fn send_to_worker(&self, request: WorkerRequest) {
275        if let Err(e) = self
276            .request_sender
277            .send(WorkerRequestWithTime::new(request))
278            .await
279        {
280            error!(
281                "Failed to notify compaction job status for region {}, request: {:?}",
282                self.compaction_region.region_id, e.0
283            );
284        }
285    }
286
287    async fn invoke_sst_hook(&self, merge_output: &MergeOutput) {
288        self.compaction_region.invoke_sst_hook(merge_output).await;
289    }
290}
291
292#[async_trait::async_trait]
293impl CompactionTask for CompactionTaskImpl {
294    async fn run(&mut self) {
295        // Acquire memory budget before starting compaction
296        let _memory_guard = match self.acquire_memory_with_policy().await {
297            Ok(guard) => guard,
298            Err(e) => {
299                error!(e; "Failed to acquire memory for compaction, region id: {}", self.compaction_region.region_id);
300                let err = Arc::new(e);
301                self.on_failure(err.clone());
302                let notify = BackgroundNotify::CompactionFailed(CompactionFailed {
303                    region_id: self.compaction_region.region_id,
304                    err,
305                });
306                self.send_to_worker(WorkerRequest::Background {
307                    region_id: self.compaction_region.region_id,
308                    notify,
309                })
310                .await;
311                return;
312            }
313        };
314
315        // Marks files compacting before compaction and unmark after compaction (even if compaction is cancelled or failed), so that they won't be picked by other compaction tasks.
316        self.mark_files_compacting(true);
317        self.handle_expiration().await;
318
319        let cancel_handle = self.state.cancel_handle();
320        // Run compaction with cooperative cancellation.
321        let notify = match CancellableFuture::new(
322            async { self.handle_compaction().await },
323            cancel_handle,
324        )
325        .await
326        {
327            Ok(Ok(merge_output)) => {
328                self.invoke_sst_hook(&merge_output).await;
329                // Stop accepting cancellation once we are about to publish the compaction edit.
330                if !self.state.mark_commit_started() {
331                    let senders = std::mem::take(&mut self.waiters);
332                    BackgroundNotify::CompactionCancelled(CompactionCancelled {
333                        region_id: self.compaction_region.region_id,
334                        senders,
335                    })
336                } else {
337                    match self.update_manifest(merge_output).await {
338                        Ok((edit, _manifest_version)) => {
339                            let senders = std::mem::take(&mut self.waiters);
340                            BackgroundNotify::CompactionFinished(CompactionFinished {
341                                region_id: self.compaction_region.region_id,
342                                senders,
343                                start_time: self.start_time,
344                                edit,
345                            })
346                        }
347                        Err(e) => {
348                            error!(e; "Failed to compact region, region id: {}", self.compaction_region.region_id);
349                            let err = Arc::new(e);
350                            self.on_failure(err.clone());
351                            BackgroundNotify::CompactionFailed(CompactionFailed {
352                                region_id: self.compaction_region.region_id,
353                                err,
354                            })
355                        }
356                    }
357                }
358            }
359            Err(_) => {
360                info!(
361                    "Compaction cancelled, region id: {}",
362                    self.compaction_region.region_id
363                );
364                let senders = std::mem::take(&mut self.waiters);
365                BackgroundNotify::CompactionCancelled(CompactionCancelled {
366                    region_id: self.compaction_region.region_id,
367                    senders,
368                })
369            }
370            Ok(Err(e)) => {
371                error!(e; "Failed to compact region, region id: {}", self.compaction_region.region_id);
372                let err = Arc::new(e);
373                // notify compaction waiters
374                self.on_failure(err.clone());
375                BackgroundNotify::CompactionFailed(CompactionFailed {
376                    region_id: self.compaction_region.region_id,
377                    err,
378                })
379            }
380        };
381
382        self.send_to_worker(WorkerRequest::Background {
383            region_id: self.compaction_region.region_id,
384            notify,
385        })
386        .await;
387    }
388}
389
390#[cfg(test)]
391mod tests {
392    use store_api::storage::FileId;
393
394    use crate::compaction::picker::PickerOutput;
395    use crate::compaction::test_util::new_file_handle;
396
397    #[test]
398    fn test_picker_output_with_expired_ssts() {
399        // Test that PickerOutput correctly includes expired_ssts
400        // This verifies that expired SSTs are properly identified and included
401        // in the picker output, which is then handled by handle_expiration()
402
403        let file_ids = (0..3).map(|_| FileId::random()).collect::<Vec<_>>();
404        let expired_ssts = vec![
405            new_file_handle(file_ids[0], 0, 999, 0),
406            new_file_handle(file_ids[1], 1000, 1999, 0),
407        ];
408
409        let picker_output = PickerOutput {
410            outputs: vec![],
411            expired_ssts: expired_ssts.clone(),
412            time_window_size: 3600,
413            max_file_size: None,
414        };
415
416        // Verify expired_ssts are included
417        assert_eq!(picker_output.expired_ssts.len(), 2);
418        assert_eq!(
419            picker_output.expired_ssts[0].file_id(),
420            expired_ssts[0].file_id()
421        );
422        assert_eq!(
423            picker_output.expired_ssts[1].file_id(),
424            expired_ssts[1].file_id()
425        );
426    }
427
428    #[test]
429    fn test_picker_output_without_expired_ssts() {
430        // Test that PickerOutput works correctly when there are no expired SSTs
431        let picker_output = PickerOutput {
432            outputs: vec![],
433            expired_ssts: vec![],
434            time_window_size: 3600,
435            max_file_size: None,
436        };
437
438        // Verify empty expired_ssts
439        assert!(picker_output.expired_ssts.is_empty());
440    }
441
442    // Note: Testing remove_expired() directly requires extensive mocking of:
443    // - manifest_ctx (ManifestContext)
444    // - request_sender (mpsc::Sender<WorkerRequestWithTime>)
445    // - WorkerRequest handling
446    //
447    // The behavior is tested indirectly through integration tests:
448    // - remove_expired() logs errors but doesn't stop compaction
449    // - handle_expiration() continues even if remove_expired() encounters errors
450    // - The expiration stage is designed to be non-blocking for compaction
451}