feat: add rate-channel breaker config and health_aware default

Threshold auto-raise now uses per-source concurrency only (the M2
global-concurrency formula neutered the breaker at scale). Four new
optional keys: MIN_CALLS, FAIL_RATE, WINDOW_S, MAX_COOLDOWN_S.
This commit is contained in:
2026-07-21 08:49:36 -04:00
parent 8e2673487a
commit 72b25724d8
3 changed files with 83 additions and 10 deletions
+27 -7
View File
@@ -45,7 +45,7 @@ _SOURCE_FIELDS: dict[str, tuple[str, str]] = {
"TRUST_ENV": ("trust_env", "bool"),
}
_RESERVED_SEGMENTS = frozenset({"GLOBAL", "RETRY", "BREAKER", "BACKPRESSURE"})
_SELECTORS = frozenset({"round_robin", "least_inflight"})
_SELECTORS = frozenset({"round_robin", "least_inflight", "health_aware"})
_QUOTA_FULL = frozenset({"wait", "fail_fast"})
# 背压默认(M1 仅 poll 生效;CHS _BACKOFF_S=0.05 同源)
_DEFAULT_STALL_WINDOW_S = 300.0
@@ -136,7 +136,7 @@ class GatewaySettings:
retry=retry,
breaker=breaker,
backpressure=_load_backpressure(scope_u, env),
selector=_load_choice(env, f"{scope_u}__SELECTOR", _SELECTORS, "round_robin"),
selector=_load_choice(env, f"{scope_u}__SELECTOR", _SELECTORS, "health_aware"),
quota_full=_load_choice(env, f"{scope_u}__QUOTA_FULL", _QUOTA_FULL, "wait"),
**_load_pgw(env),
)
@@ -211,10 +211,9 @@ def _load_breaker(
key_c, cool = _require(env, f"{scope}__BREAKER__COOLDOWN_S", "LLM_CIRCUIT_BREAKER_COOLDOWN")
threshold = int(_cast(thr, "int", key_t))
cooldown_s = float(_cast(cool, "float", key_c))
# 有效阈值 = max(配置值, 并发×2)——三项目 .env 注释的手动约定入库(设计 §9 行 4)
concurrency = global_limits.max_concurrency or max(
(s.max_concurrency for s in sources), default=0
)
# 有效阈值 = max(配置值, 源级并发×2)。M2.5 修正: 只看源级并发——M2 曾用
# 全局并发抬升(SOAK 100→阈值 200)使熔断失灵(病灶 2,设计 2026-07-21-m25)
concurrency = max((s.max_concurrency for s in sources), default=0)
if concurrency > 0:
threshold = max(threshold, concurrency * 2)
slowest = max(s.timeout_s for s in sources)
@@ -231,7 +230,28 @@ def _load_breaker(
else:
# 派生规则: 探针租约须撑过一次最慢调用,且不短于冷却期(第三项保证守卫恒成立)
probe_ttl_s = max(2 * slowest, cooldown_s, probe_floor)
return BreakerConfig(fail_threshold=threshold, cooldown_s=cooldown_s, probe_ttl_s=probe_ttl_s)
# M2.5 失败率通道参数(可选键,库缺省——韧性参数缺省先例同 backpressure)
min_calls = int(_opt_float(env, f"{scope}__BREAKER__MIN_CALLS", 10))
fail_rate = _opt_float(env, f"{scope}__BREAKER__FAIL_RATE", 0.6)
window_s = _opt_float(env, f"{scope}__BREAKER__WINDOW_S", 60.0)
max_cooldown_s = _opt_float(env, f"{scope}__BREAKER__MAX_COOLDOWN_S", max(300.0, cooldown_s))
return BreakerConfig(
fail_threshold=threshold,
cooldown_s=cooldown_s,
probe_ttl_s=probe_ttl_s,
min_calls=min_calls,
fail_rate=fail_rate,
window_s=window_s,
max_cooldown_s=max_cooldown_s,
)
def _opt_float(env: Mapping[str, str], key: str, default: float) -> float:
"""可选韧性参数: 缺省用库值,显式配置则解析(坏值 fail-loud)。"""
raw = env.get(key)
if raw is None or raw == "":
return default
return float(_cast(raw, "float", key))
def _load_backpressure(scope: str, env: Mapping[str, str]) -> BackpressurePolicy: