Skip to content

fix: evict the local caches by key prefix too, instead of guessing - #2222

Merged
netomi merged 2 commits into
mainfrom
fix/evict-local-by-prefix
Sep 16, 2026
Merged

netomi merged 2 commits into
mainfrom
fix/evict-local-by-prefix

Conversation

@netomi

@netomi netomi commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

Third part of #1767, stacked on #2220 (→ #2219) — see the analysis on the issue.

What is left to fix

#2219 made Redis drop an extension's keys in one scan. The local caches — which is what a registry runs with when ovsx.redis.enabled is off, and what extension.json is kept in through JCacheCacheManager — still enumerate every key they can imagine:

(3 aliases + versions) × 13 target platforms

2639 evictions for an extension with 200 versions, nearly all of keys that do not exist.

The change

Ask the cache what it holds instead of guessing what it might. Both local cache managers are Caffeine — one wrapped directly (files, settings), one through the JCache API (extension.json and the latest-version caches, when Redis is off) — so both are reached through the native cache and their key set is scanned. Redis keeps scanning its keyspace, as before.

Through the native cache in the JCache case rather than its own iterator: CacheProxy$EntryIterator walks entries rather than keys, and per entry it copies the value and refreshes the access expiry, neither of which an eviction has any use for, over values as large as an extension's JSON.

This is about completeness, not speed. Guessing costs (3 aliases + versions) × 13 target platforms lookups and a scan costs the size of the cache, so below roughly 76 versions the guessing does fewer operations; the performance win for Redis was #2219. What a scan buys is that it cannot miss a key nobody thought to name — the fallback enumerates versions from extension.getVersions(), so a version that was just deleted is no longer among them and its cached JSON survives until the TTL. Deletion is when staleness is most visible.

It also means the lazy versions association is no longer read for any eviction: every cache in use can clear by prefix, so the snapshot #2220 had to take is never needed.

Guessing stays for a cache that is neither, which in practice means a test double.

Testing

CacheServiceEvictionTest grows cases built on real caches rather than mocks — a Caffeine one and a JCache over Caffeine, the same combination CacheConfig builds. Entries go in for foo.bar, foo.bar2, foo.bar-baz and other.bar, one extension is evicted, and only its own entries are gone.

Each of them evicts an extension with no versions loaded, so the cached keys are ones the guessing fallback could not name: with the local scan disabled, all three fail. A further case pins that the JCache API's own iterator is not used.

Note the third of those: foo.bar-baz surviving is a consequence of the key terminator from #2219. Before that commit the local scan would have swept it, exactly as the Redis glob did.

Full server suite green; pre-commit hooks pass.

Not pursued

Step 4 of the issue — generation-stamped keys, which would remove eviction rather than speed it up — is deliberately left alone: it needs the stamp readable before every cache lookup, which is complexity of a different kind for a problem this stack has already brought down to one scan.

🤖 Generated with Claude Code

@netomi
netomi added this pull request to stack #2221 September 15, 2026 21:49
@netomi
netomi force-pushed the fix/evict-local-by-prefix branch 2 times, most recently from 670b9c0 to 712a6a7 Compare September 16, 2026 06:05
@netomi
netomi force-pushed the fix/evict-local-by-prefix branch from 712a6a7 to 18a38b8 Compare September 16, 2026 06:15
Base automatically changed from fix/evict-after-commit to main September 16, 2026 06:16
Third part of #1767. Redis has been dropping an extension's keys in one scan
since the first PR in this stack; the local caches still enumerate every key
they can imagine - (3 aliases + versions) x 13 target platforms, 2600 of them
for an extension with 200 versions, nearly all of which do not exist.

They can be asked instead. A Caffeine cache drops the matching keys from its
key set; a JCache one - which is what extension.json is kept in when Redis is
off - iterates its entries and removes them. Either is bounded by what the
cache holds, a few thousand entries at most, rather than by versions times
platforms.

It is also complete, where guessing is not: the fallback enumerates versions
from the extension, so a version that was just deleted is no longer among them
and its entry survives until the TTL.

Guessing stays for a cache that is neither, which in practice means a test
double.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@netomi
netomi force-pushed the fix/evict-local-by-prefix branch from 18a38b8 to c91ae46 Compare September 16, 2026 06:19
@netomi
netomi requested a balanced review from Copilot September 16, 2026 06:21

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The JCache test currently passes through the fallback path and does not prove the new branch works.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Replaces costly guessed cache-key eviction with prefix-based scanning for local caches.

Changes:

  • Adds Caffeine and JCache prefix eviction.
  • Shares extension-key prefixes across cache generators.
  • Adds real-cache eviction tests.
File summaries
File Description
CacheService.java Implements prefix eviction for Redis, Caffeine, and JCache.
LatestExtensionVersionCacheKeyGenerator.java Exposes the shared extension prefix.
CacheServiceEvictionTest.java Tests local cache eviction behavior.
Review details
  • Files reviewed: 3/3 changed files
  • Comments generated: 1
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Review follow-up, twice over.

Iterating a javax.cache.Cache walks entries rather than keys, and its iterator
copies each value and refreshes each entry's access expiry on the way past -
work an eviction has no use for, over values as large as an extension's JSON,
and over a cache holding up to its maximum size. The Caffeine cache underneath
is reached with unwrap instead and its key set scanned, which is what the
directly-wrapped caches already did. Both local cache managers end up on the
same path.

The JCache test did not distinguish the scan from the guessing fallback: its
extension carried the version whose key it asserted on, so the fallback named
that key too and every assertion held with the scan removed. All three local
cache tests now use an extension with no versions loaded, so the cached keys
are ones only a scan can reach - and all three fail when the scan is taken out.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@netomi
netomi merged commit 39f9ad0 into main Sep 16, 2026
5 checks passed
@netomi
netomi deleted the fix/evict-local-by-prefix branch September 16, 2026 07:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants