Cache Invalidation ================== async-cache provides invalidation at every API level: core ``AsyncCache``, decorators (``AsyncLRU`` / ``AsyncTTL``), and agent cache (``AgentCacheInvalidator``). Core AsyncCache --------------- .. code-block:: python from cache import AsyncCache cache = AsyncCache(maxsize=1000, default_ttl=300) # Delete a single key cache.delete("user:123") # Clear all entries and reset metrics cache.clear() Decorator API: Per-Key Invalidation ------------------------------------ Every ``@AsyncLRU`` and ``@AsyncTTL`` wrapper exposes ``invalidate_cache()``: .. code-block:: python from cache import AsyncLRU @AsyncLRU(maxsize=128) async def get_user(user_id: int): return await db.get_user(user_id) # Invalidate a specific cached result get_user.invalidate_cache(42) # removes cache entry for get_user(42) # Clear the entire cache get_user.clear_cache() Invalidation Decorators ----------------------- For write/mutation functions that should automatically invalidate related cached reads, use ``AsyncLRUInvalidator`` or ``AsyncTTLInvalidator``. **Important**: Invalidation happens *before* the mutation executes. This ensures no reader ever sees stale data after a mutation starts, even if the mutation raises an exception. AsyncLRUInvalidator ~~~~~~~~~~~~~~~~~~~ .. code-block:: python from cache import AsyncLRU, AsyncLRUInvalidator @AsyncLRU(maxsize=128) async def get_user(user_id: int): return await db.get_user(user_id) @AsyncLRUInvalidator(get_user) async def update_user(user_id: int, data: dict): await db.update_user(user_id, data) await get_user(1) # miss -> loads from DB await get_user(1) # hit await update_user(1, {"name": "Alice"}) # invalidates get_user(1), then runs mutation await get_user(1) # miss -> reloads fresh data AsyncTTLInvalidator ~~~~~~~~~~~~~~~~~~~ .. code-block:: python from cache import AsyncTTL, AsyncTTLInvalidator @AsyncTTL(time_to_live=60) async def get_session(session_id: str): return await db.get_session(session_id) @AsyncTTLInvalidator(get_session) async def end_session(session_id: str): await db.delete_session(session_id) await get_session("s1") # miss -> loads await end_session("s1") # invalidates get_session("s1"), then runs mutation await get_session("s1") # miss -> reloads Invalidator Options ~~~~~~~~~~~~~~~~~~~ .. list-table:: :header-rows: 1 :widths: 20 15 65 * - Parameter - Default - Description * - ``cached_fn`` - (required) - The ``@AsyncLRU`` / ``@AsyncTTL`` wrapped function to invalidate * - ``clear_all`` - False - Clear the entire cache instead of a specific key * - ``key_fn`` - None - Custom ``(args, kwargs) -> (inv_args, inv_kwargs)`` for complex arg mapping .. note:: ``skip_args`` has been **removed** from invalidators. Passing a non-zero value raises ``TypeError``. The old ``skip_args`` on invalidators silently produced wrong cache keys when the cached function also used ``skip_args``. Use ``key_fn`` instead — see the example below. Mutation Failure Safety ~~~~~~~~~~~~~~~~~~~~~~~ The cache key is evicted **before** the mutation runs. If the mutation raises, the key is already gone — the next read triggers a fresh load: .. code-block:: python @AsyncLRUInvalidator(get_user) async def update_user(user_id: int, data: dict): raise RuntimeError("DB error") # mutation fails await get_user(1) # loads and caches try: await update_user(1, {"x": "y"}) # invalidates first, then raises except RuntimeError: pass await get_user(1) # cache miss -> reloads fresh data Clear All Mode ~~~~~~~~~~~~~~ When a mutation affects all cached entries: .. code-block:: python @AsyncLRUInvalidator(get_user, clear_all=True) async def rebuild_user_index(): await db.rebuild_index() await rebuild_user_index() # clears ALL get_user cache entries Custom Key Mapping (``key_fn``) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ When the mutation function's args don't directly match the cached function's args, use ``key_fn`` to translate them: .. code-block:: python @AsyncLRU(maxsize=128) async def get_item(item_id: int): return await db.get_item(item_id) # Mutation has extra args; extract just item_id for invalidation @AsyncLRUInvalidator(get_item, key_fn=lambda args, kw: (args[:1], {})) async def update_item(item_id: int, name: str, price: float): await db.update_item(item_id, name=name, price=price) **Migrating from ``skip_args``:** if you previously used ``skip_args=1`` on the invalidator to skip ``self``, replace it with ``key_fn``: .. code-block:: python # Old (raises TypeError now): # @AsyncLRUInvalidator(get_data, skip_args=1) # New: @AsyncLRUInvalidator(get_data, key_fn=lambda args, kw: (args[1:], {})) async def update_data(self, key): ... AgentCache Invalidation ----------------------- For AI agent workflows, use resource-based invalidation. Like the decorator invalidators, ``AgentCacheInvalidator`` evicts the cache **before** the mutation runs — failed mutations never leave stale data. .. code-block:: python from agent_cache import AgentCache, AgentCacheInvalidator @AgentCache(resource="cart", scope="global", ttl=60) async def get_cart(user_id): return await db.get_cart(user_id) @AgentCacheInvalidator(resource="cart", scope="global") async def add_to_cart(user_id, item): await db.add_item(user_id, item) await get_cart("u1") # cached await add_to_cart("u1", "laptop") # invalidates ALL "cart" entries, then runs mutation await get_cart("u1") # re-fetched ``AgentCacheInvalidator`` accepts the same invalidation parameters as the decorator invalidators: .. list-table:: :header-rows: 1 :widths: 20 15 65 * - Parameter - Default - Description * - ``resource`` - (required) - Resource tag to invalidate * - ``scope`` - ``"global"`` - ``"global"`` or ``"session"`` * - ``clear_all`` - True - Clear all entries for the resource (default for agent cache) * - ``key_fn`` - None - Custom ``(args, kwargs) -> cache_key`` for selective invalidation