1use std::collections::{BTreeMap, HashMap, HashSet};
25use std::sync::Arc;
26use std::time::Duration;
27
28use common_meta::datanode::GcStat;
29use common_telemetry::tracing::Instrument as _;
30use common_telemetry::{debug, error, info, warn};
31use common_time::Timestamp;
32use itertools::Itertools;
33use object_store::{Entry, ErrorKind, Lister};
34use serde::{Deserialize, Serialize};
35use snafu::{ResultExt as _, ensure};
36use store_api::storage::{FileId, FileRef, FileRefsManifest, GcReport, IndexVersion, RegionId};
37use tokio::sync::{OwnedSemaphorePermit, TryAcquireError};
38use tokio_stream::StreamExt;
39
40use crate::access_layer::AccessLayerRef;
41use crate::cache::CacheManagerRef;
42use crate::cache::file_cache::FileType;
43use crate::config::MitoConfig;
44use crate::engine::region_hook::{RegionGcInfo, RegionHookRef};
45use crate::error::{
46 DurationOutOfRangeSnafu, InvalidRequestSnafu, JoinSnafu, OpenDalSnafu, Result,
47 TooManyGcJobsSnafu, UnexpectedSnafu,
48};
49use crate::manifest::action::{RegionManifest, RemovedFile};
50use crate::metrics::{
51 GC_DELETE_FILE_CNT, GC_DURATION_SECONDS, GC_ERRORS_TOTAL, GC_FILES_DELETED_TOTAL,
52 GC_ORPHANED_INDEX_FILES, GC_RUNS_TOTAL, GC_SKIPPED_UNPARSABLE_FILES,
53};
54use crate::region::{MitoRegionRef, RegionRoleState};
55use crate::sst::file::{RegionFileId, RegionIndexId, delete_files, delete_indexes};
56use crate::sst::location::{self};
57use crate::worker::DROPPING_MARKER_FILE;
58
59#[cfg(test)]
60mod worker_test;
61
62fn should_delete_file(
65 is_in_manifest: bool,
66 is_in_tmp_ref: bool,
67 is_linger: bool,
68 is_eligible_for_delete: bool,
69 is_region_dropped: bool,
70 entry: &Entry,
71 unknown_file_may_linger_until: chrono::DateTime<chrono::Utc>,
72) -> bool {
73 if is_in_manifest || is_in_tmp_ref {
74 return false;
75 }
76
77 let is_known = is_linger || is_eligible_for_delete;
78 if is_known {
79 return is_eligible_for_delete;
80 }
81
82 if is_region_dropped {
90 return true;
91 }
92
93 entry
94 .metadata()
95 .last_modified()
96 .map(|ts| {
97 ts.into_inner().as_millisecond() < unknown_file_may_linger_until.timestamp_millis()
98 })
99 .unwrap_or(false)
100}
101
102pub struct GcLimiter {
104 pub gc_job_limit: Arc<tokio::sync::Semaphore>,
105 gc_concurrency: usize,
106}
107
108pub type GcLimiterRef = Arc<GcLimiter>;
109
110impl GcLimiter {
111 pub fn new(gc_concurrency: usize) -> Self {
112 Self {
113 gc_job_limit: Arc::new(tokio::sync::Semaphore::new(gc_concurrency)),
114 gc_concurrency,
115 }
116 }
117
118 pub fn running_gc_tasks(&self) -> u32 {
119 (self.gc_concurrency - self.gc_job_limit.available_permits()) as u32
120 }
121
122 pub fn gc_concurrency(&self) -> u32 {
123 self.gc_concurrency as u32
124 }
125
126 pub fn gc_stat(&self) -> GcStat {
127 GcStat::new(self.running_gc_tasks(), self.gc_concurrency())
128 }
129
130 pub fn permit(&self) -> Result<OwnedSemaphorePermit> {
134 self.gc_job_limit
135 .clone()
136 .try_acquire_owned()
137 .map_err(|e| match e {
138 TryAcquireError::Closed => UnexpectedSnafu {
139 reason: format!("Failed to acquire gc permit: {e}"),
140 }
141 .build(),
142 TryAcquireError::NoPermits => TooManyGcJobsSnafu {}.build(),
143 })
144 }
145}
146
147#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
148#[serde(default)]
149pub struct GcConfig {
150 pub enable: bool,
152 #[serde(with = "humantime_serde")]
157 pub lingering_time: Option<Duration>,
158 #[serde(with = "humantime_serde")]
163 pub unknown_file_lingering_time: Duration,
164 pub max_concurrent_lister_per_gc_job: usize,
167 pub max_concurrent_gc_job: usize,
171}
172
173impl Default for GcConfig {
174 fn default() -> Self {
175 Self {
176 enable: false,
177 lingering_time: Some(Duration::from_secs(60 * 60)),
179 unknown_file_lingering_time: Duration::from_secs(24 * 60 * 60),
183 max_concurrent_lister_per_gc_job: 32,
184 max_concurrent_gc_job: 4,
185 }
186 }
187}
188
189pub struct LocalGcWorker {
190 pub(crate) access_layer: AccessLayerRef,
191 pub(crate) cache_manager: Option<CacheManagerRef>,
192 pub(crate) regions: BTreeMap<RegionId, Option<MitoRegionRef>>,
193 pub(crate) opt: GcConfig,
195 pub(crate) file_ref_manifest: FileRefsManifest,
200 _permit: OwnedSemaphorePermit,
201 pub full_file_listing: bool,
210 pub(crate) hook: Option<RegionHookRef>,
213}
214
215pub struct ManifestOpenConfig {
216 pub compress_manifest: bool,
217 pub manifest_checkpoint_distance: u64,
218 pub experimental_manifest_keep_removed_file_count: usize,
219 pub experimental_manifest_keep_removed_file_ttl: Duration,
220}
221
222impl From<MitoConfig> for ManifestOpenConfig {
223 fn from(mito_config: MitoConfig) -> Self {
224 Self {
225 compress_manifest: mito_config.compress_manifest,
226 manifest_checkpoint_distance: mito_config.manifest_checkpoint_distance,
227 experimental_manifest_keep_removed_file_count: mito_config
228 .experimental_manifest_keep_removed_file_count,
229 experimental_manifest_keep_removed_file_ttl: mito_config
230 .experimental_manifest_keep_removed_file_ttl,
231 }
232 }
233}
234
235impl LocalGcWorker {
236 #[allow(clippy::too_many_arguments)]
240 pub async fn try_new(
241 access_layer: AccessLayerRef,
242 cache_manager: Option<CacheManagerRef>,
243 regions_to_gc: BTreeMap<RegionId, Option<MitoRegionRef>>,
244 opt: GcConfig,
245 file_ref_manifest: FileRefsManifest,
246 limiter: &GcLimiterRef,
247 full_file_listing: bool,
248 hook: Option<RegionHookRef>,
249 ) -> Result<Self> {
250 if let Some(first_region_id) = regions_to_gc.keys().next() {
251 let table_id = first_region_id.table_id();
252 for region_id in regions_to_gc.keys() {
253 ensure!(
254 region_id.table_id() == table_id,
255 InvalidRequestSnafu {
256 region_id: *region_id,
257 reason: format!(
258 "Region {} does not belong to table {}",
259 region_id, table_id
260 ),
261 }
262 );
263 }
264 }
265
266 let permit = limiter.permit()?;
267
268 Ok(Self {
269 access_layer,
270 cache_manager,
271 regions: regions_to_gc,
272 opt,
273 file_ref_manifest,
274 _permit: permit,
275 full_file_listing,
276 hook,
277 })
278 }
279
280 pub async fn read_tmp_ref_files(&self) -> Result<HashMap<RegionId, HashSet<FileRef>>> {
282 let mut tmp_ref_files = HashMap::new();
283 for (region_id, file_refs) in &self.file_ref_manifest.file_refs {
284 tmp_ref_files
285 .entry(*region_id)
286 .or_insert_with(HashSet::new)
287 .extend(file_refs.clone());
288 }
290
291 Ok(tmp_ref_files)
292 }
293
294 #[common_telemetry::tracing::instrument(
300 skip_all,
301 fields(region_count = self.regions.len(), full_file_listing = self.full_file_listing)
302 )]
303 pub async fn run(self) -> Result<GcReport> {
304 info!("LocalGcWorker started");
305 let _timer = GC_DURATION_SECONDS
306 .with_label_values(&["total"])
307 .start_timer();
308 let now = std::time::Instant::now();
309
310 let mut deleted_files = HashMap::new();
311 let mut deleted_indexes = HashMap::new();
312 let mut processed_regions = HashSet::new();
313 let mut need_retry_regions = HashSet::new();
314 let tmp_ref_files = self.read_tmp_ref_files().await?;
315 for (region_id, region) in &self.regions {
316 let per_region_time = std::time::Instant::now();
317 if region.as_ref().map(|r| r.manifest_ctx.current_state())
318 == Some(RegionRoleState::Follower)
319 {
320 return UnexpectedSnafu {
321 reason: format!(
322 "Region {} is in Follower state, should not run GC on follower regions",
323 region_id
324 ),
325 }
326 .fail();
327 }
328 let tmp_ref_files = tmp_ref_files
329 .get(region_id)
330 .cloned()
331 .unwrap_or_else(HashSet::new);
332 let outcome = self
333 .do_region_gc(*region_id, region.clone(), &tmp_ref_files)
334 .await?;
335 let RegionGcOutcome {
336 removed_files,
337 extension_cleanup_failed,
338 } = outcome;
339 let index_files = removed_files
340 .iter()
341 .filter_map(|f| f.index_version().map(|v| (f.file_id(), v)))
342 .collect_vec();
343 let data_files = removed_files
344 .into_iter()
345 .filter_map(|f| match f {
346 RemovedFile::File(file_id, _) => Some(file_id),
347 RemovedFile::Index(_, _) => None,
348 })
349 .collect();
350 if extension_cleanup_failed {
353 need_retry_regions.insert(*region_id);
354 } else {
355 deleted_files.insert(*region_id, data_files);
356 deleted_indexes.insert(*region_id, index_files);
357 processed_regions.insert(*region_id);
358 }
359 debug!(
360 "GC for region {} took {} secs.",
361 region_id,
362 per_region_time.elapsed().as_secs_f32()
363 );
364 }
365 info!(
366 "LocalGcWorker finished after {} secs.",
367 now.elapsed().as_secs_f32()
368 );
369 let report = GcReport {
370 deleted_files,
371 deleted_indexes,
372 need_retry_regions,
373 processed_regions,
374 };
375 Ok(report)
376 }
377}
378
379pub(crate) struct RegionGcOutcome {
385 pub removed_files: Vec<RemovedFile>,
387 pub extension_cleanup_failed: bool,
390}
391
392impl LocalGcWorker {
393 pub const CONCURRENCY_LIST_PER_FILES: usize = 1024;
396
397 #[common_telemetry::tracing::instrument(
406 skip_all,
407 fields(
408 region_id = %region_id,
409 full_file_listing = self.full_file_listing,
410 region_present = region.is_some()
411 )
412 )]
413 pub(crate) async fn do_region_gc(
414 &self,
415 region_id: RegionId,
416 region: Option<MitoRegionRef>,
417 tmp_ref_files: &HashSet<FileRef>,
418 ) -> Result<RegionGcOutcome> {
419 let mode = if self.full_file_listing {
420 "full_listing"
421 } else {
422 "fast"
423 };
424 GC_RUNS_TOTAL.with_label_values(&[mode]).inc();
425 debug!(
426 "Doing gc for region {}, {}",
427 region_id,
428 if region.is_some() {
429 "region found"
430 } else {
431 "region not found, might be dropped"
432 }
433 );
434
435 ensure!(
436 region.is_some() || self.full_file_listing,
437 InvalidRequestSnafu {
438 region_id,
439 reason: "region not found and full_file_listing is false; refusing GC without full listing".to_string(),
440 }
441 );
442
443 let manifest = if let Some(region) = ®ion {
444 let manifest = region.manifest_ctx.manifest().await;
445 let file_ref_manifest_version = self
447 .file_ref_manifest
448 .manifest_version
449 .get(®ion.region_id())
450 .cloned();
451 if file_ref_manifest_version != Some(manifest.manifest_version) {
452 warn!(
454 "Tmp ref files manifest version {:?} does not match region {} manifest version {}, skip gc for this region",
455 file_ref_manifest_version,
456 region.region_id(),
457 manifest.manifest_version
458 );
459 GC_ERRORS_TOTAL
460 .with_label_values(&["manifest_mismatch"])
461 .inc();
462 return Ok(RegionGcOutcome {
463 removed_files: vec![],
464 extension_cleanup_failed: false,
465 });
466 }
467 Some(manifest)
468 } else {
469 None
470 };
471
472 let all_entries = if let Some(manifest) = &manifest
473 && self.full_file_listing
474 {
475 self.list_from_object_store(region_id, manifest.files.len())
478 .await?
479 } else if manifest.is_none() && self.full_file_listing {
480 self.list_from_object_store(region_id, Self::CONCURRENCY_LIST_PER_FILES)
484 .await?
485 } else {
486 vec![]
487 };
488
489 let recently_removed_files = if let Some(manifest) = &manifest {
490 self.get_removed_files_expel_times(manifest).await?
491 } else {
492 Default::default()
493 };
494
495 if recently_removed_files.is_empty() {
496 debug!("No recently removed files to gc for region {}", region_id);
498 }
499
500 let removed_file_cnt = recently_removed_files
501 .values()
502 .map(|s| s.len())
503 .sum::<usize>();
504
505 let current_files = manifest.as_ref().map(|m| &m.files);
506
507 let in_manifest = if let Some(current_files) = current_files {
508 current_files
509 .iter()
510 .map(|(file_id, meta)| (*file_id, meta.index_version()))
511 .collect::<HashMap<_, _>>()
512 } else {
513 Default::default()
514 };
515
516 let is_region_dropped = region.is_none();
517
518 let in_tmp_ref = tmp_ref_files
519 .iter()
520 .map(|file_ref| (file_ref.file_id, file_ref.index_version))
521 .collect::<HashSet<_>>();
522
523 let deletable_files = self
524 .list_to_be_deleted_files(
525 region_id,
526 is_region_dropped,
527 &in_manifest,
528 &in_tmp_ref,
529 recently_removed_files,
530 all_entries,
531 )
532 .await?;
533
534 let unused_file_cnt = deletable_files.len();
535
536 info!(
537 "gc: for region{}{region_id}: In manifest file cnt: {}, Tmp ref file cnt: {}, recently removed files: {}, Unused files to delete count: {}",
538 if region.is_none() {
539 "(region dropped)"
540 } else {
541 ""
542 },
543 current_files.map(|c| c.len()).unwrap_or(0),
544 tmp_ref_files.len(),
545 removed_file_cnt,
546 deletable_files.len(),
547 );
548 debug!(
549 "gc: deletable files for region {}: {:?}",
550 region_id, &deletable_files
551 );
552
553 debug!(
554 "Found {} unused index files to delete for region {}",
555 deletable_files.len(),
556 region_id
557 );
558
559 let _delete_timer = GC_DURATION_SECONDS
560 .with_label_values(&["delete_files"])
561 .start_timer();
562 self.delete_files(region_id, &deletable_files).await?;
563
564 debug!(
565 "Successfully deleted {} unused files for region {}",
566 unused_file_cnt, region_id
567 );
568
569 let extension_cleanup_failed = if let Some(hook) = &self.hook
574 && (!deletable_files.is_empty() || self.full_file_listing)
575 {
576 let region_metadata = region.as_ref().map(|r| r.metadata());
577 let result = hook
578 .on_region_gc(
579 region_id,
580 region_metadata.as_ref(),
581 &self.access_layer,
582 &RegionGcInfo {
583 removed_files: &deletable_files,
584 is_region_dropped,
585 full_file_listing: self.full_file_listing,
586 },
587 )
588 .await;
589 if let Err(err) = result {
590 warn!(
591 err;
592 "Region hook on_region_gc failed for region {}, will retry on the next GC pass",
593 region_id
594 );
595 true
596 } else {
597 false
598 }
599 } else {
600 false
601 };
602
603 if !extension_cleanup_failed && let Some(region) = ®ion {
607 let _update_timer = GC_DURATION_SECONDS
608 .with_label_values(&["update_manifest"])
609 .start_timer();
610 self.update_manifest_removed_files(region, deletable_files.clone())
611 .await?;
612 }
613
614 Ok(RegionGcOutcome {
615 removed_files: deletable_files,
616 extension_cleanup_failed,
617 })
618 }
619
620 #[common_telemetry::tracing::instrument(
621 skip_all,
622 fields(region_id = %region_id, removed_file_count = removed_files.len())
623 )]
624 async fn delete_files(&self, region_id: RegionId, removed_files: &[RemovedFile]) -> Result<()> {
625 let mut index_ids = vec![];
626 let file_pairs = removed_files
627 .iter()
628 .filter_map(|f| match f {
629 RemovedFile::File(file_id, v) => Some((*file_id, v.unwrap_or(0))),
630 RemovedFile::Index(file_id, index_version) => {
631 let region_index_id =
632 RegionIndexId::new(RegionFileId::new(region_id, *file_id), *index_version);
633 index_ids.push(region_index_id);
634 None
635 }
636 })
637 .collect_vec();
638 delete_files(
639 region_id,
640 &file_pairs,
641 true,
642 &self.access_layer,
643 &self.cache_manager,
644 )
645 .await?;
646
647 if !file_pairs.is_empty() {
648 let deleted_count = file_pairs.len() as u64;
649 GC_FILES_DELETED_TOTAL
650 .with_label_values(&["parquet"])
651 .inc_by(deleted_count);
652 GC_DELETE_FILE_CNT.inc_by(deleted_count);
653 }
654
655 if !index_ids.is_empty() {
656 let deleted_count = index_ids.len() as u64;
657 delete_indexes(&index_ids, &self.access_layer, &self.cache_manager)
658 .await
659 .inspect_err(|_| {
660 GC_ERRORS_TOTAL.with_label_values(&["delete_failed"]).inc();
661 })?;
662 GC_FILES_DELETED_TOTAL
663 .with_label_values(&["index"])
664 .inc_by(deleted_count);
665 GC_DELETE_FILE_CNT.inc_by(deleted_count);
666 }
667
668 Ok(())
669 }
670
671 #[common_telemetry::tracing::instrument(
673 skip_all,
674 fields(region_id = %region.region_id(), deleted_file_count = deleted_files.len())
675 )]
676 async fn update_manifest_removed_files(
677 &self,
678 region: &MitoRegionRef,
679 deleted_files: Vec<RemovedFile>,
680 ) -> Result<()> {
681 let deleted_file_cnt = deleted_files.len();
682 debug!(
683 "Trying to update manifest for {deleted_file_cnt} removed files for region {}",
684 region.region_id()
685 );
686
687 let mut manager = region.manifest_ctx.manifest_manager.write().await;
688 let cnt = deleted_files.len();
689 manager.clear_deleted_files(deleted_files);
690 debug!(
691 "Updated region_id={} region manifest to clear {cnt} deleted files",
692 region.region_id(),
693 );
694
695 Ok(())
696 }
697
698 pub async fn get_removed_files_expel_times(
703 &self,
704 region_manifest: &Arc<RegionManifest>,
705 ) -> Result<BTreeMap<Timestamp, HashSet<RemovedFile>>> {
706 let mut ret = BTreeMap::new();
707 for files in ®ion_manifest.removed_files.removed_files {
708 let expel_time = Timestamp::new_millisecond(files.removed_at);
709 let set = ret.entry(expel_time).or_insert_with(HashSet::new);
710 set.extend(files.files.iter().cloned());
711 }
712
713 Ok(ret)
714 }
715
716 async fn partition_region_files(
719 &self,
720 region_id: RegionId,
721 concurrency: usize,
722 ) -> Result<Vec<(Lister, Option<String>)>> {
723 let region_dir = self.access_layer.build_region_dir(region_id);
724
725 let partitions = gen_partition_from_concurrency(concurrency);
726 let bounds = vec![None]
727 .into_iter()
728 .chain(partitions.iter().map(|p| Some(p.clone())))
729 .chain(vec![None])
730 .collect::<Vec<_>>();
731
732 let mut listers = vec![];
733 for part in bounds.windows(2) {
734 let start = part[0].clone();
735 let end = part[1].clone();
736 let mut lister = self.access_layer.object_store().lister_with(®ion_dir);
737 if let Some(s) = start {
738 lister = lister.start_after(&s);
739 }
740
741 let lister = lister.await.context(OpenDalSnafu)?;
742 listers.push((lister, end));
743 }
744
745 Ok(listers)
746 }
747
748 #[common_telemetry::tracing::instrument(
752 skip_all,
753 fields(region_id = %region_id, file_cnt_hint = file_cnt_hint)
754 )]
755 async fn list_from_object_store(
756 &self,
757 region_id: RegionId,
758 file_cnt_hint: usize,
759 ) -> Result<Vec<Entry>> {
760 let _timer = GC_DURATION_SECONDS
761 .with_label_values(&["list_files"])
762 .start_timer();
763 let start = tokio::time::Instant::now();
764 let concurrency = (file_cnt_hint / Self::CONCURRENCY_LIST_PER_FILES)
765 .max(1)
766 .min(self.opt.max_concurrent_lister_per_gc_job);
767
768 let listers = self
769 .partition_region_files(region_id, concurrency)
770 .await
771 .inspect_err(|_| {
772 GC_ERRORS_TOTAL.with_label_values(&["list_failed"]).inc();
773 })?;
774 let lister_cnt = listers.len();
775
776 let mut all_entries = self
778 .list_region_files_concurrent(listers)
779 .await
780 .inspect_err(|_| {
781 GC_ERRORS_TOTAL.with_label_values(&["list_failed"]).inc();
782 })?;
783 let root_cnt = all_entries.len();
784
785 let index_entries = self
789 .list_region_index_files(region_id)
790 .await
791 .inspect_err(|_| {
792 GC_ERRORS_TOTAL.with_label_values(&["list_failed"]).inc();
793 })?;
794 let index_cnt = index_entries.len();
795 all_entries.extend(index_entries);
796 info!(
797 "gc: full listing mode cost {} secs using {lister_cnt} lister for root={root_cnt} index={index_cnt} files in region {}.",
798 start.elapsed().as_secs_f64(),
799 region_id
800 );
801 Ok(all_entries)
802 }
803
804 async fn list_region_index_files(&self, region_id: RegionId) -> Result<Vec<Entry>> {
808 let region_dir = self.access_layer.build_region_dir(region_id);
809 let index_dir = object_store::util::join_dir(®ion_dir, "index");
810
811 let mut lister = match self
812 .access_layer
813 .object_store()
814 .lister_with(&index_dir)
815 .await
816 {
817 Ok(l) => l,
818 Err(e) if e.kind() == ErrorKind::NotFound => {
819 debug!(
823 "Index directory not found for region {}: {}. Treating as empty.",
824 region_id, e
825 );
826 return Ok(vec![]);
827 }
828 Err(e) => return Err(e).context(OpenDalSnafu),
829 };
830
831 let mut entries = Vec::new();
832 while let Some(entry) = lister.next().await {
833 let entry = entry.context(OpenDalSnafu)?;
834 if entry.metadata().is_file() && entry.name().ends_with(".puffin") {
835 entries.push(entry);
836 }
837 }
838
839 Ok(entries)
840 }
841
842 async fn list_region_files_concurrent(
845 &self,
846 listers: Vec<(Lister, Option<String>)>,
847 ) -> Result<Vec<Entry>> {
848 let (tx, mut rx) = tokio::sync::mpsc::channel(1024);
849 let mut handles = vec![];
850
851 for (lister, end) in listers {
852 let tx = tx.clone();
853 let handle = tokio::spawn(
854 async move {
855 let stream = lister.take_while(|e: &std::result::Result<Entry, _>| match e {
856 Ok(e) => {
857 if let Some(end) = &end {
858 e.name() < end.as_str()
860 } else {
861 true
863 }
864 }
865 Err(err) => {
868 warn!("Failed to list entry: {}", err);
869 true
870 }
871 });
872 let stream = stream
873 .filter(|e| {
874 if let Ok(e) = &e {
875 e.metadata().is_file()
877 } else {
878 true
880 }
881 })
882 .collect::<Vec<_>>()
883 .await;
884 tx.send(stream).await.expect("Failed to send entries");
886 }
887 .instrument(common_telemetry::tracing::info_span!("gc_list_partition")),
888 );
889
890 handles.push(handle);
891 }
892
893 for handle in handles {
895 handle.await.context(JoinSnafu)?;
896 }
897
898 drop(tx); let mut all_entries = vec![];
902 while let Some(stream) = rx.recv().await {
903 for entry in stream {
904 all_entries.push(entry.context(OpenDalSnafu)?);
905 }
906 }
907
908 Ok(all_entries)
909 }
910
911 #[allow(clippy::too_many_arguments)]
912 fn filter_deletable_files(
913 &self,
914 is_region_dropped: bool,
915 entries: Vec<Entry>,
916 in_manifest: &HashMap<FileId, Option<IndexVersion>>,
917 in_tmp_ref: &HashSet<(FileId, Option<IndexVersion>)>,
918 may_linger_files: &HashSet<&RemovedFile>,
919 eligible_for_delete: &HashSet<&RemovedFile>,
920 unknown_file_may_linger_until: chrono::DateTime<chrono::Utc>,
921 ) -> Vec<RemovedFile> {
922 let mut ready_for_delete = vec![];
923 let in_tmp_ref: HashMap<FileId, HashSet<IndexVersion>> =
925 in_tmp_ref
926 .iter()
927 .fold(HashMap::new(), |mut acc, (file, version)| {
928 let indices = acc.entry(*file).or_default();
929 if let Some(version) = version {
930 indices.insert(*version);
931 }
932 acc
933 });
934
935 let may_linger_files: HashMap<FileId, HashSet<&RemovedFile>> = may_linger_files
936 .iter()
937 .fold(HashMap::new(), |mut acc, file| {
938 let indices = acc.entry(file.file_id()).or_default();
939 indices.insert(file);
940 acc
941 });
942
943 let eligible_for_delete: HashMap<FileId, HashSet<&RemovedFile>> = eligible_for_delete
944 .iter()
945 .fold(HashMap::new(), |mut acc, file| {
946 let indices = acc.entry(file.file_id()).or_default();
947 indices.insert(file);
948 acc
949 });
950
951 for entry in entries {
952 if entry.name() == DROPPING_MARKER_FILE {
953 continue;
954 }
955
956 let (file_id, file_type) = match location::parse_file_id_type_from_path(entry.name()) {
957 Ok((file_id, file_type)) => (file_id, file_type),
958 Err(err) => {
959 error!(err; "Failed to parse file id from path: {}", entry.name());
960 GC_SKIPPED_UNPARSABLE_FILES.inc();
963 continue;
964 }
965 };
966
967 let should_delete = match file_type {
968 FileType::Parquet => {
969 let is_in_manifest = in_manifest.contains_key(&file_id);
970 let is_in_tmp_ref = in_tmp_ref.contains_key(&file_id);
971 let is_linger = may_linger_files.contains_key(&file_id);
972 let is_eligible_for_delete = eligible_for_delete.contains_key(&file_id);
973
974 should_delete_file(
975 is_in_manifest,
976 is_in_tmp_ref,
977 is_linger,
978 is_eligible_for_delete,
979 is_region_dropped,
980 &entry,
981 unknown_file_may_linger_until,
982 )
983 }
984 FileType::Puffin(version) => {
985 let is_in_manifest = in_manifest
987 .get(&file_id)
988 .map(|opt_ver| *opt_ver == Some(version))
989 .unwrap_or(false);
990 let is_in_tmp_ref = in_tmp_ref
991 .get(&file_id)
992 .map(|versions| versions.contains(&version))
993 .unwrap_or(false);
994 let is_linger = may_linger_files
995 .get(&file_id)
996 .map(|files| files.contains(&&RemovedFile::Index(file_id, version)))
997 .unwrap_or(false);
998 let is_eligible_for_delete = eligible_for_delete
999 .get(&file_id)
1000 .map(|files| files.contains(&&RemovedFile::Index(file_id, version)))
1001 .unwrap_or(false);
1002
1003 should_delete_file(
1004 is_in_manifest,
1005 is_in_tmp_ref,
1006 is_linger,
1007 is_eligible_for_delete,
1008 is_region_dropped,
1009 &entry,
1010 unknown_file_may_linger_until,
1011 )
1012 }
1013 };
1014
1015 if should_delete {
1016 let removed_file = match file_type {
1017 FileType::Parquet => {
1018 RemovedFile::File(file_id, None)
1021 }
1022 FileType::Puffin(version) => {
1023 GC_ORPHANED_INDEX_FILES.inc();
1024 RemovedFile::Index(file_id, version)
1025 }
1026 };
1027 ready_for_delete.push(removed_file);
1028 }
1029 }
1030 ready_for_delete
1031 }
1032
1033 pub async fn list_to_be_deleted_files(
1042 &self,
1043 region_id: RegionId,
1044 is_region_dropped: bool,
1045 in_manifest: &HashMap<FileId, Option<IndexVersion>>,
1046 in_tmp_ref: &HashSet<(FileId, Option<IndexVersion>)>,
1047 recently_removed_files: BTreeMap<Timestamp, HashSet<RemovedFile>>,
1048 all_entries: Vec<Entry>,
1049 ) -> Result<Vec<RemovedFile>> {
1050 let now = chrono::Utc::now();
1051 let may_linger_until = self
1052 .opt
1053 .lingering_time
1054 .map(|lingering_time| {
1055 chrono::Duration::from_std(lingering_time)
1056 .with_context(|_| DurationOutOfRangeSnafu {
1057 input: lingering_time,
1058 })
1059 .map(|t| now - t)
1060 })
1061 .transpose()?;
1062
1063 let unknown_file_may_linger_until = now
1064 - chrono::Duration::from_std(self.opt.unknown_file_lingering_time).with_context(
1065 |_| DurationOutOfRangeSnafu {
1066 input: self.opt.unknown_file_lingering_time,
1067 },
1068 )?;
1069
1070 let threshold =
1072 may_linger_until.map(|until| Timestamp::new_millisecond(until.timestamp_millis()));
1073 let mut recently_removed_files = recently_removed_files;
1076 let may_linger_files = match threshold {
1077 Some(threshold) => recently_removed_files.split_off(&threshold),
1078 None => BTreeMap::new(),
1079 };
1080 debug!("may_linger_files: {:?}", may_linger_files);
1081
1082 let all_may_linger_files = may_linger_files.values().flatten().collect::<HashSet<_>>();
1083
1084 let eligible_for_removal = recently_removed_files
1087 .values()
1088 .flatten()
1089 .collect::<HashSet<_>>();
1090
1091 if !self.full_file_listing {
1094 let files_to_delete: Vec<RemovedFile> = eligible_for_removal
1099 .iter()
1100 .filter(|file_id| {
1101 let in_use = match file_id {
1102 RemovedFile::File(file_id, index_version) => {
1103 in_manifest.get(file_id) == Some(index_version)
1104 || in_tmp_ref.contains(&(*file_id, *index_version))
1105 }
1106 RemovedFile::Index(file_id, index_version) => {
1107 in_manifest.get(file_id) == Some(&Some(*index_version))
1108 || in_tmp_ref.contains(&(*file_id, Some(*index_version)))
1109 }
1110 };
1111 !in_use
1112 })
1113 .map(|&f| f.clone())
1114 .collect();
1115
1116 info!(
1117 "gc: fast mode (no full listing) for region {}, found {} files to delete from manifest",
1118 region_id,
1119 files_to_delete.len()
1120 );
1121
1122 return Ok(files_to_delete);
1123 }
1124
1125 let all_unused_files_ready_for_delete = self.filter_deletable_files(
1129 is_region_dropped,
1130 all_entries,
1131 in_manifest,
1132 in_tmp_ref,
1133 &all_may_linger_files,
1134 &eligible_for_removal,
1135 unknown_file_may_linger_until,
1136 );
1137
1138 Ok(all_unused_files_ready_for_delete)
1139 }
1140}
1141
1142fn gen_partition_from_concurrency(concurrency: usize) -> Vec<String> {
1152 let n = concurrency.next_power_of_two();
1153 if n <= 1 {
1154 return vec![];
1155 }
1156
1157 let mut d = 0;
1161 let mut p: u128 = 1;
1162 while p < n as u128 {
1163 p *= 16;
1164 d += 1;
1165 }
1166
1167 let total_space = p;
1168 let step = total_space / n as u128;
1169
1170 (1..n)
1171 .map(|i| {
1172 let boundary = i as u128 * step;
1173 format!("{:0width$x}", boundary, width = d)
1174 })
1175 .collect()
1176}
1177
1178#[cfg(test)]
1179mod tests {
1180 use super::*;
1181
1182 #[test]
1183 fn test_gen_partition_from_concurrency() {
1184 let partitions = gen_partition_from_concurrency(1);
1185 assert!(partitions.is_empty());
1186
1187 let partitions = gen_partition_from_concurrency(2);
1188 assert_eq!(partitions, vec!["8"]);
1189
1190 let partitions = gen_partition_from_concurrency(3);
1191 assert_eq!(partitions, vec!["4", "8", "c"]);
1192
1193 let partitions = gen_partition_from_concurrency(4);
1194 assert_eq!(partitions, vec!["4", "8", "c"]);
1195
1196 let partitions = gen_partition_from_concurrency(8);
1197 assert_eq!(partitions, vec!["2", "4", "6", "8", "a", "c", "e"]);
1198
1199 let partitions = gen_partition_from_concurrency(16);
1200 assert_eq!(
1201 partitions,
1202 vec![
1203 "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"
1204 ]
1205 );
1206
1207 let partitions = gen_partition_from_concurrency(32);
1208 assert_eq!(
1209 partitions,
1210 [
1211 "08", "10", "18", "20", "28", "30", "38", "40", "48", "50", "58", "60", "68", "70",
1212 "78", "80", "88", "90", "98", "a0", "a8", "b0", "b8", "c0", "c8", "d0", "d8", "e0",
1213 "e8", "f0", "f8",
1214 ]
1215 );
1216 }
1217
1218 #[test]
1219 fn test_gc_config_default_lingering_time() {
1220 assert_eq!(
1221 GcConfig::default().lingering_time,
1222 Some(Duration::from_secs(60 * 60))
1223 );
1224 }
1225}