1use std::fmt;
16use std::sync::Arc;
17
18use common_telemetry::error;
19use store_api::region_request::PathType;
20use store_api::storage::FileId;
21
22use crate::access_layer::AccessLayerRef;
23use crate::cache::CacheManagerRef;
24use crate::error::Result;
25use crate::schedule::scheduler::SchedulerRef;
26use crate::sst::file::{FileMeta, delete_files};
27use crate::sst::file_ref::FileReferenceManagerRef;
28use crate::sst::range_index::RangeIndexDeleter;
29
30pub trait FilePurger: Send + Sync + fmt::Debug {
32 fn purge_file(&self, file_meta: FileMeta);
35
36 fn invalidate_file(&self, _: &FileMeta) {}
38
39 fn file_ref_manager(&self) -> Option<FileReferenceManagerRef> {
42 None
43 }
44}
45
46pub type FilePurgerRef = Arc<dyn FilePurger>;
47
48#[derive(Debug)]
50pub struct NoopFilePurger;
51
52impl FilePurger for NoopFilePurger {
53 fn purge_file(&self, _file_meta: FileMeta) {}
54}
55
56pub struct LocalFilePurger {
58 scheduler: SchedulerRef,
59 sst_layer: AccessLayerRef,
60 cache_manager: Option<CacheManagerRef>,
61 range_index_deleter: Option<RangeIndexDeleter>,
62}
63
64impl fmt::Debug for LocalFilePurger {
65 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66 f.debug_struct("LocalFilePurger")
67 .field("sst_layer", &self.sst_layer)
68 .finish()
69 }
70}
71
72#[cfg(not(any(debug_assertions, test)))]
73pub fn should_enable_gc(global_gc_enabled: bool, object_store_scheme: &'static str) -> bool {
75 global_gc_enabled && object_store_scheme != object_store::services::FS_SCHEME
76}
77
78#[cfg(any(debug_assertions, test))]
79pub fn should_enable_gc(global_gc_enabled: bool, _object_store_scheme: &'static str) -> bool {
82 global_gc_enabled
83}
84
85pub fn create_file_purger(
96 gc_enabled: bool,
97 path_type: PathType,
98 scheduler: SchedulerRef,
99 sst_layer: AccessLayerRef,
100 cache_manager: Option<CacheManagerRef>,
101 file_ref_manager: FileReferenceManagerRef,
102 range_index_deleter: Option<RangeIndexDeleter>,
103) -> FilePurgerRef {
104 if should_enable_gc(gc_enabled, sst_layer.object_store().info().scheme())
108 && matches!(path_type, PathType::Data | PathType::Bare)
109 {
110 Arc::new(ObjectStoreFilePurger {
111 cache_manager,
112 file_ref_manager,
113 scheduler,
114 range_index_deleter,
115 })
116 } else {
117 Arc::new(
118 LocalFilePurger::new(scheduler, sst_layer, cache_manager)
119 .with_range_index_deleter(range_index_deleter),
120 )
121 }
122}
123
124pub fn create_local_file_purger(
126 scheduler: SchedulerRef,
127 sst_layer: AccessLayerRef,
128 cache_manager: Option<CacheManagerRef>,
129 _file_ref_manager: FileReferenceManagerRef,
130 range_index_deleter: Option<RangeIndexDeleter>,
131) -> FilePurgerRef {
132 Arc::new(
133 LocalFilePurger::new(scheduler, sst_layer, cache_manager)
134 .with_range_index_deleter(range_index_deleter),
135 )
136}
137
138impl LocalFilePurger {
139 pub fn new(
141 scheduler: SchedulerRef,
142 sst_layer: AccessLayerRef,
143 cache_manager: Option<CacheManagerRef>,
144 ) -> Self {
145 Self {
146 scheduler,
147 sst_layer,
148 cache_manager,
149 range_index_deleter: None,
150 }
151 }
152
153 pub fn with_range_index_deleter(mut self, deleter: Option<RangeIndexDeleter>) -> Self {
155 self.range_index_deleter = deleter;
156 self
157 }
158
159 pub async fn stop_scheduler(&self) -> Result<()> {
161 self.scheduler.stop(true).await
162 }
163
164 fn delete_file(&self, file_meta: FileMeta) {
166 let sst_layer = self.sst_layer.clone();
167 let cache_manager = self.cache_manager.clone();
168 if let Err(e) = self.scheduler.schedule(Box::pin(async move {
169 if let Err(e) = delete_files(
170 file_meta.region_id,
171 &[(file_meta.file_id, file_meta.index_id().version)],
172 file_meta.exists_index(),
173 &sst_layer,
174 &cache_manager,
175 )
176 .await
177 {
178 error!(e; "Failed to delete file {:?} from storage", file_meta);
179 }
180 })) {
181 error!(e; "Failed to schedule the file purge request");
182 }
183 }
184}
185
186impl FilePurger for LocalFilePurger {
187 fn invalidate_file(&self, file_meta: &FileMeta) {
188 if let Some(cache) = &self.cache_manager {
189 cache.invalidate_range_results(file_meta.file_id);
190 }
191 }
192
193 fn purge_file(&self, file_meta: FileMeta) {
194 schedule_range_index_deletion(
195 &self.scheduler,
196 self.range_index_deleter.as_ref(),
197 file_meta.file_id,
198 );
199 self.delete_file(file_meta);
200 }
201}
202
203pub struct ObjectStoreFilePurger {
204 cache_manager: Option<CacheManagerRef>,
205 file_ref_manager: FileReferenceManagerRef,
206 scheduler: SchedulerRef,
207 range_index_deleter: Option<RangeIndexDeleter>,
208}
209
210impl fmt::Debug for ObjectStoreFilePurger {
211 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
212 f.debug_struct("ObjectStoreFilePurger")
213 .field("file_ref_manager", &self.file_ref_manager)
214 .field("range_index_deleter", &self.range_index_deleter)
215 .finish_non_exhaustive()
216 }
217}
218
219fn schedule_range_index_deletion(
221 scheduler: &SchedulerRef,
222 deleter: Option<&RangeIndexDeleter>,
223 file_id: FileId,
224) {
225 let Some(deleter) = deleter.cloned() else {
226 return;
227 };
228 if let Err(error) = scheduler.schedule(Box::pin(async move {
229 if let Err(error) = deleter.delete(file_id).await {
230 error!(error; "Failed to delete range index, file_id: {file_id}");
231 }
232 })) {
233 error!(error; "Failed to schedule range-index deletion, file_id: {file_id}");
234 }
235}
236
237impl FilePurger for ObjectStoreFilePurger {
238 fn invalidate_file(&self, file_meta: &FileMeta) {
239 if let Some(cache) = &self.cache_manager {
240 cache.invalidate_range_results(file_meta.file_id);
241 }
242 }
243
244 fn purge_file(&self, file_meta: FileMeta) {
245 schedule_range_index_deletion(
246 &self.scheduler,
247 self.range_index_deleter.as_ref(),
248 file_meta.file_id,
249 );
250 }
251
252 fn file_ref_manager(&self) -> Option<FileReferenceManagerRef> {
253 Some(self.file_ref_manager.clone())
254 }
255}
256
257#[cfg(test)]
258mod tests {
259 use std::num::NonZeroU64;
260 use std::sync::Barrier;
261 use std::thread;
262
263 use common_test_util::temp_dir::{TempDir, create_temp_dir};
264 use object_store::ObjectStore;
265 use object_store::services::Fs;
266 use smallvec::SmallVec;
267 use store_api::region_request::PathType;
268 use store_api::storage::{FileId, RegionId};
269
270 use super::*;
271 use crate::access_layer::AccessLayer;
272 use crate::schedule::scheduler::{LocalScheduler, Scheduler};
273 use crate::sst::file::{
274 ColumnIndexMetadata, FileHandle, FileMeta, FileTimeRange, IndexType, RegionFileId,
275 RegionIndexId,
276 };
277 use crate::sst::index::intermediate::IntermediateManager;
278 use crate::sst::index::puffin_manager::PuffinManagerFactory;
279 use crate::sst::location;
280
281 struct PurgeTestEnv {
282 layer: AccessLayerRef,
283 scheduler: Arc<LocalScheduler>,
284 file_id: RegionFileId,
285 _dir: TempDir,
286 }
287
288 impl PurgeTestEnv {
289 async fn new() -> Self {
290 common_telemetry::init_default_ut_logging();
291 let dir = create_temp_dir("file-purge");
292 let index_aux_path = dir.path().join("index_aux");
293 let puffin_mgr = PuffinManagerFactory::new(&index_aux_path, 4096, None, None)
294 .await
295 .unwrap();
296 let intm_mgr = IntermediateManager::init_fs(index_aux_path.to_str().unwrap())
297 .await
298 .unwrap();
299 let store =
300 ObjectStore::new(Fs::default().root(&dir.path().display().to_string())).unwrap();
301 let env = Self {
302 layer: Arc::new(AccessLayer::new(
303 "table1",
304 PathType::Bare,
305 store,
306 puffin_mgr,
307 intm_mgr,
308 )),
309 scheduler: Arc::new(LocalScheduler::new(1)),
310 file_id: RegionFileId::new(RegionId::new(0, 0), FileId::random()),
311 _dir: dir,
312 };
313 env.layer
314 .object_store()
315 .write(&env.sst_path(), vec![0; 4096])
316 .await
317 .unwrap();
318 env
319 }
320
321 fn sst_path(&self) -> String {
322 location::sst_file_path(self.layer.table_dir(), self.file_id, self.layer.path_type())
323 }
324
325 fn index_path(&self, version: u64) -> String {
326 location::index_file_path(
327 self.layer.table_dir(),
328 RegionIndexId::new(self.file_id, version),
329 self.layer.path_type(),
330 )
331 }
332
333 async fn wait_for_purges(&self) {
334 let (tx, rx) = tokio::sync::oneshot::channel();
336 self.scheduler
337 .schedule(Box::pin(async move {
338 tx.send(()).unwrap();
339 }))
340 .unwrap();
341 rx.await.unwrap();
342 }
343 }
344
345 #[rstest::rstest]
346 #[tokio::test]
347 async fn test_concurrent_index_view_release_keeps_live_files_and_purges_retired_files(
348 #[values(false, true)] retired: bool,
349 ) {
350 let env = PurgeTestEnv::new().await;
351 let store = env.layer.object_store();
352 let purger = Arc::new(LocalFilePurger::new(
353 env.scheduler.clone(),
354 env.layer.clone(),
355 None,
356 ));
357 let meta = FileMeta {
358 region_id: env.file_id.region_id(),
359 file_id: env.file_id.file_id(),
360 ..Default::default()
361 };
362 let old_reader = FileHandle::new(meta.clone(), purger);
363 let mut readers = vec![old_reader];
364 for index_version in 1..=2 {
365 store
366 .write(&env.index_path(index_version), "index")
367 .await
368 .unwrap();
369 readers.push(readers.last().unwrap().with_updated_index(FileMeta {
370 index_version,
371 available_indexes: smallvec::smallvec![IndexType::InvertedIndex],
372 ..meta.clone()
373 }));
374 }
375 if retired {
376 readers.last().unwrap().mark_deleted();
377 }
378 drop(readers.remove(1));
380 env.wait_for_purges().await;
381 assert!(store.exists(&env.sst_path()).await.unwrap());
382 for version in 1..=2 {
383 assert!(store.exists(&env.index_path(version)).await.unwrap());
384 }
385
386 let barrier = Barrier::new(readers.len());
389 thread::scope(|scope| {
390 for reader in readers {
391 let barrier = &barrier;
392 scope.spawn(move || {
393 barrier.wait();
394 drop(reader);
395 });
396 }
397 });
398 env.scheduler.stop(true).await.unwrap();
399 assert_eq!(store.exists(&env.sst_path()).await.unwrap(), !retired);
400 for version in 1..=2 {
401 assert_eq!(
402 store.exists(&env.index_path(version)).await.unwrap(),
403 !retired
404 );
405 }
406 }
407
408 #[rstest::rstest]
409 #[case(false)]
410 #[case(true)]
411 #[tokio::test]
412 async fn test_range_index_purge_on_handle_release(
413 #[case] gc_enabled: bool,
414 #[values(false, true)] is_delete: bool,
415 #[values(false, true)] update_index: bool,
416 ) {
417 let env = PurgeTestEnv::new().await;
418 let object_store = env.layer.object_store();
419 let path = env.sst_path();
420 let sst_file_id = env.file_id;
421 let scheduler = &env.scheduler;
422
423 let index_store = ObjectStore::new(object_store::services::Memory::default()).unwrap();
424 let owner = RegionId::new(9, 1);
425 let index_path = crate::sst::range_index::range_index_path(owner, sst_file_id.file_id());
426 index_store.write(&index_path, "range index").await.unwrap();
427 let cache = Arc::new(
428 crate::cache::CacheManager::builder()
429 .range_result_cache_size(1024 * 1024)
430 .build(),
431 );
432 let cache_key = crate::cache::test_util::range_cache_key(&[sst_file_id.file_id()]);
433 cache.put_range_result(
434 cache_key.clone(),
435 Arc::new(crate::cache::range_cache::RangeScanCacheValue::empty()),
436 );
437 let file_purger = create_file_purger(
438 gc_enabled,
439 PathType::Bare,
440 scheduler.clone(),
441 env.layer.clone(),
442 Some(cache.clone()),
443 Arc::new(crate::sst::file_ref::FileReferenceManager::new(None)),
444 Some(RangeIndexDeleter::new(index_store.clone(), owner)),
445 );
446
447 {
448 let meta = FileMeta {
449 region_id: sst_file_id.region_id(),
450 file_id: sst_file_id.file_id(),
451 file_size: 4096,
452 max_row_group_uncompressed_size: 4096,
453 ..Default::default()
454 };
455 let mut version = crate::sst::version::SstVersion::new(
456 crate::test_util::memtable_util::metadata_for_test(),
457 );
458 version.add_files(file_purger, [meta].into_iter());
459 let reader = version.levels()[0].files[&sst_file_id.file_id()].clone();
460 if update_index {
461 for index_version in 1..=2 {
462 let mut meta = reader.meta_ref().clone();
463 meta.index_version = index_version;
464 meta.available_indexes.push(IndexType::InvertedIndex);
465 let path = env.index_path(index_version);
466 object_store.write(&path, "index").await.unwrap();
467 version.add_files(reader.file_purger(), [meta].into_iter());
468 }
469 }
470 if is_delete {
471 version.mark_all_deleted();
472 }
473 assert_eq!(cache.get_range_result(&cache_key).is_none(), is_delete);
475 drop(version);
476 env.wait_for_purges().await;
477 assert!(object_store.exists(&path).await.unwrap());
478 assert!(index_store.exists(&index_path).await.unwrap());
479 if update_index {
480 for index_version in 1..=2 {
481 let path = env.index_path(index_version);
482 assert!(object_store.exists(&path).await.unwrap());
483 }
484 }
485 drop(reader);
486 }
487
488 scheduler.stop(true).await.unwrap();
489
490 assert_eq!(
491 object_store.exists(&path).await.unwrap(),
492 gc_enabled || !is_delete
493 );
494 assert_eq!(index_store.exists(&index_path).await.unwrap(), !is_delete);
495 if update_index {
496 for index_version in 1..=2 {
497 let path = env.index_path(index_version);
498 assert_eq!(
499 object_store.exists(&path).await.unwrap(),
500 gc_enabled || !is_delete
501 );
502 }
503 }
504 }
505
506 #[tokio::test]
507 async fn test_file_purge_with_index() {
508 let env = PurgeTestEnv::new().await;
509 let object_store = env.layer.object_store();
510 let sst_file_id = env.file_id;
511 let path = env.sst_path();
512 let index_path = env.index_path(0);
513 object_store
514 .write(&index_path, vec![0; 4096])
515 .await
516 .unwrap();
517
518 let scheduler = &env.scheduler;
519 let file_purger = Arc::new(LocalFilePurger::new(
520 scheduler.clone(),
521 env.layer.clone(),
522 None,
523 ));
524
525 {
526 let handle = FileHandle::new(
527 FileMeta {
528 region_id: sst_file_id.region_id(),
529 file_id: sst_file_id.file_id(),
530 time_range: FileTimeRange::default(),
531 level: 0,
532 file_size: 4096,
533 max_row_group_uncompressed_size: 4096,
534 available_indexes: SmallVec::from_iter([IndexType::InvertedIndex]),
535 indexes: vec![ColumnIndexMetadata {
536 column_id: 0,
537 created_indexes: SmallVec::from_iter([IndexType::InvertedIndex]),
538 ..Default::default()
539 }],
540 index_file_size: 4096,
541 index_version: 0,
542 num_rows: 1024,
543 num_row_groups: 1,
544 sequence: NonZeroU64::new(4096),
545 partition_expr: None,
546 num_series: 0,
547 ..Default::default()
548 },
549 file_purger,
550 );
551 handle.mark_deleted();
553 }
554
555 scheduler.stop(true).await.unwrap();
556
557 assert!(!object_store.exists(&path).await.unwrap());
558 assert!(!object_store.exists(&index_path).await.unwrap());
559 }
560}