mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 17:30:23 +08:00
- 删除全部 Python 源码 (src/) 及 Alembic 迁移脚本,归档至 _deprecated_py_src/ - 重构 Rust gateway ai_pipeline: 拆分 planner/finalize 模块,新增 contracts/adaptation 层 - 重组 handlers 模块为 admin/public/proxy/internal/shared 子模块结构 - 新增 executor 模块,引入 Rust 原生数据库迁移 (aether-data/migrations) - 简化 CI/Docker 构建流程,移除 base image 二级构建,统一为单一 app image - 移除 Python 相关基础设施文件 (entrypoint.sh, gunicorn_conf.py, Dockerfile.base)
105 lines
3.5 KiB
Python
105 lines
3.5 KiB
Python
"""In-process pool health score cache.
|
|
|
|
This cache avoids recomputing per-key health aggregation on every request.
|
|
It does not replace persistent health storage; source data still comes from
|
|
``ProviderAPIKey.health_by_format`` carried on key objects.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import threading
|
|
import time
|
|
from typing import Any
|
|
|
|
_TTL_SECONDS = 30.0
|
|
_MAX_PROVIDERS = 500 # 最大缓存 provider 数量,防止无界增长
|
|
_LOCK = threading.Lock()
|
|
_CACHE: dict[str, tuple[float, dict[str, float]]] = {}
|
|
|
|
|
|
def aggregate_health_score(health_by_format: Any) -> float:
|
|
"""Aggregate health score from ``health_by_format`` (lower-bound strategy)."""
|
|
if not isinstance(health_by_format, dict) or not health_by_format:
|
|
return 1.0
|
|
scores: list[float] = []
|
|
for item in health_by_format.values():
|
|
if not isinstance(item, dict):
|
|
continue
|
|
try:
|
|
score = float(item.get("health_score") or 1.0)
|
|
except (TypeError, ValueError):
|
|
score = 1.0
|
|
scores.append(max(0.0, min(score, 1.0)))
|
|
if not scores:
|
|
return 1.0
|
|
return min(scores)
|
|
|
|
|
|
def get_health_scores(provider_id: str, keys: list[Any]) -> dict[str, float]:
|
|
"""Return key health scores with per-provider TTL cache.
|
|
|
|
Uses incremental merge: if the cache is still valid but missing some keys,
|
|
only the missing keys are computed and merged into the existing cache entry.
|
|
"""
|
|
now = time.monotonic()
|
|
keys_by_id: dict[str, Any] = {}
|
|
for k in keys:
|
|
kid = str(getattr(k, "id", "") or "")
|
|
if kid:
|
|
keys_by_id[kid] = k
|
|
if not keys_by_id:
|
|
return {}
|
|
|
|
with _LOCK:
|
|
cached = _CACHE.get(provider_id)
|
|
if cached is not None:
|
|
expires_at, payload = cached
|
|
if now < expires_at:
|
|
missing_ids = [kid for kid in keys_by_id if kid not in payload]
|
|
if not missing_ids:
|
|
return {kid: payload[kid] for kid in keys_by_id}
|
|
# Compute only for missing keys, merge into existing cache
|
|
for kid in missing_ids:
|
|
payload[kid] = aggregate_health_score(
|
|
getattr(keys_by_id[kid], "health_by_format", None)
|
|
)
|
|
# Trim stale key entries that are no longer in the current key set
|
|
stale_ids = [sid for sid in payload if sid not in keys_by_id]
|
|
for sid in stale_ids:
|
|
del payload[sid]
|
|
return {kid: payload[kid] for kid in keys_by_id}
|
|
|
|
fresh: dict[str, float] = {}
|
|
for kid, key in keys_by_id.items():
|
|
fresh[kid] = aggregate_health_score(getattr(key, "health_by_format", None))
|
|
|
|
with _LOCK:
|
|
_CACHE[provider_id] = (now + _TTL_SECONDS, fresh)
|
|
# 超出上限时清理过期条目,仍超限则淘汰最旧条目
|
|
if len(_CACHE) > _MAX_PROVIDERS:
|
|
expired = [k for k, (exp, _) in _CACHE.items() if now >= exp]
|
|
for k in expired:
|
|
del _CACHE[k]
|
|
if len(_CACHE) > _MAX_PROVIDERS:
|
|
oldest_key = min(_CACHE, key=lambda k: _CACHE[k][0])
|
|
del _CACHE[oldest_key]
|
|
return dict(fresh)
|
|
|
|
|
|
def invalidate_provider_health_scores(provider_id: str) -> None:
|
|
"""Invalidate health-score cache for one provider."""
|
|
with _LOCK:
|
|
_CACHE.pop(provider_id, None)
|
|
|
|
|
|
def _clear_cache_for_tests() -> None:
|
|
with _LOCK:
|
|
_CACHE.clear()
|
|
|
|
|
|
__all__ = [
|
|
"aggregate_health_score",
|
|
"get_health_scores",
|
|
"invalidate_provider_health_scores",
|
|
]
|