[WIP] prototype for negative caching in StoreCache#4042
Open
espg wants to merge 2 commits into
Open
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #4042 +/- ##
==========================================
+ Coverage 93.55% 93.59% +0.04%
==========================================
Files 88 88
Lines 11896 11926 +30
==========================================
+ Hits 11129 11162 +33
+ Misses 767 764 -3
🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds opt-in negative caching to
zarr.experimental.cache_store.CacheStore: when enabled, a full-key read that finds the key absent in the source store is remembered, so subsequent reads of that absent key returnNoneimmediately without a source round-trip. The remembered miss is evicted when the key is later written. Default off; no behavior change unlesscache_missing=True. Follows from discussion on #4028Motivation
CacheStorecaches present values only. On a full-key miss it deletes any stale entry and stores nothing, so a key absent in the source is a permanent cache miss — every read re-pays a source round-trip. This is the dominant cost when reading sparse arrays through aCacheStore: most chunks are empty, and the positive cache structurally cannot help (there is no value to store, and "not in cache" is indistinguishable from "not cached yet"). Negative caching closes that gap.It is intentionally narrow: it benefits the stock
arr[:]path (which probes every chunk) read repeatedly through aCacheStore. Code using the #4028 discovery primitives never issues the empty-chunk reads in the first place and does not need this.API
cache_missing: bool = True— remember full-key misses (opt-out).cache_stats()gainsnegative_hits;cache_info()gainscache_missingandmissing_keys.No new bounding parameter is introduced: remembered misses are bounded by the existing
max_age_seconds, mirroring how the positive cache is bounded bymax_size.Design
CacheStorewraps a whole store and sees opaque keys (no chunk-grid knowledge), so negative knowledge is tracked per full key in a smalldict[str, float](key → insert time). Negative entries carry no bytes and are kept out of themax_sizebyte budget, so they never evict real cached data.max_age_seconds, so a key written to the source out-of-band becomes visible again after expiry. Like the positive cache (unbounded whenmax_size is None), the negative cache is bounded only bymax_age_seconds; with an infinite TTL a scan over a very large sparse key space accumulates one small entry per absent key, so set a finite TTL (orcache_missing=False) for such workloads. This is called out in the docstring.setand an overriddenset_if_not_existsdrop any remembered miss for the key.deletedoes not create one (a delete is a mutation, not a checked-absence read).exists()are unchanged.exists()deliberately does not consult the negative cache (the defaultset_if_not_existscallsexiststhenset; a stale "missing" there could overwrite present data).negative_hitsand counts as neither a hit nor a miss, so the positivehit_rateis unaffected.Correctness notes
max_age_seconds="infinity"a remembered miss never expires, so a key written by another process stays invisible through the cache until eviction-on-write. Paircache_missing=Truewith a finitemax_age_secondswhen the source may be written concurrently.getruns outside the state lock, so a concurrentsetcan land between the source returningNoneand the miss being recorded. This is the same window the positive cache already has; it is TTL-bounded and self-heals. Documented as a known limitation rather than over-engineered away.Testing
tests/test_experimental/test_cache_store.py— newTestCacheStoreNegativeCaching: enabled-by-default andcache_missing=Falsedisable, basic negative hit (asserts the source is hit exactly once viamonkeypatch), eviction onsetandset_if_not_exists, TTL expiry with an out-of-band source write, byte-range reads unaffected, stats/info surfacing, anddeletedoes not record. The existingtest_cache_infokey-set assertion is updated for the two new info keys. Full suite: 54 passed; ruff, mypy (strict), and numpydoc clean.TODO:
docs/user-guide/*.mdchanges/