Scope clear() to the cache namespace - #1083
Open
rodrigobnogueira wants to merge 4 commits into
Open
rodrigobnogueira wants to merge 4 commits into
rodrigobnogueira wants to merge 4 commits into
Conversation
clear() forwarded its namespace argument straight to the backend, so a cache built with a namespace passed None and every backend took its "clear everything" path: FLUSHDB on Valkey, flush_all on Memcached, and a fresh dict for in-memory. Two caches sharing one server could therefore destroy each other's keys, and the docstring already promised the opposite. Resolve the namespace the way every other operation does. Valkey's namespaced branch only ever deleted the first SCAN batch, which left almost everything behind once a namespace grew past a few keys, so iterate until the cursor returns to 0. Memcached cannot clear by namespace and already raised ValueError when given one explicitly; a namespaced instance now gets that same error instead of silently flushing the server. Pass namespace="" to flush.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #1083 +/- ##
==========================================
+ Coverage 98.85% 98.88% +0.03%
==========================================
Files 32 32
Lines 3579 3691 +112
Branches 125 128 +3
==========================================
+ Hits 3538 3650 +112
Misses 41 41
Continue to review full report in Codecov by Harness.
🚀 New features to boost your workflow:
|
Both namespaced backends assumed the default key layout: Valkey scanned "<namespace>:*" and memory compared against the bare namespace. A cache with a custom key_builder therefore cleared nothing at all on Valkey, which is worse than the flush it used to do. Derive the prefix from build_key() instead. With a builder that does not separate the namespace from the key, a namespace still matches longer namespaces starting with it, since those keys are indistinguishable. Say so rather than promise otherwise. Also pin that an empty namespace still clears the whole backend; that is the documented way to flush and nothing covered it.
rodrigobnogueira
marked this pull request as ready for review
August 9, 2026 05:12
Dreamsorcerer
approved these changes
Aug 9, 2026
Dreamsorcerer
left a comment
Member
There was a problem hiding this comment.
Think this looks alright.
Deriving the scan pattern from the key_builder's prefix left two ways for clear() to touch keys outside the namespace it was given. A prefix is a glob pattern to Valkey's SCAN, so a namespace containing glob syntax matched something else entirely: "ten[a]nt" became the character class "ten[a]nt:*", which matches "tenant:*". Clearing it deleted the neighbouring namespace's keys and left its own in place, the exact reverse of what was asked. Escape the metacharacters before appending the wildcard. A key_builder that ignores the namespace produces an empty prefix, making the pattern a bare "*" and clear() a full flush of the database, other namespaces included, reported as success. Memory has the same hole, since every key starts with "". Neither can be scoped, so raise rather than delete more than was asked for. A builder that places the namespace anywhere but the start is left alone: it cannot be told apart from a valid prefix, so it deletes nothing and says so in the docs instead. Cover the prefix derivation itself, which nothing pinned before: reverting it to the old hardcoded "<namespace>:" passed the whole suite, because both default key_builders happen to produce exactly that string.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains. Reviews (2): Last reviewed commit: "Check the namespace really leads the key..." | Re-trigger Greptile |
Member
Author
|
Fix: the derived prefix must actually lead a probe key, else ValueError. Covers appending and hashing. |
Deriving the prefix from build_key("", namespace) assumed the key_builder
puts the namespace first. Nothing enforced that, and the assumption fails
destructively rather than harmlessly.
A builder appending the namespace, lambda k, ns: f"{k}{ns}", makes keys
like "minens" but yields the prefix "ns". That prefix does not match the
namespace's own keys, so they survive a clear(), while unrelated keys that
merely start with the same characters, "ns-foreign", are deleted. Exactly
inverted, on both prefix-matching backends. Hashing the whole key fails the
same way, matching whatever happens to share the hash's leading digits.
So verify the property instead of assuming it: build a key nothing collides
with and confirm the derived prefix leads it. Builders that append, hash or
drop the namespace are refused, which also covers the empty prefix the
previous guard caught, and leaves ns:key, nskey and ns__key working.
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.
What do these changes do?
BaseCache.clear()forwarded itsnamespaceargument to the backend untouched. Called with no argument on a cache that was built with a namespace, it passedNone, and every backend read that as "clear everything":ValkeyCacherunsFLUSHDBMemcachedCacherunsflush_allSimpleMemoryCachereplaces its whole dictSo two namespaced caches pointed at one shared server destroy each other's data:
clear()is the only namespaced operation that skipped theself.namespacefallback every other operation applies through_str_build_key(), and its docstring already documented the intended behaviour: "Clears the cache in the cache namespace." This resolves the namespace the same way.Two changes come with it:
_clear()issued a singleSCANfrom cursor0and deleted only that batch. Against a real server, clearing a 5000-key namespace removed 11 keys and left 4989. Since this fix routes namespaced caches onto that path, it now iterates until the cursor returns to0.ValueErrorwhen given one explicitly, and a namespaced instance now gets the same error instead of silently flushing the server.clear(namespace="")still flushes.Caches with no namespace are unaffected: the default is
"", which stays falsy and takes the same flush path as before.Are there changes in behavior for the user?
Yes, for caches configured with a namespace:
clear()removes only that namespace's keys instead of the whole backend.clear()raisesValueErrorinstead of flushing everything.clear(namespace="")is the explicit flush.Both are noted under the 1.0.0 migration instructions in
CHANGES.rst.Related issue number
#479 reported this for
SimpleMemoryCache. It was closed as resolved by #562, but #562 only moved the in-memory backend's state onto the instance and never touchedbase.clear(), so the shared-backend case stayed broken. #523 proposed the same fallback and was closed at the time on that assumption.Checklist
CONTRIBUTORS.txt— N/A, no such file in this repositoryCHANGES/folder — N/A, this repository editsCHANGES.rstdirectlyTest run
Full suite against real Valkey and Memcached containers, matching the service setup in CI:
Behaviour against a real Valkey server, before and after: