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