86 lines
2.7 KiB
Python
86 lines
2.7 KiB
Python
"""Response cache.
|
|
|
|
Not a performance nicety. Stack Exchange allows 300 calls/day/IP without a
|
|
key and GitHub 60/hour, and those budgets are shared by every user of the
|
|
assistant at once. Without caching, two people comparing package versions
|
|
can spend the day's quota before lunch and every later answer degrades to
|
|
"I couldn't check" - or worse, to the model guessing.
|
|
|
|
Fails open: if valkey is unreachable the tools still work, they are just
|
|
uncached. A dead cache should never take the assistant's tools with it.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import logging
|
|
|
|
import redis.asyncio as redis
|
|
|
|
from .settings import settings
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
_pool: redis.Redis | None = None
|
|
|
|
# Chosen per-source from how fast the underlying thing actually changes.
|
|
TTL = {
|
|
"search": 900, # 15m - fresh enough for "latest", cheap to redo
|
|
"page": 3600, # 1h - article text rarely changes within a session
|
|
"wikipedia": 21600, # 6h
|
|
"stackexchange": 21600, # 6h - 300/day keyless makes this the tightest budget
|
|
"rfc": 604800, # 7d - a published RFC is immutable
|
|
"man": 604800, # 7d
|
|
"cve": 10800, # 3h - CVSS scores do get revised
|
|
"package": 3600, # 1h - the whole point is catching a new release
|
|
"rdap": 86400, # 24h
|
|
"dns": 300, # 5m - short on purpose; people debug live changes
|
|
"tls": 1800, # 30m
|
|
"papers": 86400, # 24h
|
|
"datasheet": 604800, # 7d
|
|
}
|
|
|
|
|
|
def _key(namespace: str, *parts: str) -> str:
|
|
digest = hashlib.sha256("\x1f".join(parts).encode()).hexdigest()[:32]
|
|
return f"owuitools:{namespace}:{digest}"
|
|
|
|
|
|
async def get_redis() -> redis.Redis | None:
|
|
global _pool
|
|
if _pool is None:
|
|
try:
|
|
_pool = redis.from_url(settings.valkey_url, decode_responses=True, socket_timeout=2)
|
|
await _pool.ping()
|
|
except Exception as exc:
|
|
log.warning("cache unavailable, continuing without it: %s", exc)
|
|
_pool = None
|
|
return _pool
|
|
|
|
|
|
async def close_redis() -> None:
|
|
global _pool
|
|
if _pool is not None:
|
|
await _pool.aclose()
|
|
_pool = None
|
|
|
|
|
|
async def cached(namespace: str, *parts: str) -> str | None:
|
|
client = await get_redis()
|
|
if client is None:
|
|
return None
|
|
try:
|
|
return await client.get(_key(namespace, *parts))
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
async def store(namespace: str, value: str, *parts: str) -> None:
|
|
client = await get_redis()
|
|
if client is None:
|
|
return
|
|
try:
|
|
await client.setex(_key(namespace, *parts), TTL.get(namespace, 3600), value)
|
|
except Exception as exc:
|
|
log.debug("cache write failed for %s: %s", namespace, exc)
|