91 lines
2.6 KiB
Python
91 lines
2.6 KiB
Python
# retoor <retoor@molodetz.nl>
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import time
|
|
from collections import OrderedDict
|
|
from dataclasses import dataclass
|
|
from typing import Callable
|
|
|
|
from devplacepy.services.nodes.spec import SPEC
|
|
|
|
LIMITS = SPEC.limits
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class CachedResult:
|
|
status: int
|
|
headers: dict
|
|
body: bytes
|
|
truncated: bool
|
|
stored_at: float
|
|
|
|
|
|
def cache_key(method: str, url: str) -> str:
|
|
return hashlib.sha256(f"{method.upper()} {url}".encode()).hexdigest()
|
|
|
|
|
|
class ResultCache:
|
|
def __init__(
|
|
self,
|
|
ttl_seconds: int = LIMITS.cache_ttl_seconds,
|
|
max_bytes: int = LIMITS.cache_max_bytes,
|
|
entry_max_bytes: int = LIMITS.cache_entry_max_bytes,
|
|
clock: Callable[[], float] = time.monotonic,
|
|
) -> None:
|
|
self.ttl_seconds = max(0, min(ttl_seconds, LIMITS.cache_ttl_seconds))
|
|
self.max_bytes = max_bytes
|
|
self.entry_max_bytes = min(entry_max_bytes, LIMITS.cache_entry_max_bytes)
|
|
self._clock = clock
|
|
self._entries: OrderedDict[str, CachedResult] = OrderedDict()
|
|
self._bytes = 0
|
|
self.hits = 0
|
|
self.misses = 0
|
|
|
|
@property
|
|
def size_bytes(self) -> int:
|
|
return self._bytes
|
|
|
|
def __len__(self) -> int:
|
|
return len(self._entries)
|
|
|
|
def _drop(self, key: str) -> None:
|
|
entry = self._entries.pop(key, None)
|
|
if entry is not None:
|
|
self._bytes -= len(entry.body)
|
|
|
|
def get(self, method: str, url: str) -> CachedResult | None:
|
|
key = cache_key(method, url)
|
|
entry = self._entries.get(key)
|
|
if entry is None:
|
|
self.misses += 1
|
|
return None
|
|
if self._clock() - entry.stored_at >= self.ttl_seconds:
|
|
self._drop(key)
|
|
self.misses += 1
|
|
return None
|
|
self._entries.move_to_end(key)
|
|
self.hits += 1
|
|
return entry
|
|
|
|
def put(
|
|
self, method: str, url: str, status: int, headers: dict, body: bytes, truncated: bool
|
|
) -> bool:
|
|
if self.ttl_seconds <= 0 or status >= 500 or truncated:
|
|
return False
|
|
if len(body) > self.entry_max_bytes or len(body) > self.max_bytes:
|
|
return False
|
|
key = cache_key(method, url)
|
|
self._drop(key)
|
|
self._entries[key] = CachedResult(status, dict(headers), body, truncated, self._clock())
|
|
self._bytes += len(body)
|
|
while self._bytes > self.max_bytes and self._entries:
|
|
oldest = next(iter(self._entries))
|
|
self._drop(oldest)
|
|
return True
|
|
|
|
def clear(self) -> None:
|
|
self._entries.clear()
|
|
self._bytes = 0
|