1use std::collections::{HashMap, HashSet, hash_map};
16use std::sync::Arc;
17use std::time::Instant;
18
19use api::v1::region::compact_request;
20use common_meta::key::SchemaMetadataManagerRef;
21use common_telemetry::debug;
22use common_time::range::TimestampRange;
23use snafu::ResultExt;
24use store_api::storage::RegionId;
25use tokio::sync::mpsc::Sender;
26
27use crate::access_layer::AccessLayerRef;
28use crate::cache::CacheManagerRef;
29use crate::compaction::compactor::CompactionVersion;
30use crate::compaction::picker::PickerOutput;
31use crate::compaction::scheduler::local::LocalCompaction;
32use crate::compaction::scheduler::planning::CompactionRequest;
33use crate::config::MitoConfig;
34use crate::error::{
35 CompactRegionSnafu, CompactionCancelledSnafu, Error, ManualCompactionOverrideSnafu,
36};
37use crate::region::ManifestContextRef;
38use crate::region::version::VersionControlRef;
39use crate::request::{OptionOutputTx, OutputTx, SenderDdlRequest, WorkerRequestWithTime};
40#[cfg(test)]
41use crate::schedule::CancellableTaskState;
42use crate::schedule::RequestCancelResult;
43use crate::sst::file::{FileHandle, RegionFileId};
44use crate::worker::WorkerListener;
45
46#[derive(Debug, Clone)]
49pub(crate) struct CompactionExecution {
50 plan_id: u64,
51 _files: CompactingFiles,
52}
53
54impl CompactionExecution {
55 pub(crate) fn plan_id(&self) -> u64 {
57 self.plan_id
58 }
59 pub(super) fn new(plan_id: u64, files: CompactingFiles) -> Self {
60 Self {
61 plan_id,
62 _files: files,
63 }
64 }
65
66 pub(crate) fn matches(&self, other: &Self) -> bool {
67 self.plan_id == other.plan_id
68 }
69
70 #[cfg(test)]
71 pub(crate) fn for_test(plan_id: u64) -> Self {
72 Self::new(plan_id, CompactingFiles::empty())
73 }
74}
75
76#[derive(Debug)]
77pub(crate) enum CompactionPhase {
78 Idle,
80 Picking {
81 plan_id: u64,
82 cancelled: bool,
83 },
84 #[cfg(test)]
85 Local {
86 state: CancellableTaskState,
87 execution: CompactionExecution,
88 },
89 Remote {
90 execution: CompactionExecution,
91 },
92 Units(LocalCompaction),
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub(super) enum CompactionTrigger {
98 Automatic,
99 Manual,
100}
101
102#[derive(Debug)]
103pub(super) struct ActiveCompaction {
104 pub(super) phase: CompactionPhase,
105 trigger: CompactionTrigger,
106 pub(super) waiters: Vec<OutputTx>,
109 pub(super) automatic_followup_required: bool,
113}
114
115impl ActiveCompaction {
116 fn idle() -> Self {
118 Self {
119 phase: CompactionPhase::Idle,
120 trigger: CompactionTrigger::Automatic,
121 waiters: Vec::new(),
122 automatic_followup_required: false,
123 }
124 }
125
126 #[cfg(test)]
127 pub(super) fn picking(
128 plan_id: u64,
129 waiters: Vec<OutputTx>,
130 trigger: CompactionTrigger,
131 ) -> Self {
132 Self {
133 phase: CompactionPhase::Picking {
134 plan_id,
135 cancelled: false,
136 },
137 trigger,
138 waiters,
139 automatic_followup_required: false,
140 }
141 }
142
143 pub(super) fn start_picking(&mut self, plan_id: u64, trigger: CompactionTrigger) {
144 self.phase = CompactionPhase::Picking {
145 plan_id,
146 cancelled: false,
147 };
148 self.trigger = trigger;
149 }
150
151 pub(super) fn start_regular_picking(&mut self, plan_id: u64) {
152 self.start_picking(plan_id, CompactionTrigger::Automatic);
153 }
154
155 pub(super) fn mark_automatic_trigger(&mut self) {
158 self.automatic_followup_required = true;
159 }
160
161 pub(super) fn reset_automatic_followup(&mut self) -> bool {
164 std::mem::take(&mut self.automatic_followup_required)
165 }
166
167 pub(super) fn is_manual(&self) -> bool {
168 self.trigger == CompactionTrigger::Manual
169 }
170
171 pub(super) fn is_picking(&self, expected_plan_id: u64) -> bool {
172 matches!(
173 self.phase,
174 CompactionPhase::Picking { plan_id, .. } if plan_id == expected_plan_id
175 )
176 }
177
178 pub(super) fn accept_plan(&self, expected_plan_id: u64) -> bool {
179 matches!(
180 self.phase,
181 CompactionPhase::Picking {
182 plan_id,
183 cancelled: false,
184 } if plan_id == expected_plan_id
185 )
186 }
187
188 pub(super) fn matches_execution(&self, execution: &CompactionExecution) -> bool {
189 match &self.phase {
190 CompactionPhase::Idle | CompactionPhase::Picking { .. } => None,
191 #[cfg(test)]
192 CompactionPhase::Local { execution, .. } => Some(execution),
193 CompactionPhase::Units(_) => None,
194 CompactionPhase::Remote { execution } => Some(execution),
195 }
196 .is_some_and(|current| current.matches(execution))
197 }
198
199 pub(super) fn request_cancel(&mut self) -> RequestCancelResult {
200 match &mut self.phase {
201 CompactionPhase::Idle => RequestCancelResult::TooLateToCancel,
202 CompactionPhase::Picking { cancelled, .. } => {
203 if *cancelled {
204 RequestCancelResult::AlreadyCancelling
205 } else {
206 *cancelled = true;
207 RequestCancelResult::CancelIssued
208 }
209 }
210 #[cfg(test)]
211 CompactionPhase::Local { state, .. } => state.request_cancel(),
212 CompactionPhase::Units(units) => units.request_cancel(),
213 CompactionPhase::Remote { .. } => RequestCancelResult::TooLateToCancel,
214 }
215 }
216
217 pub(super) fn merge_waiter(&mut self, mut waiter: OptionOutputTx) {
218 if let Some(waiter) = waiter.take_inner() {
219 self.waiters.push(waiter);
220 }
221 }
222}
223
224#[derive(Debug, Clone)]
226pub(crate) struct CompactingFiles {
227 files: HashMap<RegionFileId, Arc<CompactingFile>>,
228}
229
230#[derive(Debug)]
232struct CompactingFile {
233 file: FileHandle,
234}
235
236impl CompactingFiles {
237 pub(super) fn try_new(output: &PickerOutput) -> Option<Self> {
239 Self::try_reserve(
240 output
241 .outputs
242 .iter()
243 .flat_map(|o| &o.inputs)
244 .chain(&output.expired_ssts),
245 )
246 }
247
248 fn try_reserve<'a>(selected_files: impl IntoIterator<Item = &'a FileHandle>) -> Option<Self> {
250 let mut files = HashMap::new();
251
252 for file in selected_files {
253 let hash_map::Entry::Vacant(entry) = files.entry(file.file_id()) else {
254 continue;
255 };
256 if !file.try_set_compacting() {
257 return None;
258 }
259 entry.insert(Arc::new(CompactingFile { file: file.clone() }));
260 }
261
262 Some(Self { files })
263 }
264
265 pub(crate) fn for_inputs(&self, inputs: &[FileHandle]) -> Self {
271 Self {
272 files: inputs
273 .iter()
274 .filter_map(|file| {
275 let id = file.file_id();
276 self.files.get(&id).map(|lease| (id, lease.clone()))
277 })
278 .collect(),
279 }
280 }
281
282 #[cfg(test)]
283 pub(super) fn empty() -> Self {
284 Self {
285 files: HashMap::new(),
286 }
287 }
288}
289
290impl Drop for CompactingFile {
291 fn drop(&mut self) {
292 self.file.set_compacting(false);
293 }
294}
295
296#[derive(Debug)]
298pub(crate) struct DdlExecution {
299 generation: u64,
300 pending_replies: HashSet<usize>,
301}
302
303impl DdlExecution {
304 pub(crate) fn new(generation: u64, count: usize) -> Self {
306 Self {
307 generation,
308 pending_replies: (0..count).collect(),
309 }
310 }
311
312 pub(crate) fn complete(&mut self, generation: u64, request_index: usize) -> bool {
314 self.generation == generation
315 && self.pending_replies.remove(&request_index)
316 && self.pending_replies.is_empty()
317 }
318}
319
320pub(super) struct CompactionStatus {
322 pub(super) region_id: RegionId,
324 pub(super) version_control: VersionControlRef,
326 pub(super) access_layer: AccessLayerRef,
328 pub(super) active: ActiveCompaction,
330 pub(super) pending_request: Option<PendingCompaction>,
335 pub(super) pending_ddl_requests: Vec<SenderDdlRequest>,
341 pub(crate) executing_ddl: Option<DdlExecution>,
343}
344
345impl CompactionStatus {
346 pub(super) fn new(
348 region_id: RegionId,
349 version_control: VersionControlRef,
350 access_layer: AccessLayerRef,
351 ) -> CompactionStatus {
352 CompactionStatus {
353 region_id,
354 version_control,
355 access_layer,
356 active: ActiveCompaction::idle(),
357 pending_request: None,
358 pending_ddl_requests: Vec::new(),
359 executing_ddl: None,
360 }
361 }
362
363 #[cfg(test)]
364 pub(super) fn for_test(
365 region_id: RegionId,
366 version_control: VersionControlRef,
367 access_layer: AccessLayerRef,
368 ) -> Self {
369 let mut status = Self::new(region_id, version_control, access_layer);
370 status.active = ActiveCompaction::picking(0, Vec::new(), CompactionTrigger::Automatic);
371 status
372 }
373
374 pub(crate) fn is_compacting(&self) -> bool {
376 !matches!(self.active.phase, CompactionPhase::Idle)
377 }
378
379 pub(crate) fn has_ddl(&self) -> bool {
381 !self.pending_ddl_requests.is_empty() || self.executing_ddl.is_some()
382 }
383
384 pub(crate) fn become_idle(&mut self) {
386 self.active = ActiveCompaction::idle();
387 }
388
389 #[cfg(test)]
390 pub(super) fn start_picking(&mut self, plan_id: u64) {
391 self.start_picking_with_trigger(plan_id, CompactionTrigger::Automatic);
392 }
393
394 pub(super) fn start_picking_with_trigger(&mut self, plan_id: u64, trigger: CompactionTrigger) {
395 self.active.start_picking(plan_id, trigger);
396 }
397
398 pub(super) fn start_regular_picking(&mut self, plan_id: u64) {
399 self.active.start_regular_picking(plan_id);
400 }
401
402 pub(super) fn is_picking(&self, expected_plan_id: u64) -> bool {
403 self.active.is_picking(expected_plan_id)
404 }
405
406 pub(super) fn accept_plan(&self, expected_plan_id: u64) -> bool {
407 self.active.accept_plan(expected_plan_id)
408 }
409
410 pub(super) fn is_manual_compaction(&self) -> bool {
411 self.active.is_manual()
412 }
413
414 pub(super) fn matches_execution(&self, execution: &CompactionExecution) -> bool {
415 self.active.matches_execution(execution)
416 }
417
418 #[cfg(test)]
419 pub(super) fn start_local_task(&mut self) -> CancellableTaskState {
420 let state = CancellableTaskState::new();
421 let execution = CompactionExecution::new(0, CompactingFiles::empty());
422 let phase = CompactionPhase::Local {
423 state: state.clone(),
424 execution,
425 };
426 self.active.phase = phase;
427 state
428 }
429
430 #[cfg(test)]
431 pub(super) fn start_remote_task(&mut self) {
432 let execution = CompactionExecution::new(0, CompactingFiles::empty());
433 let phase = CompactionPhase::Remote { execution };
434 self.active.phase = phase;
435 }
436
437 pub(super) fn request_cancel(&mut self) -> RequestCancelResult {
438 self.active.request_cancel()
439 }
440
441 pub(super) fn mark_automatic_trigger(&mut self) {
442 self.active.mark_automatic_trigger();
443 }
444
445 pub(super) fn merge_waiter(&mut self, waiter: OptionOutputTx) {
447 self.active.merge_waiter(waiter);
448 }
449
450 pub(super) fn take_waiters(&mut self) -> Vec<OutputTx> {
451 std::mem::take(&mut self.active.waiters)
452 }
453
454 pub(super) fn extend_waiters(&mut self, waiters: Vec<OutputTx>) {
455 self.active.waiters.extend(waiters);
456 }
457
458 pub(super) fn set_phase(&mut self, phase: CompactionPhase) {
459 self.active.phase = phase;
460 }
461
462 pub(super) fn set_pending_request(&mut self, pending: PendingCompaction) {
464 if let Some(prev) = self.pending_request.replace(pending) {
465 debug!(
466 "Replace pending compaction options with new request {:?} for region: {}",
467 prev.options, self.region_id
468 );
469 prev.waiter.send(ManualCompactionOverrideSnafu.fail());
470 }
471 }
472
473 pub(super) fn on_failure(&mut self, err: Arc<Error>) {
474 let mut active = std::mem::replace(&mut self.active, ActiveCompaction::idle());
475 for waiter in active.waiters.drain(..) {
476 waiter.send(Err(err.clone()).context(CompactRegionSnafu {
477 region_id: self.region_id,
478 }));
479 }
480
481 if let Some(pending_compaction) = self.pending_request.take() {
482 pending_compaction
483 .waiter
484 .send(Err(err.clone()).context(CompactRegionSnafu {
485 region_id: self.region_id,
486 }));
487 }
488
489 for pending_ddl in self.pending_ddl_requests.drain(..) {
490 pending_ddl
491 .sender
492 .send(Err(err.clone()).context(CompactRegionSnafu {
493 region_id: self.region_id,
494 }));
495 }
496 }
497
498 #[must_use]
499 pub(super) fn on_cancel(&mut self) -> Vec<SenderDdlRequest> {
500 let mut active = std::mem::replace(&mut self.active, ActiveCompaction::idle());
501 for waiter in active.waiters.drain(..) {
502 waiter.send(CompactionCancelledSnafu.fail());
503 }
504
505 if let Some(pending_compaction) = self.pending_request.take() {
506 pending_compaction.waiter.send(
507 Err(Arc::new(CompactionCancelledSnafu.build())).context(CompactRegionSnafu {
508 region_id: self.region_id,
509 }),
510 );
511 }
512
513 std::mem::take(&mut self.pending_ddl_requests)
514 }
515
516 #[allow(clippy::too_many_arguments)]
518 pub(super) fn new_compaction_request(
519 &self,
520 request_sender: Sender<WorkerRequestWithTime>,
521 engine_config: Arc<MitoConfig>,
522 cache_manager: CacheManagerRef,
523 manifest_ctx: &ManifestContextRef,
524 listener: WorkerListener,
525 schema_metadata_manager: SchemaMetadataManagerRef,
526 max_parallelism: usize,
527 ) -> CompactionRequest {
528 let current_version = CompactionVersion::from(self.version_control.current().version);
529 let start_time = Instant::now();
530
531 CompactionRequest {
532 engine_config,
533 current_version,
534 access_layer: self.access_layer.clone(),
535 request_sender: request_sender.clone(),
536 start_time,
537 cache_manager,
538 manifest_ctx: manifest_ctx.clone(),
539 listener,
540 schema_metadata_manager,
541 max_parallelism,
542 }
543 }
544}
545
546pub(super) struct PendingCompaction {
548 pub(crate) options: compact_request::Options,
550 pub(crate) waiter: OptionOutputTx,
552 pub(crate) max_parallelism: usize,
554 pub(crate) time_range: Option<TimestampRange>,
556}
557
558#[cfg(test)]
559mod tests {
560 use store_api::storage::FileId;
561
562 use super::*;
563 use crate::compaction::test_util::new_file_handle;
564
565 #[test]
566 fn test_compaction_unit_input_leases_release_independently() {
567 let x = new_file_handle(FileId::random(), 0, 100, 0);
568 let y = new_file_handle(FileId::random(), 100, 200, 0);
569 let unused = new_file_handle(FileId::random(), 200, 300, 0);
570 let reserved = CompactingFiles::try_reserve([&x, &y, &x, &unused]).unwrap();
571 let first = reserved.for_inputs(&[x.clone(), x.clone()]);
572 let second = reserved.for_inputs(std::slice::from_ref(&y));
573 drop(reserved);
574 assert!(x.compacting() && y.compacting());
575 assert!(
576 !unused.compacting(),
577 "unit leases must not retain unrelated inputs"
578 );
579 drop(first);
580 assert!(!x.compacting() && y.compacting());
581 let notification = second.clone();
582 drop(second);
583 assert!(y.compacting());
584 drop(notification);
585 assert!(!y.compacting());
586 }
587}