Repository navigation
Conversation
Problem: with client-side caching enabled and configured with both a max
size and a ttl (`cache_enabled=True`, `cache_max_size=N`, `cache_ttl=T`),
the cache stopped working after its second eviction: cache writes raised a
bare KeyError('GET <key>') out of execute_command.
Cause: `_LocalCache._evict()` dropped the evicted command with
`self.cache.popitem(last=False)` / `self.cache.pop(...)`, so the command
stayed in `self.commands_ttl_list` (and its keys stayed in
`key_commands_map`). `_evict()` inspects `commands_ttl_list[0]` for expiry
before applying the eviction policy, and `_is_expired()` indexes
`self.cache[command]`; as soon as the head of that list was an already
evicted command, the lookup raised KeyError. `_LocalCache.set()` also
appended a duplicate id to `commands_ttl_list` when a command that was
already cached was cached again, which broke the same invariant on its own.
Fix: evict through `delete_command()` so a command is removed from the
cache, from `commands_ttl_list` and from `key_commands_map` together, and
refresh the `commands_ttl_list` position instead of appending a duplicate
when the same command is cached again.
Tests: new server-free `TestLocalCacheEviction` cases in tests/test_cache.py
and tests/test_asyncio/test_cache.py cover eviction with a ttl under the
LRU/LFU/RANDOM policies, the commands_ttl_list/cache invariant, and
re-caching a command. The Valkey-backed tests in tests/test_cache.py could
not be run here: this host has no valkey server.
Signed-off-by: simpleqt <2015900818@qq.com>
simpleqt
requested review from
ahmedsobeh,
bogdanp05,
mkmkme and
smeso
as code owners
October 20, 2026 13:59
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.
Problem
Client-side caching stops working — and raises an unexpected
KeyError— as soon as a_LocalCachethat is configured with both a max size and a ttl evicts for the secondtime. Both settings are reachable from the public client API:
cache_max_size/cache_ttlare forwarded to every connection(
valkey/connection.py,AbstractConnection.__init__→_LocalCache(cache_max_size, cache_ttl, cache_policy)), and_add_to_local_cache()callscache.set()on everycacheable response, so the
KeyErrorescapes fromexecute_command().Reproduction (no server required)
G:\pr\work\vk\scratch_repro.pydrives_LocalCachedirectly (the class used by theclient and passed as
client_cache=/built bycache_enabled=True):G:\pr\work\vk\.venv\Scripts\python.exe scratch_repro.pyObserved on
main(seevk-repro-unpatched.txt, all three eviction policies):Expected: the eviction succeeds (
result: no error), and the bookkeeping lists only thecommands still held by the cache. Observed with the fix (
vk-repro-patched.txt):A second scenario shows the same class of breakage after re-caching one command
(
GET k1twice), and a third shows the silent version of the bug with the defaultttl=0: two live commands but four ids incommands_ttl_list.Root cause
valkey/_cache.py,_LocalCache._evict()removed the victim withself.cache.popitem(last=False)/self.cache.pop(...)instead ofdelete_command().The evicted command therefore stayed in
self.commands_ttl_list(and its keys stayed inself.key_commands_map)._evict()then checks the head of that list for expiry before applying the evictionpolicy:
and
_is_expired()indexes the cache:Once the head of
commands_ttl_listwas an already evicted command, that indexing raisedKeyError. (ttl == 0short-circuits in_is_expired(), which is why the crash needs attl; it is also why the
ttl=0variant only leaks stale ids.)_LocalCache.set()had a second, independent way to break the same invariant: caching acommand that was already cached appended a second id to
commands_ttl_list, anddelete_command()removes only one occurrence, leaving a stale head again.Fix
_evict()now removes the victim throughdelete_command(), so the command leavesself.cache,self.commands_ttl_listandself.key_commands_maptogether. The victimselection is unchanged: LRU still takes the front of the (recently-used ordered) dict,
LFU the minimum
access_count, RANDOM a random key.set()refreshes thecommands_ttl_listposition instead of appending a duplicate whenthe command is already cached, so the list holds exactly one id per cached command.
Diff:
valkey/_cache.py, +8 −3.Tests
New server-free class
TestLocalCacheEvictionintests/test_cache.py(and its asyncmirror in
tests/test_asyncio/test_cache.py, required by CONTRIBUTING.md/AGENTS.md since_LocalCacheis shared code):test_eviction_with_ttl_does_not_raise— LRU/LFU/RANDOM, 10 cache writes on amax_size=2, ttl=60cache.test_evicted_commands_are_dropped_from_ttl_list— thecommands_ttl_list == cache(and no-duplicates) invariant.test_recaching_a_command_does_not_duplicate_ttl_entry.RED on base (source only reverted, tests kept):
with the underlying error
GREEN with the fix:
Checks run
python -m pytest tests/test_cache.py::TestLocalCacheEviction tests/test_asyncio/test_cache.py::TestLocalCacheEviction -qpython -m pytest tests/test_helpers.py tests/test_connection_pool.py::TestConnectionPoolURLParsing tests/test_connection_pool.py::TestBlockingConnectionPoolURLParsing tests/test_connection_pool.py::TestConnectionPoolUnixSocketURLParsing tests/test_connection_pool.py::TestSSLConnectionURLParsing tests/test_retry.py::TestRetry tests/test_retry.py::TestConnectionConstructorWithRetry tests/test_connection.py::TestConnection tests/test_connection.py::test_pack_command tests/test_connection.py::test_format_error_message tests/test_connection.py::test_parsing_unix_socket_relative_path tests/test_connection.py::TestConnectionTimeoutValidation tests/test_encoding.py::TestMemoryviewsAreNotPacked -qTestConnection::test_retry_connect_on_timeout_errorneeds a real server)ruff check tests valkeyruff format --check docs tests valkeyvulture valkey whitelist.py --min-confidence 80flynt --fail-on-change --dry-run tests valkeymypy(with thetype-checkdependency group installed)Not run — no Valkey server on this host.
tests/test_cache.pyandtests/test_asyncio/test_cache.pyin full were executed but every Valkey-backed casefailed/errored with
ConnectionError: Error 10061 connecting to localhost:6379(sentinel/cluster cases: their master/sentinel ports) — the only tests that passed were the
15 server-free ones: the 7 new tests in each module, plus
tests/test_cache.py::TestLocalCache::test_cache_not_available_with_resp2(sync module: 8 passed, 21 failed, 28 errors; both modules together: 15 passed, 41 failed,
64 errors). The integration suites
(
make standalone-tests,make cluster-tests), everything under thevalkeymod,ssl,replica,onlyclusterand sentinel markers, and the client-side-caching tests thatexercise a real server were not run.
Pull Request check-list (
.github/PULL_REQUEST_TEMPLATE.md)subset and all pinned linters/type checks (see table); the Valkey-backed suites could
not be exercised on this host.
this host, no fork CI run); a CI run is required.
_evict,setbookkeeping) are covered by the new server-free tests.
AI assistance
The defect was found, reproduced, fixed and tested by the assistant (DeepSeek Harness
agent), which wrote this draft; every command output quoted above was produced by that
agent on this host. A human reviewed the change before submission.