| """ |
| In-memory TTL cache. |
| |
| Keys are strings (typically image hashes + provider name + capability). |
| Values are arbitrary JSON-serializable objects. |
| |
| The cache tracks hit/miss counters which the metrics subsystem reads. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import threading |
| import time |
| from collections import OrderedDict |
| from typing import Any, Optional |
|
|
|
|
| class Cache: |
| """LRU + TTL cache. Thread-safe.""" |
|
|
| def __init__(self, ttl_seconds: int = 3600, max_entries: int = 1000) -> None: |
| self._ttl = ttl_seconds |
| self._max = max_entries |
| self._data: OrderedDict[str, tuple[float, Any]] = OrderedDict() |
| self._lock = threading.RLock() |
| self._hits = 0 |
| self._misses = 0 |
| self._evictions = 0 |
|
|
| |
| |
| |
| def get(self, key: str) -> Optional[Any]: |
| with self._lock: |
| entry = self._data.get(key) |
| if entry is None: |
| self._misses += 1 |
| return None |
| expires_at, value = entry |
| if time.time() > expires_at: |
| self._data.pop(key, None) |
| self._misses += 1 |
| return None |
| self._data.move_to_end(key) |
| self._hits += 1 |
| return value |
|
|
| def set(self, key: str, value: Any, ttl: Optional[int] = None) -> None: |
| with self._lock: |
| expires_at = time.time() + (ttl if ttl is not None else self._ttl) |
| self._data[key] = (expires_at, value) |
| self._data.move_to_end(key) |
| while len(self._data) > self._max: |
| self._data.popitem(last=False) |
| self._evictions += 1 |
|
|
| def invalidate(self, key: str) -> bool: |
| with self._lock: |
| return self._data.pop(key, None) is not None |
|
|
| def clear(self) -> int: |
| with self._lock: |
| n = len(self._data) |
| self._data.clear() |
| return n |
|
|
| |
| |
| |
| def stats(self) -> dict: |
| with self._lock: |
| total = self._hits + self._misses |
| return { |
| "entries": len(self._data), |
| "max_entries": self._max, |
| "ttl_seconds": self._ttl, |
| "hits": self._hits, |
| "misses": self._misses, |
| "hit_ratio": round(self._hits / total, 4) if total else 0.0, |
| "evictions": self._evictions, |
| } |
|
|