Spaces:
Sleeping
Sleeping
Download src/inference/redis_cache.py from Moni38/smartvision-ai: direct link, hf CLI and curl.
- Browser
- Download file 4.75 kB
-
https://huggingface.co/spaces/Moni38/smartvision-ai/resolve/main/src/inference/redis_cache.py
- Command line
-
hf download hf://spaces/Moni38/smartvision-ai/src/inference/redis_cache.py
-
curl -L -o redis_cache.py https://huggingface.co/spaces/Moni38/smartvision-ai/resolve/main/src/inference/redis_cache.py
4.75 kB
| # filename: src/inference/redis_cache.py | |
| # purpose: Redis cache-aside pattern for classify and detect endpoints. | |
| # Rule 23: never raise on Redis errors β inference continues without cache. | |
| # Rule 31 (socket timeout): 1s connect timeout prevents blocking event loop | |
| # when Redis is not running locally. | |
| from __future__ import annotations | |
| import hashlib | |
| import json | |
| import logging | |
| from typing import Optional | |
| logger = logging.getLogger(__name__) | |
| class RedisCache: | |
| """SHA256 cache-aside with graceful degradation on Redis unavailability. | |
| Cache hit: returns dict (caller must pop 'cached' key before unpacking into response). | |
| Cache miss or error: returns None β inference runs normally. | |
| """ | |
| def __init__( | |
| self, | |
| host: str = "redis", | |
| port: int = 6379, | |
| password: str = "", | |
| timeout: float = 1.0, | |
| *, | |
| _client=None, | |
| ) -> None: | |
| self._available = False | |
| self._client = None | |
| try: | |
| if _client is not None: | |
| # Injected client (used in tests via fakeredis) | |
| self._client = _client | |
| self._client.ping() | |
| else: | |
| import redis as redis_lib | |
| self._client = redis_lib.Redis( | |
| host=host, | |
| port=port, | |
| password=password | |
| or None, # None = no auth (empty string means no auth) | |
| socket_connect_timeout=timeout, # fail fast β not 20-30s OS timeout | |
| socket_timeout=timeout, | |
| decode_responses=False, # raw bytes for json.loads | |
| ) | |
| self._client.ping() | |
| self._available = True | |
| logger.info(f"Redis connected: {host}:{port}") | |
| except Exception as e: | |
| logger.warning(f"Redis unavailable ({host}:{port}): {e}. Cache disabled.") | |
| def available(self) -> bool: | |
| return self._available | |
| def get(self, key: str) -> Optional[dict]: | |
| """Return cached dict or None on miss / connection error. Never raises.""" | |
| if not self._available: | |
| return None | |
| try: | |
| val = self._client.get(key) | |
| return json.loads(val) if val else None | |
| except Exception as e: | |
| logger.warning(f"Redis get error (key={key[:20]}...): {e}") | |
| return None | |
| def set(self, key: str, value: dict, ttl: int) -> None: | |
| """Store value with TTL. Silent on any Redis error. Never raises.""" | |
| if not self._available: | |
| return | |
| try: | |
| self._client.setex(key, ttl, json.dumps(value)) | |
| except Exception as e: | |
| logger.warning(f"Redis set error (key={key[:20]}...): {e}") | |
| def push_to_list(self, key: str, value: float, max_len: int) -> None: | |
| """LPUSH + LTRIM in pipeline (single round-trip). | |
| NOT atomic (no MULTI/EXEC). Safe for single-worker uvicorn deployments. | |
| With multiple workers the list may transiently exceed max_len between | |
| the two commands β the caller's in-process deque provides the safety net. | |
| """ | |
| if not self._available: | |
| return | |
| try: | |
| pipe = self._client.pipeline() | |
| pipe.lpush(key, str(value)) | |
| pipe.ltrim(key, 0, max_len - 1) | |
| pipe.execute() | |
| except Exception as e: | |
| logger.warning(f"Redis push_to_list error (key={key}): {e}") | |
| def get_list(self, key: str) -> list[float]: | |
| """LRANGE 0 -1 β decode bytes β list[float]. | |
| Returns [] on any error or missing key. | |
| decode_responses=False: items are bytes, decoded before float(). | |
| Items are in LPUSH (newest-first) order β caller must reverse() | |
| before extending a deque to preserve chronological insertion order. | |
| """ | |
| if not self._available: | |
| return [] | |
| try: | |
| raw: list[bytes] = self._client.lrange(key, 0, -1) | |
| return [float(v.decode("utf-8")) for v in raw] | |
| except Exception as e: | |
| logger.warning(f"Redis get_list error (key={key}): {e}") | |
| return [] | |
| def make_classify_key( | |
| image_bytes: bytes, model_name: str, model_hash: str = "" | |
| ) -> str: | |
| """32-char SHA256 prefix (128-bit) β negligible birthday collision probability.""" | |
| img = hashlib.sha256(image_bytes).hexdigest()[:32] | |
| return f"sv:classify:{img}:{model_name}:{model_hash}" | |
| def make_detect_key(image_bytes: bytes, conf: float) -> str: | |
| img = hashlib.sha256(image_bytes).hexdigest()[:32] | |
| return f"sv:detect:{img}:{conf:.2f}" | |