mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2025-12-16 15:40:12 +01:00
113 lines
No EOL
3.7 KiB
Python
113 lines
No EOL
3.7 KiB
Python
"""Cache backend abstraction (Phase 2 extension) with Redis PoC.
|
|
|
|
The in-memory cache remains authoritative for adaptive eviction heuristics.
|
|
This backend layer provides optional read-through / write-through to Redis
|
|
for latency & CPU comparison. It is intentionally minimal:
|
|
|
|
Environment:
|
|
THEME_PREVIEW_REDIS_URL=redis://host:port/db -> enable PoC if redis-py importable
|
|
THEME_PREVIEW_REDIS_DISABLE=1 -> hard disable even if URL present
|
|
|
|
Behavior:
|
|
- On store: serialize payload + metadata into JSON and SETEX with TTL.
|
|
- On get (memory miss only): attempt Redis GET and rehydrate (respect TTL).
|
|
- Failures are swallowed; metrics track attempts/hits/errors.
|
|
|
|
No eviction coordination is attempted; Redis TTL handles expiry. The goal is
|
|
purely observational at this stage.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from typing import Optional, Dict, Any, Tuple
|
|
import json
|
|
import os
|
|
import time
|
|
|
|
try: # lazy optional dependency
|
|
import redis # type: ignore
|
|
except Exception: # pragma: no cover - absence path
|
|
redis = None # type: ignore
|
|
|
|
_URL = os.getenv("THEME_PREVIEW_REDIS_URL")
|
|
_DISABLED = (os.getenv("THEME_PREVIEW_REDIS_DISABLE") or "").lower() in {"1","true","yes","on"}
|
|
|
|
_CLIENT = None
|
|
_INIT_ERR: str | None = None
|
|
|
|
def _init() -> None:
|
|
global _CLIENT, _INIT_ERR
|
|
if _CLIENT is not None or _INIT_ERR is not None:
|
|
return
|
|
if _DISABLED or not _URL or not redis:
|
|
_INIT_ERR = "disabled_or_missing"
|
|
return
|
|
try:
|
|
_CLIENT = redis.Redis.from_url(_URL, socket_timeout=0.25) # type: ignore
|
|
# lightweight ping (non-fatal)
|
|
try:
|
|
_CLIENT.ping()
|
|
except Exception:
|
|
pass
|
|
except Exception as e: # pragma: no cover - network/dep issues
|
|
_INIT_ERR = f"init_error:{e}"[:120]
|
|
|
|
|
|
def backend_info() -> Dict[str, Any]:
|
|
return {
|
|
"enabled": bool(_CLIENT),
|
|
"init_error": _INIT_ERR,
|
|
"url_present": bool(_URL),
|
|
}
|
|
|
|
def _serialize(key: Tuple[str, int, str | None, str | None, str], payload: Dict[str, Any], build_cost_ms: float) -> str:
|
|
return json.dumps({
|
|
"k": list(key),
|
|
"p": payload,
|
|
"bc": build_cost_ms,
|
|
"ts": time.time(),
|
|
}, separators=(",", ":"))
|
|
|
|
def redis_store(key: Tuple[str, int, str | None, str | None, str], payload: Dict[str, Any], ttl_seconds: int, build_cost_ms: float) -> bool:
|
|
_init()
|
|
if not _CLIENT:
|
|
return False
|
|
try:
|
|
data = _serialize(key, payload, build_cost_ms)
|
|
# Compose a simple namespaced key; join tuple parts with '|'
|
|
skey = "tpv:" + "|".join([str(part) for part in key])
|
|
_CLIENT.setex(skey, ttl_seconds, data)
|
|
return True
|
|
except Exception: # pragma: no cover
|
|
return False
|
|
|
|
def redis_get(key: Tuple[str, int, str | None, str | None, str]) -> Optional[Dict[str, Any]]:
|
|
_init()
|
|
if not _CLIENT:
|
|
return None
|
|
try:
|
|
skey = "tpv:" + "|".join([str(part) for part in key])
|
|
raw: bytes | None = _CLIENT.get(skey) # type: ignore
|
|
if not raw:
|
|
return None
|
|
obj = json.loads(raw.decode("utf-8"))
|
|
# Expect shape from _serialize
|
|
payload = obj.get("p")
|
|
if not isinstance(payload, dict):
|
|
return None
|
|
return {
|
|
"payload": payload,
|
|
"_cached_at": float(obj.get("ts") or 0),
|
|
"cached_at": float(obj.get("ts") or 0),
|
|
"inserted_at": float(obj.get("ts") or 0),
|
|
"last_access": float(obj.get("ts") or 0),
|
|
"hit_count": 0,
|
|
"build_cost_ms": float(obj.get("bc") or 0.0),
|
|
}
|
|
except Exception: # pragma: no cover
|
|
return None
|
|
|
|
__all__ = [
|
|
"backend_info",
|
|
"redis_store",
|
|
"redis_get",
|
|
] |