Add multi-layer caching: 24h Redis TTL, stale-while-revalidate, frontend LRU cache
- Increase Redis cache TTL from 30 minutes to 24 hours - Add stale-while-revalidate: serve stale cache (>4h) immediately while repopulating in background with SETNX lock to prevent concurrent rebuilds - Add in-memory frontend LRU cache (5 entries) so repeat filter visits are instant without network requests - Invalidate frontend cache on listing refresh and task completion - Add unit tests for get_cache_age, is_cache_stale, acquire_repopulation_lock
This commit is contained in:
parent
04bda8c127
commit
1ae00b7cbf
5 changed files with 270 additions and 1 deletions
|
|
@ -15,7 +15,9 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
CACHE_PREFIX = "listings:geojson:"
|
||||
STAGING_PREFIX = "listings:geojson:staging:"
|
||||
CACHE_TTL_SECONDS = 30 * 60 # 30 minutes
|
||||
CACHE_TTL_SECONDS = 24 * 60 * 60 # 24 hours
|
||||
STALE_AFTER_SECONDS = 4 * 60 * 60 # 4 hours — serve stale, revalidate in background
|
||||
REPOPULATING_PREFIX = "listings:geojson:repopulating:"
|
||||
STAGING_TTL_SECONDS = 5 * 60 # 5 minutes safety net for orphaned staging keys
|
||||
CACHE_DB = 2
|
||||
|
||||
|
|
@ -153,3 +155,45 @@ def invalidate_cache() -> None:
|
|||
logger.info(f"Invalidated {deleted} listing cache entries")
|
||||
except redis.RedisError as e:
|
||||
logger.warning(f"Redis cache invalidation error: {e}")
|
||||
|
||||
|
||||
def get_cache_age(query_params: QueryParameters) -> int | None:
|
||||
"""Return the age in seconds of a cache entry, or None if not cached."""
|
||||
try:
|
||||
client = _get_redis_client()
|
||||
key = make_cache_key(query_params)
|
||||
ttl = client.ttl(key)
|
||||
if ttl < 0:
|
||||
# -2 = key doesn't exist, -1 = no expiry
|
||||
return None
|
||||
return CACHE_TTL_SECONDS - ttl
|
||||
except redis.RedisError as e:
|
||||
logger.warning(f"Redis cache age check error: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def is_cache_stale(query_params: QueryParameters) -> bool:
|
||||
"""Return True if the cache entry exists but is older than STALE_AFTER_SECONDS."""
|
||||
age = get_cache_age(query_params)
|
||||
if age is None:
|
||||
return False
|
||||
return age > STALE_AFTER_SECONDS
|
||||
|
||||
|
||||
def acquire_repopulation_lock(query_params: QueryParameters) -> bool:
|
||||
"""Try to acquire a lock to prevent concurrent repopulations.
|
||||
|
||||
Returns True if the lock was acquired, False if another repopulation
|
||||
is already in progress for the same query.
|
||||
"""
|
||||
try:
|
||||
client = _get_redis_client()
|
||||
key = make_cache_key(query_params)
|
||||
hash_suffix = key.removeprefix(CACHE_PREFIX)
|
||||
lock_key = f"{REPOPULATING_PREFIX}{hash_suffix}"
|
||||
# SETNX with 60-second TTL
|
||||
acquired: bool = bool(client.set(lock_key, "1", nx=True, ex=60))
|
||||
return acquired
|
||||
except redis.RedisError as e:
|
||||
logger.warning(f"Redis repopulation lock error: {e}")
|
||||
return False
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue