cache.py 3.51 KB
"""Shared translation cache utilities."""

from __future__ import annotations

import hashlib
import logging
from typing import Mapping, Optional

import redis

from config.env_config import REDIS_CONFIG

logger = logging.getLogger(__name__)


class TranslationCache:
    """Redis-backed cache shared by all translation capabilities."""

    def __init__(self, config: Mapping[str, object]) -> None:
        self.ttl_seconds = int(config["ttl_seconds"])
        self.sliding_expiration = bool(config["sliding_expiration"])
        self.redis_client = self._init_redis_client()

    @property
    def available(self) -> bool:
        return self.redis_client is not None

    def build_key(self, *, model: str, target_lang: str, source_text: str) -> str:
        normalized_model = str(model or "").strip().lower()
        normalized_target_lang = str(target_lang or "").strip().lower()
        text = str(source_text or "")
        text_prefix = text[:4]
        digest = hashlib.sha256(text.encode("utf-8")).hexdigest()
        return f"trans:{normalized_model}:{normalized_target_lang}:{text_prefix}{digest}"

    def get(self, *, model: str, target_lang: str, source_text: str) -> Optional[str]:
        if self.redis_client is None:
            return None
        key = self.build_key(model=model, target_lang=target_lang, source_text=source_text)
        try:
            value = self.redis_client.get(key)
            logger.info(
                "Translation cache %s | model=%s target_lang=%s text_len=%s key=%s",
                "hit" if value is not None else "miss",
                model,
                target_lang,
                len(str(source_text or "")),
                key,
            )
            if value and self.sliding_expiration:
                self.redis_client.expire(key, self.ttl_seconds)
            return value
        except Exception as exc:
            logger.warning("Redis get translation cache failed: %s", exc)
            return None

    def set(self, *, model: str, target_lang: str, source_text: str, translated_text: str) -> None:
        if self.redis_client is None:
            return
        key = self.build_key(model=model, target_lang=target_lang, source_text=source_text)
        try:
            self.redis_client.setex(key, self.ttl_seconds, translated_text)
            logger.info(
                "Translation cache write | model=%s target_lang=%s text_len=%s result_len=%s ttl_seconds=%s key=%s",
                model,
                target_lang,
                len(str(source_text or "")),
                len(str(translated_text or "")),
                self.ttl_seconds,
                key,
            )
        except Exception as exc:
            logger.warning("Redis set translation cache failed: %s", exc)

    @staticmethod
    def _init_redis_client() -> Optional[redis.Redis]:
        try:
            client = redis.Redis(
                host=REDIS_CONFIG.get("host", "localhost"),
                port=REDIS_CONFIG.get("port", 6479),
                password=REDIS_CONFIG.get("password"),
                decode_responses=True,
                socket_timeout=REDIS_CONFIG.get("socket_timeout", 1),
                socket_connect_timeout=REDIS_CONFIG.get("socket_connect_timeout", 1),
                retry_on_timeout=REDIS_CONFIG.get("retry_on_timeout", False),
                health_check_interval=10,
            )
            client.ping()
            return client
        except Exception as exc:
            logger.warning("Failed to initialize translation redis cache: %s", exc)
            return None