a75318b811
Stage-0 caching from docs/scalability.md. The Go API now caches hot product-detail and search-result reads in Redis with a fail-open, epoch-versioned scheme; Python ingestion bumps the epoch after a write run to invalidate the cache globally in O(1). - api/internal/cache: fail-open Cache (GetJSON/SetJSON) namespaced by an epoch counter (og:cache:epoch); disabled when Redis is unconfigured. - store: ProductByID/ProductByGTIN (24h TTL) and SearchProducts (1h TTL) read-through the cache via WithCache. - ingestion: bump_cache_epoch() called after update_off/seed_off/ import_bypos/dedup commits when rows changed; best-effort, never fails a run. Adds redis dependency. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
53 lines
1.8 KiB
Python
53 lines
1.8 KiB
Python
"""Read-cache invalidation signal for the public API.
|
|
|
|
The Go API caches hot product details and search results in Redis, namespacing
|
|
every key by a global generation counter (``og:cache:epoch``). Bumping that
|
|
counter logically invalidates the entire cache in one O(1) operation while the
|
|
old keys age out via their TTL.
|
|
|
|
Ingestion is the only writer to the database, so after a run that changed data
|
|
it calls :func:`bump_cache_epoch` to make those changes visible immediately
|
|
instead of waiting for per-key TTLs to expire.
|
|
|
|
Like the API's cache, this is strictly best effort and fails open: if Redis is
|
|
unconfigured or unreachable the ingestion run still succeeds, and stale entries
|
|
simply expire on their own.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import os
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
EPOCH_KEY = "og:cache:epoch"
|
|
|
|
|
|
def default_redis_url() -> str | None:
|
|
"""Return the configured Redis URL, or ``None`` when caching is disabled."""
|
|
return os.environ.get("OPENGOODS_REDIS_URL") or None
|
|
|
|
|
|
def bump_cache_epoch(url: str | None = None) -> bool:
|
|
"""Increment the API cache generation counter.
|
|
|
|
Returns ``True`` if the counter was bumped, ``False`` if caching is disabled
|
|
or Redis was unreachable. Never raises: invalidation failures must not fail
|
|
an ingestion run.
|
|
"""
|
|
url = url or default_redis_url()
|
|
if not url:
|
|
return False
|
|
try:
|
|
import redis # imported lazily so the dependency is optional at runtime
|
|
|
|
client = redis.Redis.from_url(url, socket_timeout=2, socket_connect_timeout=2)
|
|
new_epoch = client.incr(EPOCH_KEY)
|
|
client.close()
|
|
logger.info("bumped API cache epoch to %s", new_epoch)
|
|
return True
|
|
except Exception as exc: # noqa: BLE001 - invalidation is best effort
|
|
logger.warning("cache epoch bump skipped: %s", exc)
|
|
return False
|