Skip to main content

mito2/cache/
manifest_cache.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
15//! A cache for manifest files.
16
17use std::path::PathBuf;
18use std::sync::Arc;
19use std::time::{Duration, Instant};
20
21use common_base::readable_size::ReadableSize;
22use common_telemetry::{error, info, warn};
23use futures::{FutureExt, TryStreamExt};
24use moka::future::Cache;
25use moka::notification::RemovalCause;
26use moka::policy::EvictionPolicy;
27use object_store::ObjectStore;
28use object_store::util::join_path;
29use snafu::ResultExt;
30
31use crate::error::{OpenDalSnafu, Result};
32use crate::metrics::{CACHE_BYTES, CACHE_HIT, CACHE_MISS};
33
34/// Subdirectory of cached manifest files.
35///
36/// This must contain three layers, corresponding to [`build_prometheus_metrics_layer`](object_store::layers::build_prometheus_metrics_layer).
37const MANIFEST_DIR: &str = "cache/object/manifest/";
38
39/// Metric label for manifest files.
40const MANIFEST_TYPE: &str = "manifest";
41
42const CHUNK_SIZE: usize = 64 * 1024 * 1024; // 64MB
43
44/// A manifest cache manages manifest files on local store and evicts files based
45/// on size.
46#[derive(Debug, Clone)]
47pub struct ManifestCache {
48    /// Local store to cache files.
49    local_store: ObjectStore,
50    /// Index to track cached manifest files.
51    index: Cache<String, IndexValue>,
52}
53
54impl ManifestCache {
55    /// Creates a new manifest cache and recovers the index from local store.
56    pub async fn new(
57        local_store: ObjectStore,
58        capacity: ReadableSize,
59        ttl: Option<Duration>,
60        recover_sync: bool,
61    ) -> ManifestCache {
62        let total_capacity = capacity.as_bytes();
63
64        info!(
65            "Initializing manifest cache with capacity: {}",
66            ReadableSize(total_capacity)
67        );
68
69        let index = Self::build_cache(local_store.clone(), total_capacity, ttl);
70
71        let cache = ManifestCache { local_store, index };
72
73        // Recovers the cache index from local store.
74        cache.recover(recover_sync).await;
75
76        cache
77    }
78
79    /// Builds the cache.
80    fn build_cache(
81        local_store: ObjectStore,
82        capacity: u64,
83        ttl: Option<Duration>,
84    ) -> Cache<String, IndexValue> {
85        let cache_store = local_store;
86        let mut builder = Cache::builder()
87            .eviction_policy(EvictionPolicy::lru())
88            .weigher(|key: &String, value: &IndexValue| -> u32 {
89                key.len() as u32 + value.file_size
90            })
91            .max_capacity(capacity)
92            .async_eviction_listener(move |key: Arc<String>, value: IndexValue, cause| {
93                let store = cache_store.clone();
94                // Stores files under MANIFEST_DIR.
95                let file_path = join_path(MANIFEST_DIR, &key);
96                async move {
97                    if let RemovalCause::Replaced = cause {
98                        // The cache is replaced by another file. We don't remove the same
99                        // file but updates the metrics as the file is already replaced by users.
100                        CACHE_BYTES
101                            .with_label_values(&[MANIFEST_TYPE])
102                            .sub(value.file_size.into());
103                        return;
104                    }
105
106                    match store.delete(&file_path).await {
107                        Ok(()) => {
108                            CACHE_BYTES
109                                .with_label_values(&[MANIFEST_TYPE])
110                                .sub(value.file_size.into());
111                        }
112                        Err(e) => {
113                            warn!(e; "Failed to delete cached manifest file {}", file_path);
114                        }
115                    }
116                }
117                .boxed()
118            });
119        if let Some(ttl) = ttl {
120            builder = builder.time_to_idle(ttl);
121        }
122        builder.build()
123    }
124
125    /// Puts a file into the cache index.
126    ///
127    /// The caller should ensure the file is in the correct path.
128    pub(crate) async fn put(&self, key: String, value: IndexValue) {
129        CACHE_BYTES
130            .with_label_values(&[MANIFEST_TYPE])
131            .add(value.file_size.into());
132        self.index.insert(key, value).await;
133
134        // Since files can be large items, we run the pending tasks immediately.
135        self.index.run_pending_tasks().await;
136    }
137
138    /// Gets the index value for the key.
139    pub(crate) async fn get(&self, key: &str) -> Option<IndexValue> {
140        self.index.get(key).await
141    }
142
143    /// Removes a file from the cache explicitly.
144    pub(crate) async fn remove(&self, key: &str) {
145        let file_path = self.cache_file_path(key);
146        self.index.remove(key).await;
147        // Always deletes the file from the local store.
148        if let Err(e) = self.local_store.delete(&file_path).await {
149            warn!(e; "Failed to delete a cached manifest file {}", file_path);
150        }
151    }
152
153    /// Removes multiple files from the cache in batch.
154    pub(crate) async fn remove_batch(&self, keys: &[String]) {
155        if keys.is_empty() {
156            return;
157        }
158
159        for key in keys {
160            self.index.remove(key).await;
161        }
162
163        let file_paths: Vec<String> = keys.iter().map(|key| self.cache_file_path(key)).collect();
164
165        if let Err(e) = self.local_store.delete_iter(file_paths).await {
166            warn!(e; "Failed to delete cached manifest files in batch");
167        }
168    }
169
170    async fn recover_inner(&self) -> Result<()> {
171        let now = Instant::now();
172        let mut lister = self
173            .local_store
174            .lister_with(MANIFEST_DIR)
175            .recursive(true)
176            .await
177            .context(OpenDalSnafu)?;
178        let (mut total_size, mut total_keys) = (0i64, 0);
179        while let Some(entry) = lister.try_next().await.context(OpenDalSnafu)? {
180            let meta = entry.metadata();
181            if !meta.is_file() {
182                continue;
183            }
184
185            let meta = self
186                .local_store
187                .stat(entry.path())
188                .await
189                .context(OpenDalSnafu)?;
190            let file_size = meta.content_length() as u32;
191            let key = entry.path().trim_start_matches(MANIFEST_DIR).to_string();
192            common_telemetry::debug!("Manifest cache recover {}, size: {}", key, file_size);
193            self.index.insert(key, IndexValue { file_size }).await;
194            let size = i64::from(file_size);
195            total_size += size;
196            total_keys += 1;
197        }
198        CACHE_BYTES
199            .with_label_values(&[MANIFEST_TYPE])
200            .add(total_size);
201
202        // Runs all pending tasks of the moka cache so that the cache size is updated
203        // and the eviction policy is applied.
204        self.index.run_pending_tasks().await;
205
206        let weight = self.index.weighted_size();
207        let count = self.index.entry_count();
208        info!(
209            "Recovered manifest cache, num_keys: {}, num_bytes: {}, count: {}, weight: {}, cost: {:?}",
210            total_keys,
211            total_size,
212            count,
213            weight,
214            now.elapsed()
215        );
216        Ok(())
217    }
218
219    /// Recovers the index from local store.
220    pub(crate) async fn recover(&self, sync: bool) {
221        let moved_self = self.clone();
222        let handle = tokio::spawn(async move {
223            if let Err(err) = moved_self.recover_inner().await {
224                error!(err; "Failed to recover manifest cache.")
225            }
226
227            moved_self.clean_empty_dirs(true).await;
228        });
229
230        if sync {
231            let _ = handle.await;
232        }
233    }
234
235    /// Returns the cache file path for the key.
236    pub(crate) fn cache_file_path(&self, key: &str) -> String {
237        join_path(MANIFEST_DIR, key)
238    }
239
240    /// Gets a manifest file from cache.
241    /// Returns the file data if found in cache, None otherwise.
242    pub(crate) async fn get_file(&self, key: &str) -> Option<Vec<u8>> {
243        if self.get(key).await.is_none() {
244            CACHE_MISS.with_label_values(&[MANIFEST_TYPE]).inc();
245            return None;
246        }
247
248        let cache_file_path = self.cache_file_path(key);
249        match self.local_store.read(&cache_file_path).await {
250            Ok(data) => {
251                CACHE_HIT.with_label_values(&[MANIFEST_TYPE]).inc();
252                Some(data.to_vec())
253            }
254            Err(e) => {
255                warn!(e; "Failed to read cached manifest file {}", cache_file_path);
256                CACHE_MISS.with_label_values(&[MANIFEST_TYPE]).inc();
257                None
258            }
259        }
260    }
261
262    /// Puts a manifest file into cache.
263    pub(crate) async fn put_file(&self, key: String, data: Vec<u8>) {
264        let cache_file_path = self.cache_file_path(&key);
265
266        if let Err(e) = self
267            .local_store
268            .write_with(&cache_file_path, data.clone())
269            .chunk(CHUNK_SIZE)
270            .await
271        {
272            warn!(e; "Failed to write manifest to cache {}", cache_file_path);
273            return;
274        }
275
276        let file_size = data.len() as u32;
277        self.put(key, IndexValue { file_size }).await;
278    }
279
280    /// Removes empty directories recursively under the manifest cache directory.
281    ///
282    /// If `check_mtime` is true, only removes directories that have not been modified
283    /// for at least 1 hour.
284    pub(crate) async fn clean_empty_dirs(&self, check_mtime: bool) {
285        info!("Clean empty dirs start");
286
287        let root = self.local_store.info().root();
288        let manifest_dir = PathBuf::from(root).join(MANIFEST_DIR);
289        let manifest_dir_clone = manifest_dir.clone();
290
291        let result = tokio::task::spawn_blocking(move || {
292            Self::clean_empty_dirs_sync(&manifest_dir_clone, check_mtime)
293        })
294        .await;
295
296        match result {
297            Ok(Ok(())) => {
298                info!("Clean empty dirs end");
299            }
300            Ok(Err(e)) => {
301                warn!(e; "Failed to clean empty directories under {}", manifest_dir.display());
302            }
303            Err(e) => {
304                warn!(e; "Failed to spawn blocking task for cleaning empty directories");
305            }
306        }
307    }
308
309    /// Removes all manifest files under the given directory from cache and cleans up empty directories.
310    pub(crate) async fn clean_manifests(&self, dir: &str) {
311        info!("Clean manifest cache for directory: {}", dir);
312
313        let cache_dir = join_path(MANIFEST_DIR, dir);
314        let mut lister = match self
315            .local_store
316            .lister_with(&cache_dir)
317            .recursive(true)
318            .await
319        {
320            Ok(lister) => lister,
321            Err(e) => {
322                warn!(e; "Failed to list manifest files under {}", cache_dir);
323                return;
324            }
325        };
326
327        let mut keys_to_remove = Vec::new();
328        loop {
329            match lister.try_next().await {
330                Ok(Some(entry)) => {
331                    let meta = entry.metadata();
332                    if meta.is_file() {
333                        keys_to_remove
334                            .push(entry.path().trim_start_matches(MANIFEST_DIR).to_string());
335                    }
336                }
337                Ok(None) => break,
338                Err(e) => {
339                    warn!(e; "Failed to read entry while listing {}", cache_dir);
340                    break;
341                }
342            }
343        }
344
345        info!(
346            "Going to remove files from manifest cache, files: {:?}",
347            keys_to_remove
348        );
349
350        // Removes all files from cache in batch
351        self.remove_batch(&keys_to_remove).await;
352
353        // Cleans up empty directories under the given dir
354        let root = self.local_store.info().root();
355        let dir_path = PathBuf::from(root).join(&cache_dir);
356        let dir_path_clone = dir_path.clone();
357
358        let result = tokio::task::spawn_blocking(move || {
359            Self::clean_empty_dirs_sync(&dir_path_clone, false)
360        })
361        .await;
362
363        match result {
364            Ok(Ok(())) => {
365                info!("Cleaned manifest cache for directory: {}", dir);
366            }
367            Ok(Err(e)) => {
368                warn!(e; "Failed to clean empty directories under {}", dir_path.display());
369            }
370            Err(e) => {
371                warn!(e; "Failed to spawn blocking task for cleaning empty directories");
372            }
373        }
374    }
375
376    /// Synchronously removes empty directories recursively.
377    ///
378    /// If `check_mtime` is true, only removes directories that have not been modified
379    /// for at least 1 hour.
380    fn clean_empty_dirs_sync(dir: &PathBuf, check_mtime: bool) -> std::io::Result<()> {
381        let is_empty = Self::remove_empty_dirs_recursive_sync(dir, check_mtime)?;
382        if is_empty {
383            if let Err(e) = std::fs::remove_dir(dir) {
384                if e.kind() != std::io::ErrorKind::NotFound {
385                    warn!(e; "Failed to remove empty root dir {}", dir.display());
386                    return Err(e);
387                } else {
388                    info!("Empty root dir not found before removal {}", dir.display());
389                }
390            } else {
391                info!(
392                    "Removed empty root dir {} from manifest cache",
393                    dir.display()
394                );
395            }
396        }
397        Ok(())
398    }
399
400    fn remove_empty_dirs_recursive_sync(dir: &PathBuf, check_mtime: bool) -> std::io::Result<bool> {
401        common_telemetry::debug!(
402            "Maybe remove empty dir: {:?}, check_mtime: {}",
403            dir,
404            check_mtime
405        );
406        let entries = match std::fs::read_dir(dir) {
407            Ok(entries) => entries,
408            Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
409                // Directory doesn't exist, treat as already removed (empty)
410                return Ok(true);
411            }
412            Err(e) => return Err(e),
413        };
414
415        let mut is_empty = true;
416        // Iterates all entries under the directory.
417        // We have to check all entries to clean up all empty subdirectories.
418        for entry in entries {
419            let entry = entry?;
420            let path = entry.path();
421            let metadata = std::fs::metadata(&path)?;
422
423            if metadata.is_dir() {
424                // Checks if we should skip this directory based on modification time
425                if check_mtime
426                    && let Ok(modified) = metadata.modified()
427                    && let Ok(elapsed) = modified.elapsed()
428                    && elapsed < Duration::from_secs(3600)
429                {
430                    common_telemetry::debug!("Skip directory by mtime, elapsed: {:?}", elapsed);
431                    // Only removes if not modified for at least 1 hour.
432                    is_empty = false;
433                    continue;
434                }
435
436                let subdir_empty = Self::remove_empty_dirs_recursive_sync(&path, check_mtime)?;
437                if subdir_empty {
438                    if let Err(e) = std::fs::remove_dir(&path) {
439                        if e.kind() != std::io::ErrorKind::NotFound {
440                            warn!(e; "Failed to remove empty directory {}", path.display());
441                            is_empty = false;
442                        } else {
443                            info!(
444                                "Empty directory {} not found before removal",
445                                path.display()
446                            );
447                        }
448                    } else {
449                        info!(
450                            "Removed empty directory {} from manifest cache",
451                            path.display()
452                        );
453                    }
454                } else {
455                    is_empty = false;
456                }
457            } else {
458                is_empty = false;
459            }
460        }
461
462        Ok(is_empty)
463    }
464}
465
466/// An entity that describes the file in the manifest cache.
467///
468/// It should only keep minimal information needed by the cache.
469#[derive(Debug, Clone)]
470pub(crate) struct IndexValue {
471    /// Size of the file in bytes.
472    pub(crate) file_size: u32,
473}
474
475#[cfg(test)]
476mod tests {
477    use common_test_util::temp_dir::create_temp_dir;
478    use object_store::services::Fs;
479
480    use super::*;
481
482    fn new_fs_store(path: &str) -> ObjectStore {
483        let builder = Fs::default().root(path);
484        ObjectStore::new(builder).unwrap().finish()
485    }
486
487    #[tokio::test]
488    async fn test_manifest_cache_basic() {
489        common_telemetry::init_default_ut_logging();
490
491        let dir = create_temp_dir("");
492        let local_store = new_fs_store(dir.path().to_str().unwrap());
493
494        let cache = ManifestCache::new(local_store.clone(), ReadableSize::mb(10), None, true).await;
495        let key = "region_1/manifest/00000000000000000007.json";
496        let file_path = cache.cache_file_path(key);
497
498        // Get an empty file.
499        assert!(cache.get(key).await.is_none());
500
501        // Write a file.
502        local_store
503            .write(&file_path, b"manifest content".as_slice())
504            .await
505            .unwrap();
506        // Add to the cache.
507        cache
508            .put(key.to_string(), IndexValue { file_size: 16 })
509            .await;
510
511        // Get the cached value.
512        let value = cache.get(key).await.unwrap();
513        assert_eq!(16, value.file_size);
514
515        // Get weighted size.
516        cache.index.run_pending_tasks().await;
517        assert_eq!(59, cache.index.weighted_size());
518
519        // Remove the file.
520        cache.remove(key).await;
521        cache.index.run_pending_tasks().await;
522        assert!(cache.get(key).await.is_none());
523
524        // Ensure all pending tasks of the moka cache is done before assertion.
525        cache.index.run_pending_tasks().await;
526
527        // The file also not exists.
528        assert!(!local_store.exists(&file_path).await.unwrap());
529        assert_eq!(0, cache.index.weighted_size());
530    }
531
532    #[tokio::test]
533    async fn test_manifest_cache_recover() {
534        common_telemetry::init_default_ut_logging();
535
536        let dir = create_temp_dir("");
537        let local_store = new_fs_store(dir.path().to_str().unwrap());
538        let cache = ManifestCache::new(local_store.clone(), ReadableSize::mb(10), None, true).await;
539
540        // Write some manifest files with different paths
541        let keys = [
542            "region_1/manifest/00000000000000000001.json",
543            "region_1/manifest/00000000000000000002.json",
544            "region_1/manifest/00000000000000000001.checkpoint",
545            "region_2/manifest/00000000000000000001.json",
546        ];
547
548        let mut total_size = 0;
549        for (i, key) in keys.iter().enumerate() {
550            let file_path = cache.cache_file_path(key);
551            let content = format!("manifest-{}", i).into_bytes();
552            local_store
553                .write(&file_path, content.clone())
554                .await
555                .unwrap();
556
557            // Add to the cache.
558            cache
559                .put(
560                    key.to_string(),
561                    IndexValue {
562                        file_size: content.len() as u32,
563                    },
564                )
565                .await;
566            total_size += content.len() + key.len();
567        }
568
569        // Create a new cache instance which will automatically recover from local store
570        let cache = ManifestCache::new(local_store.clone(), ReadableSize::mb(10), None, true).await;
571
572        // Check size.
573        cache.index.run_pending_tasks().await;
574        let total_cached = cache.index.weighted_size() as usize;
575        assert_eq!(total_size, total_cached);
576
577        // Verify all files
578        for (i, key) in keys.iter().enumerate() {
579            let value = cache.get(key).await.unwrap();
580            assert_eq!(format!("manifest-{}", i).len() as u32, value.file_size);
581        }
582    }
583
584    #[tokio::test]
585    async fn test_cache_file_path() {
586        let dir = create_temp_dir("");
587        let local_store = new_fs_store(dir.path().to_str().unwrap());
588        let cache = ManifestCache::new(local_store, ReadableSize::mb(10), None, true).await;
589
590        assert_eq!(
591            "cache/object/manifest/region_1/manifest/00000000000000000007.json",
592            cache.cache_file_path("region_1/manifest/00000000000000000007.json")
593        );
594        assert_eq!(
595            "cache/object/manifest/region_1/manifest/00000000000000000007.checkpoint",
596            cache.cache_file_path("region_1/manifest/00000000000000000007.checkpoint")
597        );
598    }
599
600    #[tokio::test]
601    async fn test_clean_empty_dirs_sync_no_mtime_check() {
602        common_telemetry::init_default_ut_logging();
603
604        let dir = create_temp_dir("");
605        let root = PathBuf::from(dir.path());
606
607        // Create a directory structure:
608        // root/
609        //   empty_dir1/
610        //   empty_dir2/
611        //     empty_subdir/
612        //   non_empty_dir/
613        //     file.txt
614        //   nested/
615        //     empty_subdir1/
616        //     non_empty_subdir/
617        //       file.txt
618
619        let empty_dir1 = root.join("empty_dir1");
620        let empty_dir2 = root.join("empty_dir2");
621        let empty_subdir = empty_dir2.join("empty_subdir");
622        let non_empty_dir = root.join("non_empty_dir");
623        let nested = root.join("nested");
624        let nested_empty = nested.join("empty_subdir1");
625        let nested_non_empty = nested.join("non_empty_subdir");
626
627        // Create directories
628        std::fs::create_dir_all(&empty_dir1).unwrap();
629        std::fs::create_dir_all(&empty_subdir).unwrap();
630        std::fs::create_dir_all(&non_empty_dir).unwrap();
631        std::fs::create_dir_all(&nested_empty).unwrap();
632        std::fs::create_dir_all(&nested_non_empty).unwrap();
633
634        // Create files in non-empty directories
635        std::fs::write(non_empty_dir.join("file.txt"), b"content").unwrap();
636        std::fs::write(nested_non_empty.join("file.txt"), b"content").unwrap();
637
638        // Verify initial state
639        assert!(empty_dir1.exists());
640        assert!(empty_dir2.exists());
641        assert!(empty_subdir.exists());
642        assert!(non_empty_dir.exists());
643        assert!(nested.exists());
644        assert!(nested_empty.exists());
645        assert!(nested_non_empty.exists());
646
647        // Clean empty directories with check_mtime = false
648        ManifestCache::clean_empty_dirs_sync(&root, false).unwrap();
649
650        // Verify empty directories are removed
651        assert!(!empty_dir1.exists());
652        assert!(!empty_dir2.exists());
653        assert!(!empty_subdir.exists());
654        assert!(!nested_empty.exists());
655
656        // Verify non-empty directories still exist
657        assert!(non_empty_dir.exists());
658        assert!(non_empty_dir.join("file.txt").exists());
659        assert!(nested.exists());
660        assert!(nested_non_empty.exists());
661        assert!(nested_non_empty.join("file.txt").exists());
662    }
663
664    #[tokio::test]
665    async fn test_clean_empty_dirs_sync_with_mtime_check() {
666        common_telemetry::init_default_ut_logging();
667
668        let dir = create_temp_dir("");
669        let root = PathBuf::from(dir.path());
670
671        // Create a directory structure with recently created empty directories
672        // root/
673        //   empty_dir1/
674        //   empty_dir2/
675        //     empty_subdir/
676        //   non_empty_dir/
677        //     file.txt
678
679        let empty_dir1 = root.join("empty_dir1");
680        let empty_dir2 = root.join("empty_dir2");
681        let empty_subdir = empty_dir2.join("empty_subdir");
682        let non_empty_dir = root.join("non_empty_dir");
683
684        // Create directories
685        std::fs::create_dir_all(&empty_dir1).unwrap();
686        std::fs::create_dir_all(&empty_subdir).unwrap();
687        std::fs::create_dir_all(&non_empty_dir).unwrap();
688
689        // Create file in non-empty directory
690        std::fs::write(non_empty_dir.join("file.txt"), b"content").unwrap();
691
692        // Verify initial state
693        assert!(empty_dir1.exists());
694        assert!(empty_dir2.exists());
695        assert!(empty_subdir.exists());
696        assert!(non_empty_dir.exists());
697
698        // Clean empty directories with check_mtime = true
699        // Since the directories were just created (mtime < 1 hour), they should NOT be removed
700        ManifestCache::clean_empty_dirs_sync(&root, true).unwrap();
701
702        // Verify empty directories are NOT removed (they're too recent)
703        assert!(empty_dir1.exists());
704        assert!(empty_dir2.exists());
705        assert!(empty_subdir.exists());
706
707        // Verify non-empty directory still exists
708        assert!(non_empty_dir.exists());
709        assert!(non_empty_dir.join("file.txt").exists());
710    }
711}