fix(awooop): cache heavy operator summaries
This commit is contained in:
122
apps/api/src/services/operator_summary_cache.py
Normal file
122
apps/api/src/services/operator_summary_cache.py
Normal file
@@ -0,0 +1,122 @@
|
||||
"""Short TTL cache for read-only AwoooP operator summaries.
|
||||
|
||||
This cache intentionally lives in the API pod memory. It reduces repeated heavy
|
||||
operator-console reads without becoming a new source of truth.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import time
|
||||
from copy import deepcopy
|
||||
from dataclasses import dataclass
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from typing import Any
|
||||
|
||||
|
||||
_CACHE_SCHEMA_VERSION = "operator_summary_cache_v1"
|
||||
_CACHE_SOURCE = "api_pod_memory"
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class _CacheRecord:
|
||||
value: dict[str, Any]
|
||||
stored_at: datetime
|
||||
stored_monotonic: float
|
||||
ttl_seconds: int
|
||||
|
||||
|
||||
_CACHE: dict[str, _CacheRecord] = {}
|
||||
|
||||
|
||||
def _cache_key(namespace: str, key_parts: dict[str, Any]) -> str:
|
||||
payload = json.dumps(
|
||||
{"namespace": namespace, "key_parts": key_parts},
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
default=str,
|
||||
)
|
||||
digest = hashlib.sha256(payload.encode("utf-8")).hexdigest()
|
||||
return f"{namespace}:{digest}"
|
||||
|
||||
|
||||
def _cache_meta(
|
||||
*,
|
||||
status: str,
|
||||
record: _CacheRecord,
|
||||
age_seconds: float,
|
||||
) -> dict[str, Any]:
|
||||
ttl_seconds = max(1, int(record.ttl_seconds))
|
||||
expires_at = record.stored_at + timedelta(seconds=ttl_seconds)
|
||||
return {
|
||||
"schema_version": _CACHE_SCHEMA_VERSION,
|
||||
"status": status,
|
||||
"source": _CACHE_SOURCE,
|
||||
"ttl_seconds": ttl_seconds,
|
||||
"age_seconds": round(max(0.0, age_seconds), 3),
|
||||
"stored_at": record.stored_at.isoformat(),
|
||||
"expires_at": expires_at.isoformat(),
|
||||
}
|
||||
|
||||
|
||||
def _with_cache_meta(value: dict[str, Any], meta: dict[str, Any]) -> dict[str, Any]:
|
||||
response = deepcopy(value)
|
||||
response["cache"] = meta
|
||||
return response
|
||||
|
||||
|
||||
def get_cached_operator_summary(
|
||||
namespace: str,
|
||||
key_parts: dict[str, Any],
|
||||
*,
|
||||
ttl_seconds: int,
|
||||
now_monotonic: float | None = None,
|
||||
) -> dict[str, Any] | None:
|
||||
"""Return cached summary with hit metadata, or None if absent/expired."""
|
||||
cache_key = _cache_key(namespace, key_parts)
|
||||
record = _CACHE.get(cache_key)
|
||||
if record is None:
|
||||
return None
|
||||
|
||||
now_value = time.monotonic() if now_monotonic is None else now_monotonic
|
||||
ttl_value = max(1, int(ttl_seconds))
|
||||
age_seconds = now_value - record.stored_monotonic
|
||||
if age_seconds >= ttl_value:
|
||||
_CACHE.pop(cache_key, None)
|
||||
return None
|
||||
|
||||
return _with_cache_meta(
|
||||
record.value,
|
||||
_cache_meta(status="hit", record=record, age_seconds=age_seconds),
|
||||
)
|
||||
|
||||
|
||||
def store_operator_summary(
|
||||
namespace: str,
|
||||
key_parts: dict[str, Any],
|
||||
value: dict[str, Any],
|
||||
*,
|
||||
ttl_seconds: int,
|
||||
now_monotonic: float | None = None,
|
||||
now_utc: datetime | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Store a fresh summary and return it with miss metadata."""
|
||||
cache_key = _cache_key(namespace, key_parts)
|
||||
stored_at = now_utc or datetime.now(UTC)
|
||||
record = _CacheRecord(
|
||||
value=deepcopy(value),
|
||||
stored_at=stored_at,
|
||||
stored_monotonic=time.monotonic() if now_monotonic is None else now_monotonic,
|
||||
ttl_seconds=max(1, int(ttl_seconds)),
|
||||
)
|
||||
_CACHE[cache_key] = record
|
||||
return _with_cache_meta(
|
||||
record.value,
|
||||
_cache_meta(status="miss", record=record, age_seconds=0.0),
|
||||
)
|
||||
|
||||
|
||||
def clear_operator_summary_cache() -> None:
|
||||
"""Clear process-local cache for tests and controlled operator refreshes."""
|
||||
_CACHE.clear()
|
||||
Reference in New Issue
Block a user