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(¤t_meta, ¤t_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(¤t_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}