cache.py
3.66 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
"""Shared translation cache utilities."""
from __future__ import annotations
import hashlib
import logging
from typing import Mapping, Optional
try:
import redis
except ImportError: # pragma: no cover - runtime fallback for minimal envs
redis = None # type: ignore[assignment]
from config.loader import get_app_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 | text_len=%s key=%s",
"hit" if value is not None else "miss",
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 | text_len=%s result_len=%s ttl_seconds=%s key=%s",
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]:
if redis is None:
logger.warning("redis package is not installed; translation cache disabled")
return None
redis_config = get_app_config().infrastructure.redis
try:
client = redis.Redis(
host=redis_config.host,
port=redis_config.port,
password=redis_config.password,
decode_responses=True,
socket_timeout=redis_config.socket_timeout,
socket_connect_timeout=redis_config.socket_connect_timeout,
retry_on_timeout=redis_config.retry_on_timeout,
health_check_interval=10,
)
client.ping()
return client
except Exception as exc:
logger.warning("Failed to initialize translation redis cache: %s", exc)
return None