Class ParsedFooterCache<T>
- Type Parameters:
T- the parsed metadata type held by this cache (e.g.ParquetMetadata).
ParquetMetadata, ORC
OrcTail). Sits at the same architectural layer as FooterByteCache but stores
the result of the format-specific footer parse rather than its raw bytes, so the (typically
Thrift/protobuf) deserialization runs at most once per (path, fileLength) key across:
- concurrent splits of the same file taken by N producer threads;
- back-to-back queries against the same file within the access TTL.
Why parsed metadata and not just raw bytes
FooterByteCache eliminates redundant tail-byte reads from object storage, but the parse
still runs every time a reader is opened: N producers fanning out over a wide file each pay the
full deserialization cost, even though the cached bytes are identical. Caching the parsed
result collapses that into a single deserialization. FooterByteCache stays in place
because it also serves opportunistic partial-tail reads (page indexes, dictionary tails) that
are not full-footer parses, and because format readers fall back to byte reads on a cold cache.
Sharing keys with FooterByteCache
The cache is keyed by FooterByteCache.Key ((path, fileLength)) so that the same
key construction logic used to hit the byte cache also hits this cache; both caches stay aligned
without an extra key type. Per-format singletons (one for Parquet, one for ORC, etc.) keep the
value type concrete and the cache's ownership unambiguous.
Lifecycle
- Created as a singleton per format reader; no SPI plumbing is required to share entries
across producers since every code path that needs a parsed footer already constructs a
FooterByteCache.Keyvia the storage-object adapter. - Access-based TTL — sourced from
FooterByteCache.EXPIRE_AFTER_ACCESS_SECONDSso the two caches age out together; covers a single query's fan-out (where concurrent splits keep the entry alive) while ensuring that file modifications between queries trigger a fresh parse. - Count-based LRU eviction — parsed metadata structures (e.g. Parquet
ParquetMetadataor ORCOrcTail) do not expose a cheap byte size, so the cache caps the number of entries rather than total bytes. Worst-case heap budget: a single parsed footer for an extreme file (e.g. 100 columns × 200 row groups → ~20k column-chunk entries) can occupy ~10–20 MiB of heap. WithDEFAULT_MAX_ENTRIES= 32<T> the absolute worst case is in the hundreds of MiB if every cached entry is for such an extreme file and all live concurrently inside the TTL window. Typical workloads (≤10 columns, ≤50 row groups, ≈200 KiB per entry) sit at a few MiB total. Adding a real weigher would require an estimator pass over the parsed structure on everyput— not implemented here, but tracked for follow-up if heap pressure shows up in production. Note that the byte and parsed caches evict independently — TTL alignment keeps them timing-consistent but does not synchronize eviction events.
Cached values must be treated as immutable by all callers — callers that need to derive a filtered view (e.g. only the row groups for a specific byte range) should build a new value from the cached one rather than mutating the cached structure.
-
Field Summary
FieldsModifier and TypeFieldDescriptionstatic final intDefault maximum number of cached parsed footers across the JVM. -
Constructor Summary
ConstructorsConstructorDescriptionCreates a cache with the default maximum entry count.ParsedFooterCache(int maxEntries) Creates a cache with the given maximum entry count. -
Method Summary
Modifier and TypeMethodDescriptionget(FooterByteCache.Key key) Returns the cached parsed footer ornullif not present.getOrLoad(FooterByteCache.Key key, CacheLoader<FooterByteCache.Key, T> loader) Returns the cached parsed footer for the given key, or loads it vialoader.voidRemoves all entries.static ThrowableUnwraps anExecutionExceptionthrown bygetOrLoad(org.elasticsearch.xpack.esql.datasources.cache.FooterByteCache.Key, org.elasticsearch.common.cache.CacheLoader<org.elasticsearch.xpack.esql.datasources.cache.FooterByteCache.Key, T>)and reshapes the structural failure modes back to their original types so callers see the same shapes they would have seen had they parsed the footer synchronously:Error(includingOutOfMemoryError, given highest priority so JVM-level failures are never silently swallowed),IOException,CircuitBreakingException, andElasticsearchException.
-
Field Details
-
DEFAULT_MAX_ENTRIES
public static final int DEFAULT_MAX_ENTRIESDefault maximum number of cached parsed footers across the JVM. Sized small enough that the worst-case heap (see class Javadoc) stays bounded even when extremely wide files dominate the working set, while still being large enough to absorb the typical fan-out of one query over many distinct files.- See Also:
-
-
Constructor Details
-
ParsedFooterCache
public ParsedFooterCache()Creates a cache with the default maximum entry count. -
ParsedFooterCache
public ParsedFooterCache(int maxEntries) Creates a cache with the given maximum entry count. Exposed for tests; production callers should rely onDEFAULT_MAX_ENTRIES.- Throws:
IllegalArgumentException- ifmaxEntries <= 0
-
-
Method Details
-
invalidateAll
public void invalidateAll()Removes all entries. Intended for test isolation. -
rethrowStructural
Unwraps anExecutionExceptionthrown bygetOrLoad(org.elasticsearch.xpack.esql.datasources.cache.FooterByteCache.Key, org.elasticsearch.common.cache.CacheLoader<org.elasticsearch.xpack.esql.datasources.cache.FooterByteCache.Key, T>)and reshapes the structural failure modes back to their original types so callers see the same shapes they would have seen had they parsed the footer synchronously:Error(includingOutOfMemoryError, given highest priority so JVM-level failures are never silently swallowed),IOException,CircuitBreakingException, andElasticsearchException.Any other cause — typically a plain
RuntimeExceptionfrom a format library indicating a malformed file — is returned for format-specific wrapping. The two readers differ here: Parquet wraps every such cause innewInvalidParquetFileException, ORC rethrows it directly. Keeping that policy out of this helper avoids dragging format-specific factories into the shared cache.Always invoke as
throw new ...(rethrowStructural(e))orthrow rethrowStructural(e)so the compiler treats the call as terminal — see the callers in the format readers.- Returns:
- the original cause (or the
ExecutionExceptionitself if the cause is null), for the caller to either rethrow or wrap - Throws:
IOException- if the cause is anIOExceptionCircuitBreakingException- if the cause is aCircuitBreakingExceptionElasticsearchException- if the cause is anElasticsearchException