Skip to main content

mito2/cache/
file_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 files.
16
17use std::fmt;
18use std::ops::Range;
19use std::sync::Arc;
20use std::time::{Duration, Instant};
21
22use bytes::Bytes;
23use common_base::readable_size::ReadableSize;
24use common_telemetry::{debug, error, info, warn};
25use futures::{AsyncWriteExt, FutureExt, TryStreamExt};
26use moka::future::Cache;
27use moka::notification::RemovalCause;
28use moka::policy::EvictionPolicy;
29use object_store::util::join_path;
30use object_store::{ErrorKind, ObjectStore, Reader};
31use parquet::file::metadata::{PageIndexPolicy, ParquetMetaData};
32use snafu::ResultExt;
33use store_api::storage::{FileId, RegionId};
34use tokio::sync::mpsc::{Sender, UnboundedReceiver};
35
36use crate::access_layer::TempFileCleaner;
37use crate::cache::{
38    CachedSstMeta, FILE_TYPE, INDEX_TYPE, SstMetaPreparation, decode_sst_meta, prepare_sst_meta,
39};
40use crate::error::{self, OpenDalSnafu, Result};
41use crate::metrics::{
42    CACHE_BYTES, CACHE_HIT, CACHE_MISS, WRITE_CACHE_DOWNLOAD_BYTES_TOTAL,
43    WRITE_CACHE_DOWNLOAD_ELAPSED,
44};
45use crate::region::opener::RegionLoadCacheTask;
46use crate::sst::parquet::helper::fetch_byte_ranges;
47use crate::sst::parquet::metadata::MetadataLoader;
48use crate::sst::parquet::reader::MetadataCacheMetrics;
49
50/// Subdirectory of cached files for write.
51///
52/// This must contain three layers, corresponding to [`build_prometheus_metrics_layer`](object_store::layers::build_prometheus_metrics_layer).
53const FILE_DIR: &str = "cache/object/write/";
54
55/// Default percentage for index (puffin) cache (20% of total capacity).
56pub(crate) const DEFAULT_INDEX_CACHE_PERCENT: u8 = 20;
57
58/// Minimum capacity for each cache (512MB).
59const MIN_CACHE_CAPACITY: u64 = 512 * 1024 * 1024;
60
61/// Channel capacity for background download tasks.
62const DOWNLOAD_TASK_CHANNEL_SIZE: usize = 64;
63
64/// A task to download a file in the background.
65struct DownloadTask {
66    index_key: IndexKey,
67    remote_path: String,
68    remote_store: ObjectStore,
69    file_size: u64,
70}
71
72/// Inner struct for FileCache that can be used in spawned tasks.
73#[derive(Debug)]
74struct FileCacheInner {
75    /// Local store to cache files.
76    local_store: ObjectStore,
77    /// Index to track cached Parquet files.
78    parquet_index: Cache<IndexKey, IndexValue>,
79    /// Index to track cached Puffin files.
80    puffin_index: Cache<IndexKey, IndexValue>,
81}
82
83impl FileCacheInner {
84    /// Returns the appropriate memory index for the given file type.
85    fn memory_index(&self, file_type: FileType) -> &Cache<IndexKey, IndexValue> {
86        match file_type {
87            FileType::Parquet => &self.parquet_index,
88            FileType::Puffin { .. } => &self.puffin_index,
89        }
90    }
91
92    /// Returns the cache file path for the key.
93    fn cache_file_path(&self, key: IndexKey) -> String {
94        cache_file_path(FILE_DIR, key)
95    }
96
97    /// Puts a file into the cache index.
98    ///
99    /// The `WriteCache` should ensure the file is in the correct path.
100    async fn put(&self, key: IndexKey, value: IndexValue) {
101        CACHE_BYTES
102            .with_label_values(&[key.file_type.metric_label()])
103            .add(value.file_size.into());
104        let index = self.memory_index(key.file_type);
105        index.insert(key, value).await;
106
107        // Since files are large items, we run the pending tasks immediately.
108        index.run_pending_tasks().await;
109    }
110
111    /// Recovers the index from local store.
112    async fn recover(&self) -> Result<()> {
113        let now = Instant::now();
114        let mut lister = self
115            .local_store
116            .lister_with(FILE_DIR)
117            .await
118            .context(OpenDalSnafu)?;
119        // Use i64 for total_size to reduce the risk of overflow.
120        // It is possible that the total size of the cache is larger than i32::MAX.
121        let (mut total_size, mut total_keys) = (0i64, 0);
122        let (mut parquet_size, mut puffin_size) = (0i64, 0i64);
123        while let Some(entry) = lister.try_next().await.context(OpenDalSnafu)? {
124            let meta = entry.metadata();
125            if !meta.is_file() {
126                continue;
127            }
128            let Some(key) = parse_index_key(entry.name()) else {
129                continue;
130            };
131
132            let meta = self
133                .local_store
134                .stat(entry.path())
135                .await
136                .context(OpenDalSnafu)?;
137            let file_size = meta.content_length() as u32;
138            let index = self.memory_index(key.file_type);
139            index.insert(key, IndexValue { file_size }).await;
140            let size = i64::from(file_size);
141            total_size += size;
142            total_keys += 1;
143
144            // Track sizes separately for each file type
145            match key.file_type {
146                FileType::Parquet => parquet_size += size,
147                FileType::Puffin { .. } => puffin_size += size,
148            }
149        }
150        // The metrics is a signed int gauge so we can updates it finally.
151        CACHE_BYTES
152            .with_label_values(&[FILE_TYPE])
153            .add(parquet_size);
154        CACHE_BYTES
155            .with_label_values(&[INDEX_TYPE])
156            .add(puffin_size);
157
158        // Run all pending tasks of the moka cache so that the cache size is updated
159        // and the eviction policy is applied.
160        self.parquet_index.run_pending_tasks().await;
161        self.puffin_index.run_pending_tasks().await;
162
163        let parquet_weight = self.parquet_index.weighted_size();
164        let parquet_count = self.parquet_index.entry_count();
165        let puffin_weight = self.puffin_index.weighted_size();
166        let puffin_count = self.puffin_index.entry_count();
167        info!(
168            "Recovered file cache, num_keys: {}, num_bytes: {}, parquet(count: {}, weight: {}), puffin(count: {}, weight: {}), cost: {:?}",
169            total_keys,
170            total_size,
171            parquet_count,
172            parquet_weight,
173            puffin_count,
174            puffin_weight,
175            now.elapsed()
176        );
177        Ok(())
178    }
179
180    /// Downloads a file without cleaning up on error.
181    async fn download_without_cleaning(
182        &self,
183        index_key: IndexKey,
184        remote_path: &str,
185        remote_store: &ObjectStore,
186        file_size: u64,
187        concurrency: usize,
188    ) -> Result<()> {
189        const DOWNLOAD_READER_CHUNK_SIZE: ReadableSize = ReadableSize::mb(8);
190
191        let file_type = index_key.file_type;
192        let timer = WRITE_CACHE_DOWNLOAD_ELAPSED
193            .with_label_values(&[match file_type {
194                FileType::Parquet => "download_parquet",
195                FileType::Puffin { .. } => "download_puffin",
196            }])
197            .start_timer();
198
199        let reader = remote_store
200            .reader_with(remote_path)
201            .concurrent(concurrency)
202            .chunk(DOWNLOAD_READER_CHUNK_SIZE.as_bytes() as usize)
203            .await
204            .context(error::OpenDalSnafu)?
205            .into_futures_async_read(0..file_size)
206            .await
207            .context(error::OpenDalSnafu)?;
208
209        let cache_path = self.cache_file_path(index_key);
210        let mut writer = self
211            .local_store
212            .writer(&cache_path)
213            .await
214            .context(error::OpenDalSnafu)?
215            .into_futures_async_write();
216
217        let region_id = index_key.region_id;
218        let file_id = index_key.file_id;
219        let bytes_written =
220            futures::io::copy(reader, &mut writer)
221                .await
222                .context(error::DownloadSnafu {
223                    region_id,
224                    file_id,
225                    file_type,
226                })?;
227        writer.close().await.context(error::DownloadSnafu {
228            region_id,
229            file_id,
230            file_type,
231        })?;
232
233        WRITE_CACHE_DOWNLOAD_BYTES_TOTAL.inc_by(bytes_written);
234
235        let elapsed = timer.stop_and_record();
236        debug!(
237            "Successfully download file '{}' to local '{}', file size: {}, region: {}, cost: {:?}s",
238            remote_path, cache_path, bytes_written, region_id, elapsed,
239        );
240
241        let index_value = IndexValue {
242            file_size: bytes_written as _,
243        };
244        self.put(index_key, index_value).await;
245        Ok(())
246    }
247
248    /// Downloads a file from remote store to local cache.
249    async fn download(
250        &self,
251        index_key: IndexKey,
252        remote_path: &str,
253        remote_store: &ObjectStore,
254        file_size: u64,
255        concurrency: usize,
256    ) -> Result<()> {
257        if let Err(e) = self
258            .download_without_cleaning(index_key, remote_path, remote_store, file_size, concurrency)
259            .await
260        {
261            error!(e; "Failed to download file '{}' for region {}", remote_path, index_key.region_id);
262
263            let filename = index_key.to_string();
264            TempFileCleaner::clean_atomic_dir_files(&self.local_store, &[&filename]).await;
265
266            return Err(e);
267        }
268
269        Ok(())
270    }
271
272    /// Checks if the key is in the file cache.
273    fn contains_key(&self, key: &IndexKey) -> bool {
274        self.memory_index(key.file_type).contains_key(key)
275    }
276}
277
278/// A file cache manages files on local store and evict files based
279/// on size.
280#[derive(Debug, Clone)]
281pub(crate) struct FileCache {
282    /// Inner cache state shared with background worker.
283    inner: Arc<FileCacheInner>,
284    /// Capacity of the puffin (index) cache in bytes.
285    puffin_capacity: u64,
286    /// Channel for background download tasks. None if background worker is disabled.
287    download_task_tx: Option<Sender<DownloadTask>>,
288}
289
290pub(crate) type FileCacheRef = Arc<FileCache>;
291
292impl FileCache {
293    /// Splits the configured total capacity between parquet and puffin caches
294    /// without exceeding the requested overall budget.
295    fn split_cache_capacities(total_capacity: u64, index_percent: u8) -> (u64, u64) {
296        let desired_puffin_capacity = total_capacity * u64::from(index_percent) / 100;
297        let min_cache_capacity = MIN_CACHE_CAPACITY.min(total_capacity / 2);
298        let puffin_capacity =
299            desired_puffin_capacity.clamp(min_cache_capacity, total_capacity - min_cache_capacity);
300        let parquet_capacity = total_capacity - puffin_capacity;
301        (parquet_capacity, puffin_capacity)
302    }
303
304    /// Creates a new file cache.
305    pub(crate) fn new(
306        local_store: ObjectStore,
307        capacity: ReadableSize,
308        ttl: Option<Duration>,
309        index_cache_percent: Option<u8>,
310        enable_background_worker: bool,
311    ) -> FileCache {
312        // Validate and use the provided percent or default
313        let index_percent = index_cache_percent
314            .filter(|&percent| percent > 0 && percent < 100)
315            .unwrap_or(DEFAULT_INDEX_CACHE_PERCENT);
316        let total_capacity = capacity.as_bytes();
317
318        let (parquet_capacity, puffin_capacity) =
319            Self::split_cache_capacities(total_capacity, index_percent);
320
321        info!(
322            "Initializing file cache with index_percent: {}%, total_capacity: {}, parquet_capacity: {}, puffin_capacity: {}",
323            index_percent,
324            ReadableSize(total_capacity),
325            ReadableSize(parquet_capacity),
326            ReadableSize(puffin_capacity)
327        );
328
329        let parquet_index = Self::build_cache(local_store.clone(), parquet_capacity, ttl, "file");
330        let puffin_index = Self::build_cache(local_store.clone(), puffin_capacity, ttl, "index");
331
332        // Create inner cache shared with background worker
333        let inner = Arc::new(FileCacheInner {
334            local_store,
335            parquet_index,
336            puffin_index,
337        });
338
339        // Only create channel and spawn worker if background download is enabled
340        let download_task_tx = if enable_background_worker {
341            let (tx, rx) = tokio::sync::mpsc::channel(DOWNLOAD_TASK_CHANNEL_SIZE);
342            Self::spawn_download_worker(inner.clone(), rx);
343            Some(tx)
344        } else {
345            None
346        };
347
348        FileCache {
349            inner,
350            puffin_capacity,
351            download_task_tx,
352        }
353    }
354
355    /// Spawns a background worker to process download tasks.
356    fn spawn_download_worker(
357        inner: Arc<FileCacheInner>,
358        mut download_task_rx: tokio::sync::mpsc::Receiver<DownloadTask>,
359    ) {
360        tokio::spawn(async move {
361            info!("Background download worker started");
362            while let Some(task) = download_task_rx.recv().await {
363                // Check if the file is already in the cache
364                if inner.contains_key(&task.index_key) {
365                    debug!(
366                        "Skipping background download for region {}, file {} - already in cache",
367                        task.index_key.region_id, task.index_key.file_id
368                    );
369                    continue;
370                }
371
372                // Ignores background download errors.
373                let _ = inner
374                    .download(
375                        task.index_key,
376                        &task.remote_path,
377                        &task.remote_store,
378                        task.file_size,
379                        1, // Background downloads use concurrency=1
380                    )
381                    .await;
382            }
383            info!("Background download worker stopped");
384        });
385    }
386
387    /// Builds a cache for a specific file type.
388    fn build_cache(
389        local_store: ObjectStore,
390        capacity: u64,
391        ttl: Option<Duration>,
392        label: &'static str,
393    ) -> Cache<IndexKey, IndexValue> {
394        let cache_store = local_store;
395        let mut builder = Cache::builder()
396            .eviction_policy(EvictionPolicy::lru())
397            .weigher(|_key, value: &IndexValue| -> u32 {
398                // We only measure space on local store.
399                value.file_size
400            })
401            .max_capacity(capacity)
402            .async_eviction_listener(move |key, value, cause| {
403                let store = cache_store.clone();
404                // Stores files under FILE_DIR.
405                let file_path = cache_file_path(FILE_DIR, *key);
406                async move {
407                    if let RemovalCause::Replaced = cause {
408                        // The cache is replaced by another file (maybe download again). We don't remove the same
409                        // file but updates the metrics as the file is already replaced by users.
410                        CACHE_BYTES.with_label_values(&[label]).sub(value.file_size.into());
411                        return;
412                    }
413
414                    match store.delete(&file_path).await {
415                        Ok(()) => {
416                            CACHE_BYTES.with_label_values(&[label]).sub(value.file_size.into());
417                        }
418                        Err(e) => {
419                            warn!(e; "Failed to delete cached file {} for region {}", file_path, key.region_id);
420                        }
421                    }
422                }
423                .boxed()
424            });
425        if let Some(ttl) = ttl {
426            builder = builder.time_to_idle(ttl);
427        }
428        builder.build()
429    }
430
431    /// Puts a file into the cache index.
432    ///
433    /// The `WriteCache` should ensure the file is in the correct path.
434    pub(crate) async fn put(&self, key: IndexKey, value: IndexValue) {
435        self.inner.put(key, value).await
436    }
437
438    pub(crate) async fn get(&self, key: IndexKey) -> Option<IndexValue> {
439        self.inner.memory_index(key.file_type).get(&key).await
440    }
441
442    /// Reads a file from the cache.
443    #[allow(unused)]
444    pub(crate) async fn reader(&self, key: IndexKey) -> Option<Reader> {
445        // We must use `get()` to update the estimator of the cache.
446        // See https://docs.rs/moka/latest/moka/future/struct.Cache.html#method.contains_key
447        let index = self.inner.memory_index(key.file_type);
448        if index.get(&key).await.is_none() {
449            CACHE_MISS
450                .with_label_values(&[key.file_type.metric_label()])
451                .inc();
452            return None;
453        }
454
455        let file_path = self.inner.cache_file_path(key);
456        match self.get_reader(&file_path).await {
457            Ok(Some(reader)) => {
458                CACHE_HIT
459                    .with_label_values(&[key.file_type.metric_label()])
460                    .inc();
461                return Some(reader);
462            }
463            Err(e) => {
464                if e.kind() != ErrorKind::NotFound {
465                    warn!(e; "Failed to get file for key {:?}", key);
466                }
467            }
468            Ok(None) => {}
469        }
470
471        // We removes the file from the index.
472        index.remove(&key).await;
473        CACHE_MISS
474            .with_label_values(&[key.file_type.metric_label()])
475            .inc();
476        None
477    }
478
479    /// Reads ranges from the cache.
480    pub(crate) async fn read_ranges(
481        &self,
482        key: IndexKey,
483        ranges: &[Range<u64>],
484    ) -> Option<Vec<Bytes>> {
485        let index = self.inner.memory_index(key.file_type);
486        if index.get(&key).await.is_none() {
487            CACHE_MISS
488                .with_label_values(&[key.file_type.metric_label()])
489                .inc();
490            return None;
491        }
492
493        let file_path = self.inner.cache_file_path(key);
494        // In most cases, it will use blocking read,
495        // because FileCache is normally based on local file system, which supports blocking read.
496        let bytes_result =
497            fetch_byte_ranges(&file_path, self.inner.local_store.clone(), ranges).await;
498        match bytes_result {
499            Ok(bytes) => {
500                CACHE_HIT
501                    .with_label_values(&[key.file_type.metric_label()])
502                    .inc();
503                Some(bytes)
504            }
505            Err(e) => {
506                if e.kind() != ErrorKind::NotFound {
507                    warn!(e; "Failed to get file for key {:?}", key);
508                }
509
510                // We removes the file from the index.
511                index.remove(&key).await;
512                CACHE_MISS
513                    .with_label_values(&[key.file_type.metric_label()])
514                    .inc();
515                None
516            }
517        }
518    }
519
520    /// Removes a file from the cache explicitly.
521    /// It always tries to remove the file from the local store because we may not have the file
522    /// in the memory index if upload is failed.
523    pub(crate) async fn remove(&self, key: IndexKey) {
524        let file_path = self.inner.cache_file_path(key);
525        self.inner.memory_index(key.file_type).remove(&key).await;
526        // Always delete the file from the local store.
527        if let Err(e) = self.inner.local_store.delete(&file_path).await {
528            warn!(e; "Failed to delete a cached file {}", file_path);
529        }
530    }
531
532    /// Recovers the index from local store.
533    ///
534    /// If `task_receiver` is provided, spawns a background task after recovery
535    /// to process `RegionLoadCacheTask` messages for loading files into the cache.
536    pub(crate) async fn recover(
537        &self,
538        sync: bool,
539        task_receiver: Option<UnboundedReceiver<RegionLoadCacheTask>>,
540    ) {
541        let moved_self = self.clone();
542        let handle = tokio::spawn(async move {
543            if let Err(err) = moved_self.inner.recover().await {
544                error!(err; "Failed to recover file cache.")
545            }
546
547            // Spawns background task to process region load cache tasks after recovery.
548            // So it won't block the recovery when `sync` is true.
549            if let Some(mut receiver) = task_receiver {
550                info!("Spawning background task for processing region load cache tasks");
551                tokio::spawn(async move {
552                    while let Some(task) = receiver.recv().await {
553                        task.fill_cache(&moved_self).await;
554                    }
555                    info!("Background task for processing region load cache tasks stopped");
556                });
557            }
558        });
559
560        if sync {
561            let _ = handle.await;
562        }
563    }
564
565    /// Returns the cache file path for the key.
566    pub(crate) fn cache_file_path(&self, key: IndexKey) -> String {
567        self.inner.cache_file_path(key)
568    }
569
570    /// Returns the local store of the file cache.
571    pub(crate) fn local_store(&self) -> ObjectStore {
572        self.inner.local_store.clone()
573    }
574
575    /// Get the parquet metadata in file cache.
576    /// If the file is not in the cache or fail to load metadata, return None.
577    pub(crate) async fn get_parquet_meta_data(
578        &self,
579        key: IndexKey,
580        cache_metrics: &mut MetadataCacheMetrics,
581        page_index_policy: PageIndexPolicy,
582    ) -> Option<ParquetMetaData> {
583        // Check if file cache contains the key
584        if let Some(index_value) = self.inner.parquet_index.get(&key).await {
585            // Load metadata from file cache
586            let local_store = self.local_store();
587            let file_path = self.inner.cache_file_path(key);
588            let file_size = index_value.file_size as u64;
589            let mut metadata_loader = MetadataLoader::new(local_store, &file_path, file_size);
590            metadata_loader.with_page_index_policy(page_index_policy);
591
592            match metadata_loader.load(cache_metrics).await {
593                Ok(metadata) => {
594                    CACHE_HIT
595                        .with_label_values(&[key.file_type.metric_label()])
596                        .inc();
597                    Some(metadata)
598                }
599                Err(e) => {
600                    if !e.is_object_not_found() {
601                        warn!(
602                            e; "Failed to get parquet metadata for key {:?}",
603                            key
604                        );
605                    }
606                    // We removes the file from the index.
607                    self.inner.parquet_index.remove(&key).await;
608                    CACHE_MISS
609                        .with_label_values(&[key.file_type.metric_label()])
610                        .inc();
611                    None
612                }
613            }
614        } else {
615            CACHE_MISS
616                .with_label_values(&[key.file_type.metric_label()])
617                .inc();
618            None
619        }
620    }
621
622    /// Get fused SST metadata from the file cache.
623    /// If the file is not in the cache, or metadata loading/decoding fails, return None.
624    /// Compact cache encoding failures return decoded-only metadata to the caller.
625    pub(crate) async fn get_sst_meta_data(
626        &self,
627        key: IndexKey,
628        cache_metrics: &mut MetadataCacheMetrics,
629        page_index_policy: PageIndexPolicy,
630    ) -> Option<SstMetaPreparation> {
631        let file_path = self.inner.cache_file_path(key);
632        let metadata = self
633            .get_parquet_meta_data(key, cache_metrics, page_index_policy)
634            .await?;
635        match prepare_sst_meta(&file_path, metadata, None, page_index_policy).await {
636            Ok(metadata) => Some(metadata),
637            Err(err) => {
638                CACHE_MISS
639                    .with_label_values(&[key.file_type.metric_label()])
640                    .inc();
641                warn!(
642                    err; "Failed to prepare cached parquet metadata for key {:?}",
643                    key
644                );
645                None
646            }
647        }
648    }
649
650    /// Gets decoded SST metadata without preparing an in-memory cache entry.
651    pub(crate) async fn get_decoded_sst_meta_data(
652        &self,
653        key: IndexKey,
654        cache_metrics: &mut MetadataCacheMetrics,
655        page_index_policy: PageIndexPolicy,
656    ) -> Option<Arc<CachedSstMeta>> {
657        let file_path = self.inner.cache_file_path(key);
658        let metadata = self
659            .get_parquet_meta_data(key, cache_metrics, page_index_policy)
660            .await?;
661        match decode_sst_meta(&file_path, metadata, None, page_index_policy).await {
662            Ok(metadata) => Some(metadata),
663            Err(err) => {
664                CACHE_MISS
665                    .with_label_values(&[key.file_type.metric_label()])
666                    .inc();
667                warn!(
668                    err; "Failed to decode cached parquet metadata for key {:?}",
669                    key
670                );
671                None
672            }
673        }
674    }
675
676    async fn get_reader(&self, file_path: &str) -> object_store::Result<Option<Reader>> {
677        if self.inner.local_store.exists(file_path).await? {
678            Ok(Some(self.inner.local_store.reader(file_path).await?))
679        } else {
680            Ok(None)
681        }
682    }
683
684    /// Checks if the key is in the file cache.
685    pub(crate) fn contains_key(&self, key: &IndexKey) -> bool {
686        self.inner.contains_key(key)
687    }
688
689    /// Returns the capacity of the puffin (index) cache in bytes.
690    pub(crate) fn puffin_cache_capacity(&self) -> u64 {
691        self.puffin_capacity
692    }
693
694    /// Returns the current weighted size (used bytes) of the puffin (index) cache.
695    pub(crate) fn puffin_cache_size(&self) -> u64 {
696        self.inner.puffin_index.weighted_size()
697    }
698
699    /// Downloads a file in `remote_path` from the remote object store to the local cache
700    /// (specified by `index_key`).
701    pub(crate) async fn download(
702        &self,
703        index_key: IndexKey,
704        remote_path: &str,
705        remote_store: &ObjectStore,
706        file_size: u64,
707    ) -> Result<()> {
708        self.inner
709            .download(index_key, remote_path, remote_store, file_size, 8) // Foreground uses concurrency=8
710            .await
711    }
712
713    /// Downloads a file in `remote_path` from the remote object store to the local cache
714    /// (specified by `index_key`) in the background. Errors are logged but not returned.
715    ///
716    /// This method attempts to send a download task to the background worker.
717    /// If the channel is full, the task is silently dropped.
718    pub(crate) fn maybe_download_background(
719        &self,
720        index_key: IndexKey,
721        remote_path: String,
722        remote_store: ObjectStore,
723        file_size: u64,
724    ) {
725        // Do nothing if background worker is disabled (channel is None)
726        let Some(tx) = &self.download_task_tx else {
727            return;
728        };
729
730        let task = DownloadTask {
731            index_key,
732            remote_path,
733            remote_store,
734            file_size,
735        };
736
737        // Try to send the task; if the channel is full, just drop it
738        if let Err(e) = tx.try_send(task) {
739            debug!(
740                "Failed to queue background download task for region {}, file {}: {:?}",
741                index_key.region_id, index_key.file_id, e
742            );
743        }
744    }
745}
746
747/// Key of file cache index.
748#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
749pub struct IndexKey {
750    pub region_id: RegionId,
751    pub file_id: FileId,
752    pub file_type: FileType,
753}
754
755impl IndexKey {
756    /// Creates a new index key.
757    pub fn new(region_id: RegionId, file_id: FileId, file_type: FileType) -> IndexKey {
758        IndexKey {
759            region_id,
760            file_id,
761            file_type,
762        }
763    }
764}
765
766impl fmt::Display for IndexKey {
767    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
768        write!(
769            f,
770            "{}.{}.{}",
771            self.region_id.as_u64(),
772            self.file_id,
773            self.file_type
774        )
775    }
776}
777
778/// Type of the file.
779#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
780pub enum FileType {
781    /// Parquet file.
782    Parquet,
783    /// Puffin file.
784    Puffin(u64),
785}
786
787impl fmt::Display for FileType {
788    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
789        match self {
790            FileType::Parquet => write!(f, "parquet"),
791            FileType::Puffin(version) => write!(f, "{}.puffin", version),
792        }
793    }
794}
795
796impl FileType {
797    /// Parses the file type from string.
798    pub(crate) fn parse(s: &str) -> Option<FileType> {
799        match s {
800            "parquet" => Some(FileType::Parquet),
801            "puffin" => Some(FileType::Puffin(0)),
802            _ => {
803                // if post-fix with .puffin, try to parse the version
804                if let Some(version_str) = s.strip_suffix(".puffin") {
805                    let version = version_str.parse::<u64>().ok()?;
806                    Some(FileType::Puffin(version))
807                } else {
808                    None
809                }
810            }
811        }
812    }
813
814    /// Returns the metric label for this file type.
815    fn metric_label(&self) -> &'static str {
816        match self {
817            FileType::Parquet => FILE_TYPE,
818            FileType::Puffin(_) => INDEX_TYPE,
819        }
820    }
821}
822
823/// An entity that describes the file in the file cache.
824///
825/// It should only keep minimal information needed by the cache.
826#[derive(Debug, Clone)]
827pub(crate) struct IndexValue {
828    /// Size of the file in bytes.
829    pub(crate) file_size: u32,
830}
831
832/// Generates the path to the cached file.
833///
834/// The file name format is `{region_id}.{file_id}.{file_type}`
835fn cache_file_path(cache_file_dir: &str, key: IndexKey) -> String {
836    join_path(cache_file_dir, &key.to_string())
837}
838
839/// Parse index key from the file name.
840fn parse_index_key(name: &str) -> Option<IndexKey> {
841    let mut split = name.splitn(3, '.');
842    let region_id = split.next().and_then(|s| {
843        let id = s.parse::<u64>().ok()?;
844        Some(RegionId::from_u64(id))
845    })?;
846    let file_id = split.next().and_then(|s| FileId::parse_str(s).ok())?;
847    let file_type = split.next().and_then(FileType::parse)?;
848
849    Some(IndexKey::new(region_id, file_id, file_type))
850}
851
852#[cfg(test)]
853mod tests {
854    use common_test_util::temp_dir::create_temp_dir;
855    use object_store::services::Fs;
856
857    use super::*;
858
859    fn new_fs_store(path: &str) -> ObjectStore {
860        let builder = Fs::default().root(path);
861        ObjectStore::new(builder).unwrap().finish()
862    }
863
864    #[tokio::test]
865    async fn test_file_cache_ttl() {
866        let dir = create_temp_dir("");
867        let local_store = new_fs_store(dir.path().to_str().unwrap());
868
869        let cache = FileCache::new(
870            local_store.clone(),
871            ReadableSize::mb(10),
872            Some(Duration::from_millis(10)),
873            None,
874            true, // enable_background_worker
875        );
876        let region_id = RegionId::new(2000, 0);
877        let file_id = FileId::random();
878        let key = IndexKey::new(region_id, file_id, FileType::Parquet);
879        let file_path = cache.cache_file_path(key);
880
881        // Get an empty file.
882        assert!(cache.reader(key).await.is_none());
883
884        // Write a file.
885        local_store
886            .write(&file_path, b"hello".as_slice())
887            .await
888            .unwrap();
889
890        // Add to the cache.
891        cache
892            .put(
893                IndexKey::new(region_id, file_id, FileType::Parquet),
894                IndexValue { file_size: 5 },
895            )
896            .await;
897
898        let exist = cache.reader(key).await;
899        assert!(exist.is_some());
900        tokio::time::sleep(Duration::from_millis(15)).await;
901        cache.inner.parquet_index.run_pending_tasks().await;
902        let non = cache.reader(key).await;
903        assert!(non.is_none());
904    }
905
906    #[tokio::test]
907    async fn test_file_cache_basic() {
908        let dir = create_temp_dir("");
909        let local_store = new_fs_store(dir.path().to_str().unwrap());
910
911        let cache = FileCache::new(
912            local_store.clone(),
913            ReadableSize::mb(10),
914            None,
915            None,
916            true, // enable_background_worker
917        );
918        let region_id = RegionId::new(2000, 0);
919        let file_id = FileId::random();
920        let key = IndexKey::new(region_id, file_id, FileType::Parquet);
921        let file_path = cache.cache_file_path(key);
922
923        // Get an empty file.
924        assert!(cache.reader(key).await.is_none());
925
926        // Write a file.
927        local_store
928            .write(&file_path, b"hello".as_slice())
929            .await
930            .unwrap();
931        // Add to the cache.
932        cache
933            .put(
934                IndexKey::new(region_id, file_id, FileType::Parquet),
935                IndexValue { file_size: 5 },
936            )
937            .await;
938
939        // Read file content.
940        let reader = cache.reader(key).await.unwrap();
941        let buf = reader.read(..).await.unwrap().to_vec();
942        assert_eq!("hello", String::from_utf8(buf).unwrap());
943
944        // Get weighted size.
945        cache.inner.parquet_index.run_pending_tasks().await;
946        assert_eq!(5, cache.inner.parquet_index.weighted_size());
947
948        // Remove the file.
949        cache.remove(key).await;
950        assert!(cache.reader(key).await.is_none());
951
952        // Ensure all pending tasks of the moka cache is done before assertion.
953        cache.inner.parquet_index.run_pending_tasks().await;
954
955        // The file also not exists.
956        assert!(!local_store.exists(&file_path).await.unwrap());
957        assert_eq!(0, cache.inner.parquet_index.weighted_size());
958    }
959
960    #[tokio::test]
961    async fn test_file_cache_file_removed() {
962        let dir = create_temp_dir("");
963        let local_store = new_fs_store(dir.path().to_str().unwrap());
964
965        let cache = FileCache::new(
966            local_store.clone(),
967            ReadableSize::mb(10),
968            None,
969            None,
970            true, // enable_background_worker
971        );
972        let region_id = RegionId::new(2000, 0);
973        let file_id = FileId::random();
974        let key = IndexKey::new(region_id, file_id, FileType::Parquet);
975        let file_path = cache.cache_file_path(key);
976
977        // Write a file.
978        local_store
979            .write(&file_path, b"hello".as_slice())
980            .await
981            .unwrap();
982        // Add to the cache.
983        cache
984            .put(
985                IndexKey::new(region_id, file_id, FileType::Parquet),
986                IndexValue { file_size: 5 },
987            )
988            .await;
989
990        // Remove the file but keep the index.
991        local_store.delete(&file_path).await.unwrap();
992
993        // Reader is none.
994        assert!(cache.reader(key).await.is_none());
995        // Key is removed.
996        assert!(!cache.inner.parquet_index.contains_key(&key));
997    }
998
999    #[tokio::test]
1000    async fn test_file_cache_recover() {
1001        let dir = create_temp_dir("");
1002        let local_store = new_fs_store(dir.path().to_str().unwrap());
1003        let cache = FileCache::new(
1004            local_store.clone(),
1005            ReadableSize::mb(10),
1006            None,
1007            None,
1008            true, // enable_background_worker
1009        );
1010
1011        let region_id = RegionId::new(2000, 0);
1012        let file_type = FileType::Parquet;
1013        // Write N files.
1014        let file_ids: Vec<_> = (0..10).map(|_| FileId::random()).collect();
1015        let mut total_size = 0;
1016        for (i, file_id) in file_ids.iter().enumerate() {
1017            let key = IndexKey::new(region_id, *file_id, file_type);
1018            let file_path = cache.cache_file_path(key);
1019            let bytes = i.to_string().into_bytes();
1020            local_store.write(&file_path, bytes.clone()).await.unwrap();
1021
1022            // Add to the cache.
1023            cache
1024                .put(
1025                    IndexKey::new(region_id, *file_id, file_type),
1026                    IndexValue {
1027                        file_size: bytes.len() as u32,
1028                    },
1029                )
1030                .await;
1031            total_size += bytes.len();
1032        }
1033
1034        // Recover the cache.
1035        let cache = FileCache::new(
1036            local_store.clone(),
1037            ReadableSize::mb(10),
1038            None,
1039            None,
1040            true, // enable_background_worker
1041        );
1042        // No entry before recovery.
1043        assert!(
1044            cache
1045                .reader(IndexKey::new(region_id, file_ids[0], file_type))
1046                .await
1047                .is_none()
1048        );
1049        cache.recover(true, None).await;
1050
1051        // Check size.
1052        cache.inner.parquet_index.run_pending_tasks().await;
1053        assert_eq!(
1054            total_size,
1055            cache.inner.parquet_index.weighted_size() as usize
1056        );
1057
1058        for (i, file_id) in file_ids.iter().enumerate() {
1059            let key = IndexKey::new(region_id, *file_id, file_type);
1060            let reader = cache.reader(key).await.unwrap();
1061            let buf = reader.read(..).await.unwrap().to_vec();
1062            assert_eq!(i.to_string(), String::from_utf8(buf).unwrap());
1063        }
1064    }
1065
1066    #[tokio::test]
1067    async fn test_file_cache_read_ranges() {
1068        let dir = create_temp_dir("");
1069        let local_store = new_fs_store(dir.path().to_str().unwrap());
1070        let file_cache = FileCache::new(
1071            local_store.clone(),
1072            ReadableSize::mb(10),
1073            None,
1074            None,
1075            true, // enable_background_worker
1076        );
1077        let region_id = RegionId::new(2000, 0);
1078        let file_id = FileId::random();
1079        let key = IndexKey::new(region_id, file_id, FileType::Parquet);
1080        let file_path = file_cache.cache_file_path(key);
1081        // Write a file.
1082        let data = b"hello greptime database";
1083        local_store
1084            .write(&file_path, data.as_slice())
1085            .await
1086            .unwrap();
1087        // Add to the cache.
1088        file_cache.put(key, IndexValue { file_size: 5 }).await;
1089        // Ranges
1090        let ranges = vec![0..5, 6..10, 15..19, 0..data.len() as u64];
1091        let bytes = file_cache.read_ranges(key, &ranges).await.unwrap();
1092
1093        assert_eq!(4, bytes.len());
1094        assert_eq!(b"hello", bytes[0].as_ref());
1095        assert_eq!(b"grep", bytes[1].as_ref());
1096        assert_eq!(b"data", bytes[2].as_ref());
1097        assert_eq!(data, bytes[3].as_ref());
1098    }
1099
1100    #[test]
1101    fn test_file_cache_capacity_respects_total_budget() {
1102        let total_capacity = ReadableSize::mb(256).as_bytes();
1103        let (parquet_capacity, puffin_capacity) =
1104            FileCache::split_cache_capacities(total_capacity, 20);
1105
1106        assert_eq!(total_capacity, parquet_capacity + puffin_capacity);
1107        assert_eq!(ReadableSize::mb(128).as_bytes(), parquet_capacity);
1108        assert_eq!(ReadableSize::mb(128).as_bytes(), puffin_capacity);
1109    }
1110
1111    #[test]
1112    fn test_file_cache_capacity_keeps_split_when_total_allows_it() {
1113        let total_capacity = ReadableSize::gb(5).as_bytes();
1114        let (parquet_capacity, puffin_capacity) =
1115            FileCache::split_cache_capacities(total_capacity, 20);
1116
1117        assert_eq!(total_capacity, parquet_capacity + puffin_capacity);
1118        assert_eq!(ReadableSize::gb(4).as_bytes(), parquet_capacity);
1119        assert_eq!(ReadableSize::gb(1).as_bytes(), puffin_capacity);
1120    }
1121
1122    #[test]
1123    fn test_cache_file_path() {
1124        let file_id = FileId::parse_str("3368731b-a556-42b8-a5df-9c31ce155095").unwrap();
1125        assert_eq!(
1126            "test_dir/5299989643269.3368731b-a556-42b8-a5df-9c31ce155095.parquet",
1127            cache_file_path(
1128                "test_dir",
1129                IndexKey::new(RegionId::new(1234, 5), file_id, FileType::Parquet)
1130            )
1131        );
1132        assert_eq!(
1133            "test_dir/5299989643269.3368731b-a556-42b8-a5df-9c31ce155095.parquet",
1134            cache_file_path(
1135                "test_dir/",
1136                IndexKey::new(RegionId::new(1234, 5), file_id, FileType::Parquet)
1137            )
1138        );
1139    }
1140
1141    #[test]
1142    fn test_parse_file_name() {
1143        let file_id = FileId::parse_str("3368731b-a556-42b8-a5df-9c31ce155095").unwrap();
1144        let region_id = RegionId::new(1234, 5);
1145        assert_eq!(
1146            IndexKey::new(region_id, file_id, FileType::Parquet),
1147            parse_index_key("5299989643269.3368731b-a556-42b8-a5df-9c31ce155095.parquet").unwrap()
1148        );
1149        assert_eq!(
1150            IndexKey::new(region_id, file_id, FileType::Puffin(0)),
1151            parse_index_key("5299989643269.3368731b-a556-42b8-a5df-9c31ce155095.puffin").unwrap()
1152        );
1153        assert_eq!(
1154            IndexKey::new(region_id, file_id, FileType::Puffin(42)),
1155            parse_index_key("5299989643269.3368731b-a556-42b8-a5df-9c31ce155095.42.puffin")
1156                .unwrap()
1157        );
1158        assert!(parse_index_key("").is_none());
1159        assert!(parse_index_key(".").is_none());
1160        assert!(parse_index_key("5299989643269").is_none());
1161        assert!(parse_index_key("5299989643269.").is_none());
1162        assert!(parse_index_key(".5299989643269").is_none());
1163        assert!(parse_index_key("5299989643269.").is_none());
1164        assert!(parse_index_key("5299989643269.3368731b-a556-42b8-a5df").is_none());
1165        assert!(parse_index_key("5299989643269.3368731b-a556-42b8-a5df-9c31ce155095").is_none());
1166        assert!(
1167            parse_index_key("5299989643269.3368731b-a556-42b8-a5df-9c31ce155095.parque").is_none()
1168        );
1169        assert!(
1170            parse_index_key("5299989643269.3368731b-a556-42b8-a5df-9c31ce155095.parquet.puffin")
1171                .is_none()
1172        );
1173    }
1174}