mirror of
https://github.com/wahyd4/hey-search.git
synced 2026-08-10 05:37:13 +10:00
- Add redis_url to SQLite settings table (persisted across restarts) - On startup, prefer DB-stored URL over REDIS_URL env var - Add reconnect_redis() to support live URL changes without restart - Settings API now accepts/returns redis_url field - Cache tab shows editable URL input with save button and connection status - Immediate feedback: shows Connected/Disconnected after save Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
122 lines
3.6 KiB
Python
122 lines
3.6 KiB
Python
"""Optional Redis cache for search results.
|
|
|
|
When REDIS_URL is not set or Redis is unreachable, all operations
|
|
gracefully degrade to no-ops (cache miss / skip).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import hashlib
|
|
import logging
|
|
import os
|
|
|
|
import redis.asyncio as aioredis
|
|
|
|
from app.settings import get_cache_ttl_seconds
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_redis: aioredis.Redis | None = None
|
|
_available: bool = False
|
|
|
|
|
|
async def init_redis() -> None:
|
|
"""Try to connect to Redis using DB setting first, then env var fallback."""
|
|
from app.settings import get_setting
|
|
url = get_setting("redis_url") or os.environ.get("REDIS_URL", "")
|
|
await reconnect_redis(url)
|
|
|
|
|
|
async def reconnect_redis(url: str) -> None:
|
|
"""Connect (or reconnect) to Redis at the given URL."""
|
|
global _redis, _available
|
|
# Close existing connection first
|
|
if _redis:
|
|
try:
|
|
await _redis.aclose()
|
|
except Exception:
|
|
pass
|
|
_redis = None
|
|
_available = False
|
|
if not url:
|
|
logger.info("Redis URL not set — caching disabled")
|
|
return
|
|
try:
|
|
_redis = aioredis.from_url(url, decode_responses=True, socket_connect_timeout=3)
|
|
await _redis.ping()
|
|
_available = True
|
|
logger.info("Redis connected at %s — caching enabled", url)
|
|
except Exception as e:
|
|
logger.warning("Redis unavailable (%s) — caching disabled", e)
|
|
_redis = None
|
|
_available = False
|
|
|
|
|
|
async def close_redis() -> None:
|
|
global _redis, _available
|
|
if _redis:
|
|
await _redis.aclose()
|
|
_redis = None
|
|
_available = False
|
|
|
|
|
|
def is_cache_available() -> bool:
|
|
return _available and _redis is not None
|
|
|
|
|
|
def _cache_key(query: str, category: str, page: int, image_size: str, engines: str) -> str:
|
|
"""Build a deterministic cache key."""
|
|
raw = f"hs:{category}:{page}:{image_size}:{engines}:{query}"
|
|
h = hashlib.sha256(raw.encode()).hexdigest()[:16]
|
|
return f"hs:search:{h}"
|
|
|
|
|
|
async def get_cached(query: str, category: str, page: int, image_size: str, engines: str) -> dict | None:
|
|
"""Return cached search response dict, or None on miss."""
|
|
if not is_cache_available():
|
|
return None
|
|
ttl = get_cache_ttl_seconds()
|
|
if ttl <= 0:
|
|
return None
|
|
key = _cache_key(query, category, page, image_size, engines)
|
|
try:
|
|
data = await _redis.get(key) # type: ignore[union-attr]
|
|
if data:
|
|
logger.debug("Cache HIT for key %s", key)
|
|
return json.loads(data)
|
|
except Exception as e:
|
|
logger.warning("Cache get error: %s", e)
|
|
return None
|
|
|
|
|
|
async def set_cached(query: str, category: str, page: int, image_size: str, engines: str, response_dict: dict) -> None:
|
|
"""Store a search response in cache."""
|
|
if not is_cache_available():
|
|
return
|
|
ttl = get_cache_ttl_seconds()
|
|
if ttl <= 0:
|
|
return
|
|
key = _cache_key(query, category, page, image_size, engines)
|
|
try:
|
|
await _redis.set(key, json.dumps(response_dict, default=str), ex=ttl) # type: ignore[union-attr]
|
|
logger.debug("Cache SET for key %s (ttl=%ds)", key, ttl)
|
|
except Exception as e:
|
|
logger.warning("Cache set error: %s", e)
|
|
|
|
|
|
async def flush_cache() -> int:
|
|
"""Delete all hey-search cache keys. Returns count deleted."""
|
|
if not is_cache_available():
|
|
return 0
|
|
try:
|
|
keys = []
|
|
async for key in _redis.scan_iter("hs:search:*"): # type: ignore[union-attr]
|
|
keys.append(key)
|
|
if keys:
|
|
await _redis.delete(*keys) # type: ignore[union-attr]
|
|
return len(keys)
|
|
except Exception as e:
|
|
logger.warning("Cache flush error: %s", e)
|
|
return 0
|