| """ |
| Stale-While-Revalidate Caching Pattern |
| |
| Prevents the "Thundering Herd" problem where cache expiration causes |
| 500 simultaneous database hits. |
| |
| Pattern: |
| 1. Serve stale data immediately (fast response) |
| 2. Trigger background refresh (for next user) |
| 3. No user ever waits for database |
| |
| Performance: |
| - All requests: ~5ms (always from cache) |
| - Background refresh: Async, doesn't block users |
| - Database protected from traffic spikes |
| """ |
|
|
| import asyncio |
| import time |
| from typing import Optional, Callable, Any |
| import json |
|
|
|
|
| class StaleWhileRevalidate: |
| """ |
| Cache with stale-while-revalidate pattern |
| |
| When cache expires: |
| - Returns old (stale) data immediately |
| - Triggers background refresh |
| - Next user gets fresh data |
| """ |
| |
| def __init__(self, redis_client=None): |
| """ |
| Initialize cache manager |
| |
| Args: |
| redis_client: Optional Redis client |
| """ |
| self.redis = redis_client |
| self.refresh_locks = {} |
| |
| async def get_or_fetch( |
| self, |
| cache_key: str, |
| fetch_func: Callable, |
| ttl: int = 600, |
| stale_ttl: int = 3600 |
| ) -> Any: |
| """ |
| Get data with stale-while-revalidate pattern |
| |
| Args: |
| cache_key: Cache key |
| fetch_func: Async function to fetch fresh data |
| ttl: Fresh data TTL (default: 10 minutes) |
| stale_ttl: Stale data TTL (default: 1 hour) |
| |
| Returns: |
| Cached or fresh data |
| """ |
| if not self.redis: |
| |
| return await fetch_func() |
| |
| try: |
| |
| cached_raw = await self.redis.get(cache_key) |
| |
| if cached_raw: |
| cached = json.loads(cached_raw) |
| data = cached.get('data') |
| timestamp = cached.get('timestamp', 0) |
| age = time.time() - timestamp |
| |
| |
| if age < ttl: |
| return data |
| |
| |
| if age < stale_ttl: |
| |
| |
| |
| |
| asyncio.create_task( |
| self._background_refresh(cache_key, fetch_func, ttl, stale_ttl) |
| ) |
| |
| return data |
| |
| |
| |
| |
| |
| return await self._fetch_and_cache(cache_key, fetch_func, ttl, stale_ttl) |
| |
| except Exception as e: |
| print(f"Cache error for {cache_key}: {e}") |
| |
| return await fetch_func() |
| |
| async def _background_refresh( |
| self, |
| cache_key: str, |
| fetch_func: Callable, |
| ttl: int, |
| stale_ttl: int |
| ): |
| """ |
| Refresh cache in background (doesn't block user request) |
| """ |
| |
| if cache_key in self.refresh_locks: |
| return |
| |
| try: |
| self.refresh_locks[cache_key] = True |
| |
| |
| fresh_data = await fetch_func() |
| |
| |
| cache_value = { |
| 'data': fresh_data, |
| 'timestamp': time.time() |
| } |
| |
| await self.redis.setex( |
| cache_key, |
| stale_ttl, |
| json.dumps(cache_value) |
| ) |
| |
| except Exception as e: |
| print(f"Background refresh failed for {cache_key}: {e}") |
| finally: |
| self.refresh_locks.pop(cache_key, None) |
| |
| async def _fetch_and_cache( |
| self, |
| cache_key: str, |
| fetch_func: Callable, |
| ttl: int, |
| stale_ttl: int |
| ) -> Any: |
| """ |
| Fetch fresh data and store in cache |
| """ |
| fresh_data = await fetch_func() |
| |
| |
| cache_value = { |
| 'data': fresh_data, |
| 'timestamp': time.time() |
| } |
| |
| try: |
| await self.redis.setex( |
| cache_key, |
| stale_ttl, |
| json.dumps(cache_value) |
| ) |
| except Exception as e: |
| print(f"Cache write failed for {cache_key}: {e}") |
| |
| return fresh_data |
|
|
|
|
| |
| """ |
| # In your API endpoint: |
| cache = StaleWhileRevalidate(redis_client) |
| |
| async def fetch_articles_from_db(): |
| return await db.get_articles('ai', limit=20) |
| |
| # This always returns quickly: |
| # - If fresh: from cache (~5ms) |
| # - If stale: from cache (~5ms) + background refresh |
| # - If expired: fetch from DB (~50ms) |
| articles = await cache.get_or_fetch( |
| cache_key='news:ai:cursor:xyz', |
| fetch_func=fetch_articles_from_db, |
| ttl=600, # Fresh for 10 minutes |
| stale_ttl=3600 # Serve stale for up to 1 hour |
| ) |
| """ |
|
|
|
|
| |
| """ |
| T=0: Cache miss β Fetch from DB (50ms) β Store in cache |
| T=300s: User request β Cache hit (5ms) β Fresh data |
| T=600s: User request β Cache hit (5ms) β Stale data (still valid!) |
| β Background refresh triggered (user already got response) |
| T=605s: Background refresh completes β Cache updated |
| T=610s: Next user β Cache hit (5ms) β Fresh data again! |
| |
| Result: All users get 5ms responses, DB never overwhelmed! |
| """ |
|
|