"""Redis cache layer — graceful fallback when Redis is unavailable.""" from __future__ import annotations import json import logging import os from typing import Any import redis logger = logging.getLogger(__name__) _client: redis.Redis | None = None _disabled = False DEFAULT_TTL = 600 # 10 minutes def _get_client() -> redis.Redis | None: global _client, _disabled if _disabled: return None if _client is None: host = os.environ.get("REDIS_HOST", "192.168.0.147") port = int(os.environ.get("REDIS_PORT", "6379")) db = int(os.environ.get("REDIS_DB", "0")) try: _client = redis.Redis( host=host, port=port, db=db, socket_connect_timeout=2, socket_timeout=2, decode_responses=True, ) _client.ping() logger.info("Redis connected: %s:%s/%s", host, port, db) except Exception as e: logger.warning("Redis unavailable (%s), caching disabled", e) _client = None _disabled = True return None return _client def make_key(*parts: Any) -> str: """Build a cache key like 'tasteby:restaurants:cuisine=한식:limit=100'.""" return "tasteby:" + ":".join(str(p) for p in parts if p is not None and p != "") def get(key: str) -> Any | None: """Get cached value. Returns None on miss or error.""" try: client = _get_client() if not client: return None val = client.get(key) if val is not None: return json.loads(val) except Exception as e: logger.debug("Cache get error: %s", e) return None def set(key: str, value: Any, ttl: int = DEFAULT_TTL) -> None: """Cache a value as JSON with TTL.""" try: client = _get_client() if not client: return client.setex(key, ttl, json.dumps(value, ensure_ascii=False, default=str)) except Exception as e: logger.debug("Cache set error: %s", e) def flush() -> None: """Flush all tasteby cache keys.""" try: client = _get_client() if not client: return cursor = 0 while True: cursor, keys = client.scan(cursor, match="tasteby:*", count=200) if keys: client.delete(*keys) if cursor == 0: break logger.info("Cache flushed") except Exception as e: logger.debug("Cache flush error: %s", e) def invalidate_prefix(prefix: str) -> None: """Delete all keys matching a prefix.""" try: client = _get_client() if not client: return cursor = 0 while True: cursor, keys = client.scan(cursor, match=f"{prefix}*", count=200) if keys: client.delete(*keys) if cursor == 0: break except Exception as e: logger.debug("Cache invalidate error: %s", e)