Skip to content

Scope clear() to the cache namespace - #1083

Open
rodrigobnogueira wants to merge 4 commits into
aio-libs:masterfrom
rodrigobnogueira:clear-respects-namespace
Open

rodrigobnogueira wants to merge 4 commits into
aio-libs:masterfrom
rodrigobnogueira:clear-respects-namespace

Conversation

@rodrigobnogueira

Copy link
Copy Markdown
Member

What do these changes do?

BaseCache.clear() forwarded its namespace argument to the backend untouched. Called with no argument on a cache that was built with a namespace, it passed None, and every backend read that as "clear everything":

  • ValkeyCache runs FLUSHDB
  • MemcachedCache runs flush_all
  • SimpleMemoryCache replaces its whole dict

So two namespaced caches pointed at one shared server destroy each other's data:

a = ValkeyCache(config, namespace="tenant_a")
b = ValkeyCache(config, namespace="tenant_b")
await a.set("k", "value-a")
await b.set("k", "value-b")

await a.clear()            # tenant_a clearing its own cache
await b.get("k")           # -> None, tenant_b is gone too

clear() is the only namespaced operation that skipped the self.namespace fallback 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:

  • Valkey namespaced clear was incomplete. _clear() issued a single SCAN from cursor 0 and 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 to 0.
  • Memcached cannot clear by namespace. It already raised ValueError when 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.
  • On Memcached, clear() raises ValueError instead 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 touched base.clear(), so the shared-backend case stayed broken. #523 proposed the same fallback and was closed at the time on that assumption.

Checklist

  • I think the code is well written
  • Unit tests for the changes exist
  • Documentation reflects the changes
  • If you provide code modification, please add yourself to CONTRIBUTORS.txt — N/A, no such file in this repository
  • Add a new news fragment into the CHANGES/ folder — N/A, this repository edits CHANGES.rst directly
Test run

Full suite against real Valkey and Memcached containers, matching the service setup in CI:

$ pytest tests/acceptance tests/ut
820 passed in 57.78s

$ flake8 tests/ aiocache/
(clean)

$ mypy aiocache
Found 3 errors in 1 file (checked 12 source files)   # identical before and after this change

Behaviour against a real Valkey server, before and after:

before:  tenant_a='value-a' tenant_b='value-b'
after a.clear():  tenant_a=None tenant_b='value-b'      # was tenant_b=None

clear() over a 5000-key namespace: 0 keys remaining     # was 4989 remaining

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

codecov Bot commented Aug 9, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.88%. Comparing base (ae5948b) to head (2d0ea74).

Additional details and impacted files

Impacted file tree graph

@@            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              
Files with missing lines Coverage Δ
aiocache/backends/memory.py 100.00% <100.00%> (ø)
aiocache/backends/valkey.py 100.00% <100.00%> (ø)
aiocache/base.py 99.29% <100.00%> (+0.01%) ⬆️
tests/acceptance/test_base.py 100.00% <100.00%> (ø)
tests/ut/backends/test_valkey.py 100.00% <100.00%> (ø)
tests/ut/test_base.py 100.00% <100.00%> (ø)

Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update ae5948b...2d0ea74. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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
rodrigobnogueira marked this pull request as ready for review August 9, 2026 05:12

@Dreamsorcerer Dreamsorcerer left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.
@greptile-apps

greptile-apps Bot commented Aug 15, 2026

Copy link
Copy Markdown

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Reviews (2): Last reviewed commit: "Check the namespace really leads the key..." | Re-trigger Greptile

@rodrigobnogueira

rodrigobnogueira commented Aug 15, 2026

Copy link
Copy Markdown
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.
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