Skip to main content

mito2/sst/
file_purger.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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
30/// A worker to delete files in background.
31pub trait FilePurger: Send + Sync + fmt::Debug {
32    /// Purges a retired SST and its indexes after all metadata views release their readers.
33    /// Each metadata view has already released its reader reference.
34    fn purge_file(&self, file_meta: FileMeta);
35
36    /// Retire cached scan results immediately, without waiting for physical deletion.
37    fn invalidate_file(&self, _: &FileMeta) {}
38
39    /// Returns the object-store reference manager used by SST metadata views.
40    /// Local and no-op purgers do not track GC references.
41    fn file_ref_manager(&self) -> Option<FileReferenceManagerRef> {
42        None
43    }
44}
45
46pub type FilePurgerRef = Arc<dyn FilePurger>;
47
48/// A no-op file purger can be used in combination with reading SST files outside of this region.
49#[derive(Debug)]
50pub struct NoopFilePurger;
51
52impl FilePurger for NoopFilePurger {
53    fn purge_file(&self, _file_meta: FileMeta) {}
54}
55
56/// Purger that purges file for current region.
57pub 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)))]
73/// Whether to enable GC for the file purger.
74pub 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))]
79/// Debug builds and unit tests use Fs to exercise object-store GC, including
80/// unit tests compiled with the release profile.
81pub fn should_enable_gc(global_gc_enabled: bool, _object_store_scheme: &'static str) -> bool {
82    global_gc_enabled
83}
84
85/// Creates a file purger based on the storage type of the access layer.
86/// Should be use in combination with Gc Worker.
87///
88/// If the storage is local file system, a `LocalFilePurger` is created, which deletes
89/// the files from both the storage and the cache.
90///
91/// If the storage is an object store, an `ObjectStoreFilePurger` is created, which
92/// only manages SST file references. Companion range indexes are deleted directly in either
93/// mode on final handle release when the SST is marked deleted and a deleter is provided.
94///
95pub 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    // Only enable GC for:
105    // - object store based storage
106    // - data or bare path type (metadata region doesn't need to be GCed)
107    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
124/// Creates a local file purger that deletes files from both the storage and the cache.
125pub 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    /// Creates a new purger.
140    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    /// Attaches deletion of companion range indexes.
154    pub fn with_range_index_deleter(mut self, deleter: Option<RangeIndexDeleter>) -> Self {
155        self.range_index_deleter = deleter;
156        self
157    }
158
159    /// Stop the scheduler of the file purger.
160    pub async fn stop_scheduler(&self) -> Result<()> {
161        self.scheduler.stop(true).await
162    }
163
164    /// Deletes the file(and it's index, if any) from cache and storage.
165    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
219/// Range indexes are deleted directly even when SST deletion is delegated to GC.
220fn 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            // The single worker finishes all earlier purge jobs before this barrier.
335            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        // Dropping one view must not remove files still needed by the other views.
379        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        // Regression coverage for final releases on different threads. The start barrier
387        // encourages overlap; it does not exhaustively control destructor interleavings.
388        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            // Invalidate before any old index-version reader releases the SST.
474            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            // mark file as deleted and drop the handle, we expect the sst file and the index file are deleted.
552            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}