A cache stampede occurs when multiple concurrent requests for the same missing key trigger redundant computations. The @cachebox.cached decorator prevents this by default using a per-key lock (lock=True).
- Sync functions: Uses
threading.Lock. - Async functions: Uses
asyncio.Lock automatically. - Custom locks: You can pass any object implementing
contextlib.AbstractContextManager (sync) or AbstractAsyncContextManager (async).
Warning: Passing a synchronous lock to an async function (or vice versa) raises a TypeError at decoration time.
When to disable the lock (lock=False or lock=None):
- Recursive functions: The default non-reentrant lock will cause a deadlock if a cached function calls itself.
- Cheap computations: If recomputing is nearly free, the lock overhead might not be worth it.
- Single-threaded environments: No concurrency means no stampedes.
- Already-serialized callers: If your architecture already guarantees single-caller access per key.
Note: Disabling the lock does not make the cache unsafe; internal Rust mutexes still protect reads and writes. It only allows multiple threads to compute the same missing value simultaneously.
import threading
import cachebox
# Using a re-entrant lock (RLock) to allow recursion
@cachebox.cached(cachebox.LRUCache(maxsize=256), lock=threading.RLock)
def factorial(n: int) -> int:
return 1 if n <= 1 else n * factorial(n - 1)
# Disabling the lock for cheap computations
@cachebox.cached(cachebox.LRUCache(maxsize=256), lock=False)
def cheap_func(x: int):
return x * 2