"""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