mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 01:40:21 +08:00
131 lines
4.0 KiB
Python
131 lines
4.0 KiB
Python
|
|
"""
|
||
|
|
Billing in-process cache.
|
||
|
|
|
||
|
|
This module provides a small TTL cache for billing rule lookups and other
|
||
|
|
high-read, low-churn billing configuration objects.
|
||
|
|
|
||
|
|
Important:
|
||
|
|
- Keep cached values *session-agnostic*. Avoid caching SQLAlchemy ORM objects
|
||
|
|
bound to a specific Session; prefer plain dataclasses / dicts.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import time
|
||
|
|
from typing import Any
|
||
|
|
|
||
|
|
|
||
|
|
class BillingCache:
|
||
|
|
"""
|
||
|
|
Simple TTL + LRU cache.
|
||
|
|
|
||
|
|
- TTL: 300s (5 minutes)
|
||
|
|
- Max entries per cache: 2048 (evict oldest on overflow)
|
||
|
|
"""
|
||
|
|
|
||
|
|
TTL_SECONDS = 300
|
||
|
|
MAX_ENTRIES = 2048
|
||
|
|
|
||
|
|
_rule_cache: dict[str, tuple[Any, float]] = {}
|
||
|
|
_collector_cache: dict[str, tuple[Any, float]] = {}
|
||
|
|
_default_rule_cache: dict[str, tuple[Any, float]] = {}
|
||
|
|
|
||
|
|
# ----------------------------
|
||
|
|
# Rule cache
|
||
|
|
# ----------------------------
|
||
|
|
@classmethod
|
||
|
|
def get_rule(cls, cache_key: str) -> Any | None:
|
||
|
|
return cls._get(cls._rule_cache, cache_key)
|
||
|
|
|
||
|
|
@classmethod
|
||
|
|
def set_rule(cls, cache_key: str, value: Any) -> None:
|
||
|
|
cls._set(cls._rule_cache, cache_key, value)
|
||
|
|
|
||
|
|
# ----------------------------
|
||
|
|
# Default-rule cache
|
||
|
|
# ----------------------------
|
||
|
|
@classmethod
|
||
|
|
def get_default_rule(cls, cache_key: str) -> Any | None:
|
||
|
|
return cls._get(cls._default_rule_cache, cache_key)
|
||
|
|
|
||
|
|
@classmethod
|
||
|
|
def set_default_rule(cls, cache_key: str, value: Any) -> None:
|
||
|
|
cls._set(cls._default_rule_cache, cache_key, value)
|
||
|
|
|
||
|
|
# ----------------------------
|
||
|
|
# Collector cache (reserved)
|
||
|
|
# ----------------------------
|
||
|
|
@classmethod
|
||
|
|
def get_collectors(cls, cache_key: str) -> Any | None:
|
||
|
|
return cls._get(cls._collector_cache, cache_key)
|
||
|
|
|
||
|
|
@classmethod
|
||
|
|
def set_collectors(cls, cache_key: str, value: Any) -> None:
|
||
|
|
cls._set(cls._collector_cache, cache_key, value)
|
||
|
|
|
||
|
|
# ----------------------------
|
||
|
|
# Invalidation
|
||
|
|
# ----------------------------
|
||
|
|
@classmethod
|
||
|
|
def invalidate_all(cls) -> None:
|
||
|
|
cls._rule_cache.clear()
|
||
|
|
cls._collector_cache.clear()
|
||
|
|
cls._default_rule_cache.clear()
|
||
|
|
|
||
|
|
@classmethod
|
||
|
|
def invalidate_model(cls, model_name: str) -> None:
|
||
|
|
"""
|
||
|
|
Invalidate cache entries referencing a model name.
|
||
|
|
|
||
|
|
Note:
|
||
|
|
- This is best-effort string matching (cache key format must include model_name).
|
||
|
|
"""
|
||
|
|
cls._invalidate_by_substring(cls._rule_cache, model_name)
|
||
|
|
cls._invalidate_by_substring(cls._default_rule_cache, model_name)
|
||
|
|
|
||
|
|
# ----------------------------
|
||
|
|
# Internal helpers
|
||
|
|
# ----------------------------
|
||
|
|
@classmethod
|
||
|
|
def _get(cls, cache: dict[str, tuple[Any, float]], key: str) -> Any | None:
|
||
|
|
item = cache.get(key)
|
||
|
|
if item is None:
|
||
|
|
return None
|
||
|
|
value, ts = item
|
||
|
|
if time.time() - ts < cls.TTL_SECONDS:
|
||
|
|
return value
|
||
|
|
# expired
|
||
|
|
try:
|
||
|
|
del cache[key]
|
||
|
|
except KeyError:
|
||
|
|
pass
|
||
|
|
return None
|
||
|
|
|
||
|
|
@classmethod
|
||
|
|
def _set(cls, cache: dict[str, tuple[Any, float]], key: str, value: Any) -> None:
|
||
|
|
"""Set with LRU eviction when cache exceeds MAX_ENTRIES."""
|
||
|
|
now = time.time()
|
||
|
|
cache[key] = (value, now)
|
||
|
|
|
||
|
|
# Evict oldest entries if over limit
|
||
|
|
if len(cache) > cls.MAX_ENTRIES:
|
||
|
|
cls._evict_oldest(cache, cls.MAX_ENTRIES // 4)
|
||
|
|
|
||
|
|
@classmethod
|
||
|
|
def _evict_oldest(cls, cache: dict[str, tuple[Any, float]], count: int) -> None:
|
||
|
|
"""Evict the oldest `count` entries from cache."""
|
||
|
|
if not cache or count <= 0:
|
||
|
|
return
|
||
|
|
# Sort by timestamp (oldest first) and remove
|
||
|
|
sorted_keys = sorted(cache.keys(), key=lambda k: cache[k][1])
|
||
|
|
for k in sorted_keys[:count]:
|
||
|
|
cache.pop(k, None)
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def _invalidate_by_substring(cache: dict[str, tuple[Any, float]], needle: str) -> None:
|
||
|
|
if not needle:
|
||
|
|
return
|
||
|
|
keys = [k for k in cache.keys() if needle in k]
|
||
|
|
for k in keys:
|
||
|
|
cache.pop(k, None)
|