Skip to content

ainvalidate_cache() with no args silently no-ops on parameterized functions #59

@27Bslash6

Description

@27Bslash6

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions