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::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
43pub const MAX_PARALLEL_COMPACTION: usize = 1;
45
46pub(crate) struct CompactionTaskImpl {
47 pub(crate) state: LocalCompactionState,
49 pub compaction_region: CompactionRegion,
50 pub(crate) request_sender: mpsc::Sender<WorkerRequestWithTime>,
52 pub waiters: Vec<OutputTx>,
54 pub start_time: Instant,
56 pub(crate) listener: WorkerListener,
58 pub(crate) compactor: Arc<dyn Compactor>,
60 pub(crate) picker_output: PickerOutput,
62 pub(crate) memory_manager: Arc<CompactionMemoryManager>,
64 pub(crate) memory_policy: OnExhaustedPolicy,
66 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 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 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 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 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 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 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 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 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 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 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 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 self.mark_files_compacting(true);
317 self.handle_expiration().await;
318
319 let cancel_handle = self.state.cancel_handle();
320 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 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 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 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 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 let picker_output = PickerOutput {
432 outputs: vec![],
433 expired_ssts: vec![],
434 time_window_size: 3600,
435 max_file_size: None,
436 };
437
438 assert!(picker_output.expired_ssts.is_empty());
440 }
441
442 }