1use 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
44pub const MAX_PARALLEL_COMPACTION: usize = 1;
46
47pub(crate) struct CompactionTaskImpl {
48 pub(crate) state: CancellableTaskState,
50 pub(crate) execution: CompactionExecution,
52 pub compaction_region: CompactionRegion,
53 pub(crate) request_sender: mpsc::Sender<WorkerRequestWithTime>,
55 pub waiters: Vec<OutputTx>,
57 pub start_time: Instant,
59 pub(crate) listener: WorkerListener,
61 pub(crate) compactor: Arc<dyn Compactor>,
63 pub(crate) picker_output: PickerOutput,
65 pub(crate) memory_manager: Arc<CompactionMemoryManager>,
67 pub(crate) memory_policy: OnExhaustedPolicy,
69 pub(crate) estimated_memory_bytes: u64,
71 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 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 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 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 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 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 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 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 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 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 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 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 let notify = match self.handle_compaction().await {
341 Ok(merge_output) => {
342 self.invoke_sst_hook(&merge_output).await;
343 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 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 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 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 let picker_output = PickerOutput {
449 outputs: vec![],
450 expired_ssts: vec![],
451 time_window_size: 3600,
452 max_file_size: None,
453 };
454
455 assert!(picker_output.expired_ssts.is_empty());
457 }
458
459 }