67 lines
2.1 KiB
Python
67 lines
2.1 KiB
Python
|
|
# retoor <retoor@molodetz.nl>
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import time
|
||
|
|
from collections import OrderedDict
|
||
|
|
|
||
|
|
|
||
|
|
class TTLCache:
|
||
|
|
def __init__(self, ttl: int, max_size: int = 0):
|
||
|
|
self.ttl = ttl
|
||
|
|
self.max_size = max_size
|
||
|
|
self._store: OrderedDict[str, tuple[object, float]] = OrderedDict()
|
||
|
|
|
||
|
|
def get(self, key: str):
|
||
|
|
entry = self._store.get(key)
|
||
|
|
if entry is None:
|
||
|
|
return None
|
||
|
|
value, expiry = entry
|
||
|
|
if time.time() >= expiry:
|
||
|
|
self._store.pop(key, None)
|
||
|
|
return None
|
||
|
|
self._store.move_to_end(key)
|
||
|
|
return value
|
||
|
|
|
||
|
|
def set(self, key: str, value) -> None:
|
||
|
|
self._store[key] = (value, time.time() + self.ttl)
|
||
|
|
self._store.move_to_end(key)
|
||
|
|
if self.max_size and len(self._store) > self.max_size:
|
||
|
|
self._store.popitem(last=False)
|
||
|
|
|
||
|
|
def pop(self, key: str) -> None:
|
||
|
|
self._store.pop(key, None)
|
||
|
|
|
||
|
|
def clear(self) -> None:
|
||
|
|
self._store.clear()
|
||
|
|
|
||
|
|
|
||
|
|
class CacheStateGate:
|
||
|
|
def __init__(self, *, ttl: int = 60, max_staleness: float = 5.0):
|
||
|
|
self.ttl = ttl
|
||
|
|
self.max_staleness = max_staleness
|
||
|
|
self._cache = TTLCache(ttl=ttl)
|
||
|
|
self._versions: dict[str, int] = {}
|
||
|
|
self._bumped_at: dict[str, float] = {}
|
||
|
|
|
||
|
|
def get_version(self, name: str) -> int:
|
||
|
|
return int(self._versions.get(name, 0))
|
||
|
|
|
||
|
|
def bump(self, name: str) -> int:
|
||
|
|
next_version = self.get_version(name) + 1
|
||
|
|
self._versions[name] = next_version
|
||
|
|
self._bumped_at[name] = time.time()
|
||
|
|
self._cache.clear()
|
||
|
|
return next_version
|
||
|
|
|
||
|
|
def get(self, key: str, version_name: str, loader):
|
||
|
|
bumped_at = self._bumped_at.get(version_name, 0.0)
|
||
|
|
if bumped_at and (time.time() - bumped_at) < self.max_staleness:
|
||
|
|
self._cache.pop(key)
|
||
|
|
version_key = f"{version_name}:{self.get_version(version_name)}"
|
||
|
|
cached = self._cache.get(version_key)
|
||
|
|
if cached is not None:
|
||
|
|
return cached
|
||
|
|
value = loader()
|
||
|
|
self._cache.set(version_key, value)
|
||
|
|
return value
|