Skip to main content

datafusion_execution/cache/
cache_manager.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18use crate::cache::default_cache::DefaultCache;
19pub use crate::cache::{Cache, CacheValue, SchemaFingerprint, TableScopedPath};
20use datafusion_common::HashMap;
21use datafusion_common::heap_size::{DFHeapSize, DFHeapSizeCtx};
22use datafusion_common::{Result, Statistics};
23use datafusion_physical_expr_common::sort_expr::LexOrdering;
24use object_store::ObjectMeta;
25use object_store::path::Path;
26use std::any::Any;
27use std::fmt::{Debug, Formatter};
28use std::ops::Deref;
29use std::sync::Arc;
30use std::time::Duration;
31
32pub const DEFAULT_LIST_FILES_CACHE_MEMORY_LIMIT: usize = 1024 * 1024; // 1MiB
33
34pub const DEFAULT_LIST_FILES_CACHE_TTL: Option<Duration> = None; // Infinite
35
36pub const DEFAULT_FILE_STATISTICS_MEMORY_LIMIT: usize = 20 * 1024 * 1024; // 20MiB
37
38pub const DEFAULT_METADATA_CACHE_LIMIT: usize = 50 * 1024 * 1024; // 50M
39
40/// A cache for file statistics and orderings.
41///
42/// This cache stores [`CachedFileMetadata`] which includes:
43/// - File metadata for validation (size, last_modified)
44/// - Statistics for the file
45/// - Ordering information for the file
46///
47/// If enabled via [`CacheManagerConfig::with_file_statistics_cache`] this
48/// cache avoids inferring the same file statistics repeatedly during the
49/// session lifetime.
50///
51/// The typical usage pattern is:
52/// 1. Call `get(path)` to check for cached value
53/// 2. If `Some(cached)`, validate with
54///    `cached.is_valid_for(&current_meta, &current_schema_fingerprint)`
55/// 3. If invalid or missing, compute new value and call `put(path, new_value)`
56///
57/// See [`crate::runtime_env::RuntimeEnv`] for more details
58pub type FileStatisticsCache = dyn Cache<TableScopedPath, CachedFileMetadata>;
59
60/// A cache for storing the [`ObjectMeta`]s that result from listing a path.
61///
62/// Listing a path means doing an object store "list" operation or `ls`
63/// command on the local filesystem. This operation can be expensive,
64/// especially when done over remote object stores.
65///
66/// The cache key is always the table's base path, ensuring a stable cache key.
67/// The cached value is a [`CachedFileList`] containing the files and a timestamp.
68///
69/// Partition filtering is done after retrieval using [`CachedFileList::files_matching_prefix`].
70///
71/// See [`crate::runtime_env::RuntimeEnv`] for more details.
72pub type ListFilesCache = dyn Cache<TableScopedPath, CachedFileList>;
73
74/// A cache for storing file-embedded metadata.
75///
76/// This cache stores per-file metadata in the form of [`CachedFileMetadataEntry`],
77/// which includes the [`ObjectMeta`] for validation.
78///
79/// For example, the built in [`ListingTable`] uses this cache to avoid parsing
80/// Parquet footers multiple times for the same file.
81///
82/// The typical usage pattern is:
83/// 1. Call `get(path)` to check for cached value
84/// 2. If `Some(cached)`, validate with `cached.is_valid_for(&current_meta)`
85/// 3. If invalid or missing, compute new value and call `put(path, new_value)`
86///
87/// See [`crate::runtime_env::RuntimeEnv`] for more details.
88///
89/// [`ListingTable`]: https://docs.rs/datafusion/latest/datafusion/datasource/listing/struct.ListingTable.html
90pub type FileMetadataCache = dyn Cache<Path, CachedFileMetadataEntry>;
91
92/// Cached metadata for a file, including statistics and ordering.
93///
94/// This struct embeds the [`ObjectMeta`] used for cache validation,
95/// the `file_schema` fingerprint, cached statistics, and ordering information.
96#[derive(Debug, Clone, PartialEq, Eq)]
97pub struct CachedFileMetadata {
98    /// File metadata used for cache validation (size, last_modified).
99    pub meta: ObjectMeta,
100    /// Fingerprint of the `file_schema` used to compute `statistics`.
101    pub schema_fingerprint: Arc<SchemaFingerprint>,
102    /// Cached statistics for the file, if available.
103    pub statistics: Arc<Statistics>,
104    /// Cached ordering for the file.
105    pub ordering: Option<LexOrdering>,
106}
107
108impl CachedFileMetadata {
109    /// Create a new cached file metadata entry.
110    pub fn new(
111        meta: ObjectMeta,
112        schema_fingerprint: Arc<SchemaFingerprint>,
113        statistics: Arc<Statistics>,
114        ordering: Option<LexOrdering>,
115    ) -> Self {
116        Self {
117            meta,
118            schema_fingerprint,
119            statistics,
120            ordering,
121        }
122    }
123
124    /// Check if this cached entry is still valid for the given metadata.
125    ///
126    /// Returns true if the file size, last modified time, and schema match.
127    pub fn is_valid_for(
128        &self,
129        current_meta: &ObjectMeta,
130        current_schema_fingerprint: &Arc<SchemaFingerprint>,
131    ) -> bool {
132        self.meta.size == current_meta.size
133            && self.meta.last_modified == current_meta.last_modified
134            && (Arc::ptr_eq(&self.schema_fingerprint, current_schema_fingerprint)
135                || self.schema_fingerprint.as_ref()
136                    == current_schema_fingerprint.as_ref())
137    }
138}
139
140impl CacheValue for CachedFileMetadata {
141    fn size(&self) -> usize {
142        DFHeapSize::heap_size(self, &mut DFHeapSizeCtx::default())
143    }
144}
145
146impl DFHeapSize for CachedFileMetadata {
147    fn heap_size(&self, ctx: &mut DFHeapSizeCtx) -> usize {
148        self.meta.size.heap_size(ctx)
149            + self.meta.last_modified.heap_size(ctx)
150            + self.meta.version.heap_size(ctx)
151            + self.meta.e_tag.heap_size(ctx)
152            + self.meta.location.as_ref().heap_size(ctx)
153            + self.statistics.heap_size(ctx)
154        // Do not deep-count `schema_fingerprint`: each ListingTable shares one
155        // fingerprint across all cached files.
156        //TODO add ordering once LexOrdering/PhysicalExpr implements DFHeapSize
157    }
158}
159
160/// Cached file listing.
161///
162/// TTL expiration is handled internally by the cache implementation.
163#[derive(Debug, Clone, PartialEq)]
164pub struct CachedFileList {
165    /// The cached file list.
166    pub files: Arc<Vec<ObjectMeta>>,
167}
168
169impl CachedFileList {
170    /// Create a new cached file list.
171    pub fn new(files: Vec<ObjectMeta>) -> Self {
172        Self {
173            files: Arc::new(files),
174        }
175    }
176
177    /// Filter the files by prefix.
178    fn filter_by_prefix(&self, prefix: &Option<Path>) -> Vec<ObjectMeta> {
179        match prefix {
180            Some(prefix) => self
181                .files
182                .iter()
183                .filter(|meta| meta.location.as_ref().starts_with(prefix.as_ref()))
184                .cloned()
185                .collect(),
186            None => self.files.as_ref().clone(),
187        }
188    }
189
190    /// Returns files matching the given prefix.
191    ///
192    /// When prefix is `None`, returns a clone of the `Arc` (no data copy).
193    /// When filtering is needed, returns a new `Arc` with filtered results (clones each matching [`ObjectMeta`]).
194    pub fn files_matching_prefix(&self, prefix: &Option<Path>) -> Arc<Vec<ObjectMeta>> {
195        match prefix {
196            None => Arc::clone(&self.files),
197            Some(p) => Arc::new(self.filter_by_prefix(&Some(p.clone()))),
198        }
199    }
200}
201
202impl CacheValue for CachedFileList {
203    fn size(&self) -> usize {
204        self.files.capacity() * size_of::<ObjectMeta>()
205            + self
206                .files
207                .iter()
208                .map(meta_heap_bytes)
209                .reduce(|acc, b| acc + b)
210                .unwrap_or(0)
211    }
212}
213
214/// Calculates the number of bytes an [`ObjectMeta`] occupies in the heap.
215pub fn meta_heap_bytes(object_meta: &ObjectMeta) -> usize {
216    let mut size = object_meta.location.as_ref().len();
217
218    if let Some(e) = &object_meta.e_tag {
219        size += e.len();
220    }
221    if let Some(v) = &object_meta.version {
222        size += v.len();
223    }
224
225    size
226}
227
228impl Deref for CachedFileList {
229    type Target = Arc<Vec<ObjectMeta>>;
230    fn deref(&self) -> &Self::Target {
231        &self.files
232    }
233}
234
235impl From<Vec<ObjectMeta>> for CachedFileList {
236    fn from(files: Vec<ObjectMeta>) -> Self {
237        Self::new(files)
238    }
239}
240
241/// Generic file-embedded metadata used with [`FileMetadataCache`].
242///
243/// For example, Parquet footers and page metadata can be represented
244/// using this trait.
245///
246/// See [`crate::runtime_env::RuntimeEnv`] for more details
247pub trait FileMetadata: Any + Send + Sync {
248    /// Returns the file metadata as [`Any`] so that it can be downcast to a specific
249    /// implementation.
250    fn as_any(&self) -> &dyn Any;
251
252    /// Returns the size of the metadata in bytes.
253    fn memory_size(&self) -> usize;
254
255    /// Returns extra information about this entry
256    fn extra_info(&self) -> HashMap<String, String>;
257}
258
259/// Cached file metadata entry with validation information.
260#[derive(Clone)]
261pub struct CachedFileMetadataEntry {
262    /// File metadata used for cache validation (size, last_modified).
263    pub meta: ObjectMeta,
264    /// The cached file metadata.
265    pub file_metadata: Arc<dyn FileMetadata>,
266}
267
268impl CacheValue for CachedFileMetadataEntry {
269    fn size(&self) -> usize {
270        self.file_metadata.memory_size()
271    }
272}
273
274impl CachedFileMetadataEntry {
275    /// Create a new cached file metadata entry.
276    pub fn new(meta: ObjectMeta, file_metadata: Arc<dyn FileMetadata>) -> Self {
277        Self {
278            meta,
279            file_metadata,
280        }
281    }
282
283    /// Check if this cached entry is still valid for the given metadata.
284    pub fn is_valid_for(&self, current_meta: &ObjectMeta) -> bool {
285        self.meta.size == current_meta.size
286            && self.meta.last_modified == current_meta.last_modified
287    }
288}
289
290impl Debug for CachedFileMetadataEntry {
291    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
292        f.debug_struct("CachedFileMetadataEntry")
293            .field("meta", &self.meta)
294            .field("memory_size", &self.file_metadata.memory_size())
295            .finish()
296    }
297}
298
299/// Manages various caches used in DataFusion.
300///
301/// Following DataFusion design principles, DataFusion provides default cache
302/// implementations, while also allowing users to provide their own custom cache
303/// implementations by implementing the relevant traits.
304///
305/// See [`CacheManagerConfig`] for configuration options.
306#[derive(Debug)]
307pub struct CacheManager {
308    file_statistic_cache: Option<Arc<FileStatisticsCache>>,
309    list_files_cache: Option<Arc<ListFilesCache>>,
310    file_metadata_cache: Arc<FileMetadataCache>,
311}
312
313impl CacheManager {
314    pub fn try_new(config: &CacheManagerConfig) -> Result<Arc<Self>> {
315        let file_statistic_cache: Option<Arc<FileStatisticsCache>> =
316            match &config.file_statistics_cache {
317                Some(fsc) if config.file_statistics_cache_limit > 0 => {
318                    fsc.update_cache_limit(config.file_statistics_cache_limit);
319                    Some(Arc::clone(fsc))
320                }
321                None if config.file_statistics_cache_limit > 0 => Some(Arc::new(
322                    DefaultCache::<TableScopedPath, CachedFileMetadata>::new(
323                        config.file_statistics_cache_limit,
324                    )
325                    .with_name("DefaultFileStatisticsCache"),
326                )),
327                _ => None,
328            };
329
330        let list_files_cache: Option<Arc<ListFilesCache>> = match &config.list_files_cache
331        {
332            Some(lfc) if config.list_files_cache_limit > 0 => {
333                // the cache memory limit or ttl might have changed, ensure they are updated
334                lfc.update_cache_limit(config.list_files_cache_limit);
335                // Only update TTL if explicitly set in config, otherwise preserve the cache's existing TTL
336                if let Some(ttl) = config.list_files_cache_ttl {
337                    lfc.update_cache_ttl(Some(ttl));
338                }
339                Some(Arc::clone(lfc))
340            }
341            None if config.list_files_cache_limit > 0 => Some(Arc::new(
342                DefaultCache::<TableScopedPath, CachedFileList>::new_with_ttl(
343                    config.list_files_cache_limit,
344                    config.list_files_cache_ttl,
345                )
346                .with_name("DefaultListFilesCache"),
347            )),
348            _ => None,
349        };
350
351        let file_metadata_cache = config
352            .file_metadata_cache
353            .as_ref()
354            .map(Arc::clone)
355            .unwrap_or_else(|| {
356                Arc::new(
357                    DefaultCache::new(config.metadata_cache_limit)
358                        .with_name("DefaultFileMetadataCache"),
359                )
360            });
361
362        // the cache memory limit might have changed, ensure the limit is updated
363        file_metadata_cache.update_cache_limit(config.metadata_cache_limit);
364
365        Ok(Arc::new(CacheManager {
366            file_statistic_cache,
367            list_files_cache,
368            file_metadata_cache,
369        }))
370    }
371
372    /// Get the file statistics cache.
373    pub fn get_file_statistic_cache(&self) -> Option<Arc<FileStatisticsCache>> {
374        self.file_statistic_cache.clone()
375    }
376
377    /// Get the memory limit of the file statistics cache.
378    pub fn get_file_statistic_cache_limit(&self) -> usize {
379        self.file_statistic_cache
380            .as_ref()
381            .map_or(0, |c| c.cache_limit())
382    }
383
384    /// Get the cache for storing the result of listing [`ObjectMeta`]s under the same path.
385    pub fn get_list_files_cache(&self) -> Option<Arc<ListFilesCache>> {
386        self.list_files_cache.clone()
387    }
388
389    /// Get the memory limit of the list files cache.
390    pub fn get_list_files_cache_limit(&self) -> usize {
391        self.list_files_cache
392            .as_ref()
393            .map_or(0, |c| c.cache_limit())
394    }
395
396    /// Get the TTL (time-to-live) of the list files cache.
397    pub fn get_list_files_cache_ttl(&self) -> Option<Duration> {
398        self.list_files_cache.as_ref().and_then(|c| c.cache_ttl())
399    }
400
401    /// Get the file embedded metadata cache.
402    pub fn get_file_metadata_cache(&self) -> Arc<FileMetadataCache> {
403        Arc::clone(&self.file_metadata_cache)
404    }
405
406    /// Get the limit of the file embedded metadata cache.
407    pub fn get_metadata_cache_limit(&self) -> usize {
408        self.file_metadata_cache.cache_limit()
409    }
410}
411
412#[derive(Clone)]
413pub struct CacheManagerConfig {
414    /// Enable caching of file statistics when listing files.
415    /// Enabling the cache avoids repeatedly reading file statistics in a DataFusion session.
416    /// Default is enabled. Currently only Parquet files are supported.
417    pub file_statistics_cache: Option<Arc<FileStatisticsCache>>,
418    /// Limit of the file statistics cache, in bytes. Default: 20MiB.
419    pub file_statistics_cache_limit: usize,
420    /// Enable caching of file metadata when listing files.
421    /// Enabling the cache avoids repeat list and object metadata fetch operations, which may be
422    /// expensive in certain situations (e.g. remote object storage), for objects under paths that
423    /// are cached.
424    /// Note that if this option is enabled, DataFusion will not see any updates to the underlying
425    /// storage for at least `list_files_cache_ttl` duration.
426    /// Default is enabled.
427    pub list_files_cache: Option<Arc<ListFilesCache>>,
428    /// Limit of the `list_files_cache`, in bytes. Default: 1MiB.
429    pub list_files_cache_limit: usize,
430    /// The duration the list files cache will consider an entry valid after insertion. Note that
431    /// changes to the underlying storage system, such as adding or removing data, will not be
432    /// visible until an entry expires. Default: None (infinite).
433    pub list_files_cache_ttl: Option<Duration>,
434    /// Cache of file-embedded metadata, used to avoid reading it multiple times when processing a
435    /// data file (e.g., Parquet footer and page metadata).
436    /// If not provided, the [`CacheManager`] will create it.
437    pub file_metadata_cache: Option<Arc<FileMetadataCache>>,
438    /// Limit of the file-embedded metadata cache, in bytes.
439    pub metadata_cache_limit: usize,
440}
441
442impl Default for CacheManagerConfig {
443    fn default() -> Self {
444        Self {
445            file_statistics_cache: Default::default(),
446            file_statistics_cache_limit: DEFAULT_FILE_STATISTICS_MEMORY_LIMIT,
447            list_files_cache: Default::default(),
448            list_files_cache_limit: DEFAULT_LIST_FILES_CACHE_MEMORY_LIMIT,
449            list_files_cache_ttl: DEFAULT_LIST_FILES_CACHE_TTL,
450            file_metadata_cache: Default::default(),
451            metadata_cache_limit: DEFAULT_METADATA_CACHE_LIMIT,
452        }
453    }
454}
455
456impl CacheManagerConfig {
457    /// Set the cache for file statistics.
458    pub fn with_file_statistics_cache(
459        mut self,
460        cache: Option<Arc<FileStatisticsCache>>,
461    ) -> Self {
462        self.file_statistics_cache = cache;
463        self
464    }
465
466    /// Specifies the memory limit for the file statistics cache, in bytes.
467    pub fn with_file_statistics_cache_limit(mut self, limit: usize) -> Self {
468        self.file_statistics_cache_limit = limit;
469        self
470    }
471
472    /// Set the cache for listing files.
473    ///
474    /// Default is `None` (disabled).
475    pub fn with_list_files_cache(mut self, cache: Option<Arc<ListFilesCache>>) -> Self {
476        self.list_files_cache = cache;
477        self
478    }
479
480    /// Sets the limit of the list files cache, in bytes.
481    ///
482    /// Default: 1MiB (1,048,576 bytes).
483    pub fn with_list_files_cache_limit(mut self, limit: usize) -> Self {
484        self.list_files_cache_limit = limit;
485        self
486    }
487
488    /// Sets the TTL (time-to-live) for entries in the list files cache.
489    ///
490    /// Default: None (infinite).
491    pub fn with_list_files_cache_ttl(mut self, ttl: Option<Duration>) -> Self {
492        self.list_files_cache_ttl = ttl;
493        self
494    }
495
496    /// Sets the cache for file-embedded metadata.
497    pub fn with_file_metadata_cache(
498        mut self,
499        cache: Option<Arc<FileMetadataCache>>,
500    ) -> Self {
501        self.file_metadata_cache = cache;
502        self
503    }
504
505    /// Sets the limit of the file-embedded metadata cache, in bytes.
506    pub fn with_metadata_cache_limit(mut self, limit: usize) -> Self {
507        self.metadata_cache_limit = limit;
508        self
509    }
510}
511
512#[cfg(test)]
513mod tests {
514    use super::*;
515
516    /// Test to verify that TTL is preserved when not explicitly set in config.
517    /// This fixes issue #19396 where TTL was being unset from DefaultListFilesCache
518    /// when CacheManagerConfig::list_files_cache_ttl was not set explicitly.
519    #[test]
520    fn test_ttl_preserved_when_not_set_in_config() {
521        // Create a cache with TTL = 1 second
522        let list_file_cache =
523            DefaultCache::new_with_ttl(1024, Some(Duration::from_secs(1)));
524
525        // Verify the cache has TTL set initially
526        assert_eq!(
527            list_file_cache.cache_ttl(),
528            Some(Duration::from_secs(1)),
529            "Cache should have TTL = 1 second initially"
530        );
531
532        // Put cache in config WITHOUT setting list_files_cache_ttl
533        let config = CacheManagerConfig::default()
534            .with_list_files_cache(Some(Arc::new(list_file_cache)));
535
536        // Create CacheManager from config
537        let cache_manager = CacheManager::try_new(&config).unwrap();
538
539        // Verify TTL is preserved (not unset)
540        let cache_ttl = cache_manager.get_list_files_cache().unwrap().cache_ttl();
541
542        assert!(
543            cache_ttl.is_some(),
544            "TTL should be preserved when not set in config. Expected Some(Duration::from_secs(1)), got {cache_ttl:?}"
545        );
546
547        // Verify it's the correct TTL value
548        assert_eq!(
549            cache_ttl,
550            Some(Duration::from_secs(1)),
551            "TTL should be exactly 1 second"
552        );
553    }
554
555    /// Test to verify that TTL can still be overridden when explicitly set in config.
556    #[test]
557    fn test_ttl_overridden_when_set_in_config() {
558        // Create a cache with TTL = 1 second
559        let list_file_cache =
560            DefaultCache::new_with_ttl(1024, Some(Duration::from_secs(1)));
561
562        // Put cache in config WITH a different TTL set
563        let config = CacheManagerConfig::default()
564            .with_list_files_cache(Some(Arc::new(list_file_cache)))
565            .with_list_files_cache_ttl(Some(Duration::from_secs(60)));
566
567        // Create CacheManager from config
568        let cache_manager = CacheManager::try_new(&config).unwrap();
569
570        // Verify TTL is overridden to the config value
571        let cache_ttl = cache_manager.get_list_files_cache().unwrap().cache_ttl();
572
573        assert_eq!(
574            cache_ttl,
575            Some(Duration::from_secs(60)),
576            "TTL should be overridden to 60 seconds when set in config"
577        );
578    }
579}