Repository navigation
fix(client): drop evicted entries from the client-side cache key index - #3538
Open
maxymlyskov wants to merge 2 commits into
Open
maxymlyskov wants to merge 2 commits into
maxymlyskov wants to merge 2 commits into
Conversation
BasicClientSideCache keeps a key -> cacheKeys index next to its entries. delete() and deleteOldest() removed the entry but left its cacheKey in that index, so entries evicted by maxEntries, expired by ttl or dropped after a failed load stayed indexed until the server invalidated the key. A read-mostly workload over many keys grew the index past maxEntries. invalidate(key) also left a multi-key entry (MGET a b) indexed under its other keys. Remember each entry's keys and remove its cacheKey from them whenever the entry goes, dropping a key once no entry uses it.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
Reviewed by Cursor Bugbot for commit 489e4b0. Configure here.
… key index Cursor Bugbot pointed out that set() replaced an existing cacheKey's keys in #cacheKeyToKeysMap without removing the old ones from #keyToCacheKeySetMap. An entry set with keys a and b and then with key a stayed listed under b, so invalidating b dropped the live entry. handleCache() builds the cacheKey from every argument, so it always comes with the same keys; this shows only when set() is called directly.
This branch has not been deployed
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.

Description
BasicClientSideCachekeeps a key to cacheKeys index (#keyToCacheKeySetMap) next to its entries, andinvalidate(key)uses it to find what to drop.delete()anddeleteOldest()remove the entry but leave its cacheKey in that index. So an entry evicted bymaxEntries, expired byttlor dropped after a failed load stays indexed until the server invalidates that exact key or the cache is cleared. A read-mostly workload over many distinct keys grows the index pastmaxEntries, and server invalidations don't keep it small: the defaulttracking-table-max-keysis 1000000.With
redis:8(8.10.2), a RESP3 client withBasicClientSideCache({ maxEntries: 100 })callinggeton 50000, then 200000 more distinct keys:invalidate(key)has a smaller version of the same leak: it clears that key only, so a multi-key entry (MGET a b) invalidated throughastays listed underb.The cache now remembers each entry's keys (
#cacheKeyToKeysMap, filled inset(), cleared inclear()), so an eviction doesn't have to scan every key.#deleteFromKeyIndex(cacheKey)removes the cacheKey from each of its keys' sets and drops a key once no entry uses it;delete(),deleteOldest(),invalidate(key)and aset()that overwrites an entry call it.Four tests in a new
describe('key index')usecache.set(...)andkeySetEntries():maxEntries: 2with 10 keys leaves only the 2 cached keys indexed, deleting entries keeps another entry on the same key, invalidatingaof ana bentry leavesbwith only its own entry, and ana bentry overwritten with keysastays cached whenbis invalidated. All four fail on master.npm run build && npm testpasses apart fromRedisSentinel lifecycle events destroy() from an end listener tears down a reentrant reopen, which also fails on a clean master (2 of 10 runs alone). Lint is clean.#3495 also changes
cache.ts(invalidate(),clear()and a copy of the index), so the two will conflict textually. This fix doesn't touch the lines it adds.Checklist
npm testpass with this change (including linting)?Note
Medium Risk
Changes core client-side cache bookkeeping used on every cached read and server invalidation; behavior is narrowed to fix leaks but touches hot paths in
BasicClientSideCache.Overview
Fixes a memory leak in
BasicClientSideCachewhere the Redis-key → cache-key index (#keyToCacheKeySetMap) kept growing after entries were removed. Eviction (maxEntries),delete(),deleteOldest(), TTL expiry, failed loads, and partial invalidation used to drop the entry but leave stale index entries—so read-heavy workloads over many keys could balloon heap use even whenmaxEntriesstayed small.The cache now tracks each entry’s Redis keys in
#cacheKeyToKeysMapand centralizes cleanup in#deleteFromKeyIndex, invoked whenever an entry leaves the cache (including onset()overwrite and when invalidating multi-key entries likeMGET).clear()also resets the new map.Adds a
key indextest block with four regressions: eviction undermaxEntries, delete with shared keys, multi-key invalidation, and overwrite changing an entry’s key set.Reviewed by Cursor Bugbot for commit 3267def. Bugbot is set up for automated code reviews on this repo. Configure here.