All checks were successful
Deploy / changes (push) Successful in 9s
Deploy Trade-In / changes (push) Successful in 13s
Deploy / build-frontend (push) Has been skipped
Deploy / deploy-caddy (push) Has been skipped
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy / build-backend (push) Successful in 2m23s
Deploy Trade-In / test (push) Successful in 3m56s
Deploy / build-worker (push) Successful in 4m16s
Deploy Trade-In / build-backend (push) Successful in 1m19s
Deploy / deploy (push) Successful in 1m49s
Deploy / deploy-status (push) Successful in 1s
Deploy / perimeter-smoke (push) Successful in 12s
Deploy Trade-In / deploy (push) Successful in 2m25s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 11s
62 lines
1.9 KiB
Python
62 lines
1.9 KiB
Python
"""Redis hot cache for search results (master plan sec 7.2)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
import logging
|
|
from functools import lru_cache
|
|
from typing import Any
|
|
|
|
import redis.asyncio as redis_async
|
|
|
|
from app.core.config import settings
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class SearchCache:
|
|
"""Async Redis cache wrapper. Stable sha256 hash keys, TTL per kind."""
|
|
|
|
TTL_SEARCH: int = 300
|
|
TTL_HOUSE_DETAIL: int = 3600
|
|
TTL_VALUATION: int = 86400
|
|
|
|
def __init__(self, url: str) -> None:
|
|
self._client: redis_async.Redis = redis_async.from_url(
|
|
url, decode_responses=True, socket_timeout=2.0, socket_connect_timeout=2.0
|
|
)
|
|
|
|
def search_key(self, params: dict[str, Any]) -> str:
|
|
payload = json.dumps(params, sort_keys=True, default=str, ensure_ascii=False)
|
|
digest = hashlib.sha256(payload.encode("utf-8")).hexdigest()[:32]
|
|
return f"tradein:search:{digest}"
|
|
|
|
async def get(self, key: str) -> dict[str, Any] | None:
|
|
try:
|
|
raw = await self._client.get(key)
|
|
except Exception as e:
|
|
logger.warning("redis GET failed key=%s: %s", key[:24], e)
|
|
return None
|
|
if raw is None:
|
|
return None
|
|
try:
|
|
return json.loads(raw)
|
|
except json.JSONDecodeError:
|
|
logger.warning("redis cache corrupted key=%s", key[:24])
|
|
return None
|
|
|
|
async def set(self, key: str, value: dict[str, Any], ttl: int) -> None:
|
|
try:
|
|
await self._client.set(key, json.dumps(value, default=str, ensure_ascii=False), ex=ttl)
|
|
except Exception as e:
|
|
logger.warning("redis SET failed key=%s: %s", key[:24], e)
|
|
|
|
async def close(self) -> None:
|
|
await self._client.aclose()
|
|
|
|
|
|
@lru_cache(maxsize=1)
|
|
def get_search_cache() -> SearchCache:
|
|
"""Singleton — один Redis pool на процесс."""
|
|
return SearchCache(settings.redis_url)
|