Initial Commit
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user