User Guide ========== Installation ------------ .. code-block:: bash pip install async-cache Requires Python 3.8+. No external dependencies. When to Use Which Feature -------------------------- .. list-table:: :header-rows: 1 :widths: 25 35 40 * - Scenario - Feature - Why * - Simple function memoization - ``@AsyncLRU`` - Decorator, LRU eviction, no TTL needed * - Time-sensitive data (sessions, tokens) - ``@AsyncTTL`` - Automatic expiry after ``time_to_live`` seconds * - Fine-grained key/TTL control - ``AsyncCache`` (function API) - Per-key TTL, manual set/get/delete, batch loader * - High-concurrency hot keys - ``AsyncCache`` with ``loader`` - Built-in thundering herd protection * - GraphQL resolvers / N+1 - ``AsyncCache`` with ``batch_loader`` - DataLoader-style batching within a time window * - Survive process restarts - ``DiskBackend`` - Pickle-based persistence, zero new dependencies * - Distributed / multi-instance cache - ``RedisBackend`` - Two-tier L1/L2 caching with built-in pure-Python Redis client * - AI agent tool caching - ``AgentCache`` / ``AgentCacheSession`` - Resource tagging, session scoping, loop detection * - Write-through invalidation - ``AsyncLRUInvalidator`` / ``AsyncTTLInvalidator`` / ``AgentCacheInvalidator`` - Decorator-based, ties mutations to cached reads; invalidates before mutation Quick Start ----------- Function API ~~~~~~~~~~~~ .. code-block:: python import asyncio from cache import AsyncCache cache = AsyncCache(maxsize=1000, default_ttl=300) async def get_user(user_id: int) -> dict: return await cache.get( f"user:{user_id}", loader=lambda: fetch_from_database(user_id), ) # Warmup hot keys at startup await cache.warmup({"hot:key": lambda: preload_hot()}) # Observability print(cache.get_metrics()) # {'hits': 950, 'misses': 50, 'size': 200, 'hit_rate': 0.95} Decorator API ~~~~~~~~~~~~~ .. code-block:: python from cache import AsyncLRU, AsyncTTL @AsyncLRU(maxsize=128) async def get_product(product_id: int): return await db.query_product(product_id) @AsyncTTL(time_to_live=60, skip_args=1) # skip 'self' async def get_session(self, session_id: str): return await db.get_session(session_id) Core Features ------------- Thundering Herd Protection ~~~~~~~~~~~~~~~~~~~~~~~~~~ When a cached item expires under heavy load, multiple concurrent requests would normally trigger duplicate backend calls. async-cache ensures only **one** loader executes while all others await the same result. .. code-block:: python cache = AsyncCache() async def loader(): return await expensive_db_query() # runs ONCE # 100 concurrent coroutines, 1 database call results = await asyncio.gather(*[ cache.get("hot_key", loader=loader) for _ in range(100) ]) DataLoader-Style Batching ~~~~~~~~~~~~~~~~~~~~~~~~~ Group concurrent gets into a single batch call to reduce database round-trips. Configurable window (default 5ms) and batch size (default 100). .. code-block:: python async def batch_loader(keys): return await db.batch_query(keys) # one query for all keys # Auto-batched within 5ms window user1, user2 = await asyncio.gather( cache.get(1, batch_loader=batch_loader), cache.get(2, batch_loader=batch_loader), ) Cache Warmup ~~~~~~~~~~~~ Preload critical data at startup to avoid cold-start latency spikes. .. code-block:: python await cache.warmup({ "config:app": load_app_config, "feature_flags": load_feature_flags, "popular:products": load_popular_products, }) Metrics & Observability ~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: python metrics = cache.get_metrics() # {'hits': 950, 'misses': 50, 'size': 200, 'hit_rate': 0.95} # Decorator metrics metrics = get_product.get_metrics() TTL & Manual Invalidation ~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: python cache = AsyncCache(default_ttl=300) # global 5-min default cache.set("session:123", data, ttl=3600) # per-key 1 hour cache.set("permanent", data, ttl=None) # no expiration cache.delete("session:123") # single key cache.clear() # all keys Force Refresh ~~~~~~~~~~~~~ Bypass cache for a single call using ``use_cache=False``: .. code-block:: python @AsyncTTL(time_to_live=60) async def get_status(): return await check_service_status() status = await get_status(use_cache=False) # forces fresh load Skip Arguments in Cache Key ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ For instance methods, skip ``self`` from the cache key: .. code-block:: python class UserService: @AsyncLRU(maxsize=100, skip_args=1) async def get_user(self, user_id: int): return await self.db.get_user(user_id) Configuration Reference ----------------------- AsyncCache Parameters ~~~~~~~~~~~~~~~~~~~~~ .. list-table:: :header-rows: 1 :widths: 20 15 65 * - Parameter - Default - Description * - ``maxsize`` - 128 - Maximum items in cache (``None`` = unlimited) * - ``default_ttl`` - None - Default TTL in seconds (``None`` = no expiration) * - ``batch_window_ms`` - 5 - Batch grouping window in milliseconds * - ``max_batch_size`` - 100 - Max keys per batch call * - ``backup`` - None - Optional ``CacheBackend`` for disk persistence (e.g., ``DiskBackend``) * - ``remote_cache`` - None - Optional ``RemoteCacheBackend`` for distributed L2 caching (e.g., ``RedisBackend``) AsyncLRU / AsyncTTL Parameters ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. list-table:: :header-rows: 1 :widths: 20 15 65 * - Parameter - Default - Description * - ``maxsize`` - 128 / 1024 - Maximum cache size * - ``time_to_live`` (TTL only) - 60 - TTL in seconds * - ``skip_args`` - 0 - Number of leading args to skip in cache key * - ``backup`` - None - Optional ``CacheBackend`` for disk persistence * - ``remote_cache`` - None - Optional ``RemoteCacheBackend`` for distributed L2 caching