Class FooterByteCache

java.lang.Object
org.elasticsearch.xpack.esql.datasources.cache.FooterByteCache

public class FooterByteCache extends Object
JVM-wide cache for file footer bytes (Parquet footers, ORC postscripts) shared across columnar format readers. Backed by the Elasticsearch Cache utility which provides:

Why a JVM-wide singleton instead of per-query / per-reader?

A per-query cache would avoid cross-query staleness entirely (no need for TTL) and is the cleaner design. However, the current architecture makes it impractical:
  1. A single query fans out into ~N independent split readers (one per thread), each of which creates its own ParquetStorageObjectAdapter or OrcStorageObjectAdapter. These adapters share no common parent or query-scoped context — they are created deep in the format reader call chain (FormatReader.read(StorageObject, ...)) with no mechanism to pass shared state between them.
  2. The thundering-herd benefit requires a single cache visible to all concurrent split readers. Without a shared object, each adapter would issue its own tail read — exactly the redundant I/O this cache exists to prevent.
When a query-scoped context is introduced into the format reader API, this cache should migrate from a static singleton to an instance held by that context. Until then, the access-based TTL (30Ls) bounds cross-query staleness.

Cache key design

The key is (path, fileLength). Adding lastModified was considered but rejected because range splits are created via StorageProvider.newObject(path, length) with no pre-populated modification time — calling lastModified() would trigger an extra HEAD request per split, defeating the purpose of the cache. The access-based TTL provides bounded staleness instead.

Cached byte[] entries must be treated as immutable by all callers.

  • Field Details

    • DEFAULT_MAX_BYTES

      public static final long DEFAULT_MAX_BYTES
      Default cache budget across the JVM (8 MiB).
      See Also:
    • DEFAULT_MAX_ENTRY_BYTES

      public static final long DEFAULT_MAX_ENTRY_BYTES
      Default max single entry (2 MiB). Prevents caching unusually large footers.
      See Also:
  • Method Details

    • getInstance

      public static FooterByteCache getInstance()
      Returns the JVM-wide singleton instance.
    • maxEntryBytes

      public long maxEntryBytes()
      Maximum byte size for a single cache entry.
    • getOrLoad

      public byte[] getOrLoad(FooterByteCache.Key key, CacheLoader<FooterByteCache.Key,byte[]> loader) throws ExecutionException
      Returns cached footer bytes for the given key, or loads them via the provided loader. The loader is invoked at most once per key, even under concurrent access — additional callers for the same key wait until the first load completes.

      If the loaded entry is empty or exceeds maxEntryBytes(), it is returned to the caller but immediately evicted so it does not consume cache budget or prevent future loads. This means oversized entries briefly occupy the cache between computeIfAbsent and invalidate. The alternative — manual get/put — was rejected because it loses thundering-herd protection for all first loads: N concurrent callers seeing get(key) == null would each invoke the loader independently.

      Throws:
      ExecutionException - if the loader throws an exception or returns null
    • get

      public byte[] get(FooterByteCache.Key key)
      Returns cached footer bytes or null if not present. Does not start a load, but may block briefly if another thread is currently loading the same key.
    • put

      public void put(FooterByteCache.Key key, byte[] bytes)
      Stores footer bytes directly, e.g. from an opportunistic read that reached the end of a file. Entries larger than maxEntryBytes() are silently skipped.
    • invalidateAll

      public void invalidateAll()
      Removes all entries. Intended for test isolation.