140 lines
3.5 KiB
Python
140 lines
3.5 KiB
Python
import json
|
|
import time
|
|
from importlib import import_module
|
|
from typing import Any
|
|
|
|
from config import REDIS_URL
|
|
|
|
_REDIS_CLIENT = None
|
|
_REDIS_UNAVAILABLE_UNTIL = 0.0
|
|
_REDIS_BACKOFF_SEC = 5.0
|
|
|
|
|
|
def _now() -> float:
|
|
return time.monotonic()
|
|
|
|
|
|
async def _get_redis() -> Any | None:
|
|
global _REDIS_CLIENT, _REDIS_UNAVAILABLE_UNTIL
|
|
|
|
if _REDIS_CLIENT is not None:
|
|
return _REDIS_CLIENT
|
|
if _REDIS_UNAVAILABLE_UNTIL > _now():
|
|
return None
|
|
|
|
try:
|
|
redis_from_url = import_module("redis.asyncio").from_url
|
|
client = redis_from_url(
|
|
REDIS_URL,
|
|
encoding="utf-8",
|
|
decode_responses=True,
|
|
max_connections=64,
|
|
)
|
|
await client.ping()
|
|
_REDIS_CLIENT = client
|
|
return _REDIS_CLIENT
|
|
except Exception:
|
|
_REDIS_UNAVAILABLE_UNTIL = _now() + _REDIS_BACKOFF_SEC
|
|
_REDIS_CLIENT = None
|
|
return None
|
|
|
|
|
|
def cache_key(prefix: str, *parts: Any) -> str:
|
|
tail = ":".join(str(p) for p in parts)
|
|
return f"{prefix}:{tail}" if tail else prefix
|
|
|
|
|
|
async def cache_get(key: str) -> Any | None:
|
|
client = await _get_redis()
|
|
if client is None:
|
|
return None
|
|
try:
|
|
raw = await client.get(key)
|
|
if raw is None:
|
|
return None
|
|
return json.loads(raw)
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
async def cache_mget(keys: list[str]) -> list[Any | None]:
|
|
"""Возвращает значения для ключей (None для отсутствующих). Один round-trip в Redis."""
|
|
if not keys:
|
|
return []
|
|
client = await _get_redis()
|
|
if client is None:
|
|
return [None] * len(keys)
|
|
try:
|
|
raw_list = await client.mget(keys)
|
|
result = []
|
|
for raw in raw_list:
|
|
if raw is None:
|
|
result.append(None)
|
|
else:
|
|
try:
|
|
result.append(json.loads(raw))
|
|
except Exception:
|
|
result.append(None)
|
|
return result
|
|
except Exception:
|
|
return [None] * len(keys)
|
|
|
|
|
|
async def cache_set(key: str, value: Any, ttl_sec: float) -> bool:
|
|
client = await _get_redis()
|
|
if client is None:
|
|
return False
|
|
try:
|
|
ttl = max(1, int(ttl_sec))
|
|
await client.set(key, json.dumps(value, ensure_ascii=False), ex=ttl)
|
|
return True
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
async def cache_delete(key: str) -> None:
|
|
client = await _get_redis()
|
|
if client is None:
|
|
return
|
|
try:
|
|
await client.delete(key)
|
|
except Exception:
|
|
return
|
|
|
|
|
|
async def cache_setnx(key: str, value: Any, ttl_sec: float) -> bool:
|
|
client = await _get_redis()
|
|
if client is None:
|
|
return False
|
|
try:
|
|
ttl = max(1, int(ttl_sec))
|
|
return bool(await client.set(key, json.dumps(value, ensure_ascii=False), ex=ttl, nx=True))
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
async def cache_incr(key: str, ttl_sec: float) -> int:
|
|
client = await _get_redis()
|
|
if client is None:
|
|
return 1
|
|
try:
|
|
value = await client.incr(key)
|
|
if value == 1:
|
|
await client.expire(key, max(1, int(ttl_sec)))
|
|
return int(value)
|
|
except Exception:
|
|
return 1
|
|
|
|
|
|
async def cache_delete_pattern(pattern: str) -> int:
|
|
client = await _get_redis()
|
|
if client is None:
|
|
return 0
|
|
deleted = 0
|
|
try:
|
|
async for key in client.scan_iter(match=pattern, count=200):
|
|
deleted += int(await client.delete(key))
|
|
except Exception:
|
|
return deleted
|
|
return deleted
|