-
Notifications
You must be signed in to change notification settings - Fork 0
Description
Bug
ainvalidate_cache() called with no arguments on a decorated function that takes parameters silently does nothing — it generates a cache key for the zero-argument call (which was never cached) and invalidates that non-existent key. All cached entries for real argument combinations survive in both L1 and L2.
Reproduction
import asyncio
from cachekit import cache
call_count = 0
@cache(ttl=60, namespace="repro")
async def expensive(query: str):
global call_count
call_count += 1
return f"result_{call_count}"
async def main():
await expensive("hello")
await expensive("world")
print(f"calls: {call_count}") # 2
# Invalidate — developer expects all entries cleared
await expensive.ainvalidate_cache()
await expensive("hello")
await expensive("world")
print(f"calls: {call_count}") # Still 2 — cache entries survived!
asyncio.run(main())Output:
calls: 2
calls: 2
Expected: calls: 4 — both entries should have been invalidated.
Root cause
In decorators/wrapper.py, ainvalidate_cache generates a cache key using the provided *args, **kwargs:
async def ainvalidate_cache(*args, **kwargs):
cache_key = operation_handler.get_cache_key(func, args, kwargs, namespace, integrity_checking)
if _l1_cache and cache_key:
_l1_cache.invalidate(cache_key) # per-key invalidation
if _backend and not _l1_only_mode:
await invalidator.invalidate_cache_async(func, args, kwargs, namespace)When called with no args, get_cache_key(func, (), {}, ...) produces a key that doesn't match any real cached entry. L1 and L2 invalidation both target a non-existent key.
Impact
Any code that calls fn.ainvalidate_cache() expecting to clear all cached results for that function is silently broken. This is the natural API expectation — "invalidate the cache for this function" — but it only works for zero-argument functions.
The sync invalidate_cache() has the same issue, and cache_clear() raises TypeError for async functions, so there is no way to clear all entries for a parameterized async function through the decorator API.
Suggested fix
When ainvalidate_cache() / invalidate_cache() is called with no arguments on a function that has parameters, it should clear all entries for that function's namespace rather than generating a key for the non-existent zero-arg call.
L1 already supports this via _l1_cache.invalidate_by_namespace(namespace). The L2 invalidator would need a similar namespace-level delete (e.g., Redis SCAN + DEL by key prefix).
Versions
- cachekit 0.3.1
- Python 3.12