feat: gate in-call demotion on credible alternative health

Round 3 showed unconditional yield-after-two-failures pushes the third
attempt onto known-bad sources in heterogeneous pools (83% vs 10%
expected success). OutcomeAwareSelector now exposes health(); a failed
source only yields when some untried candidate scores at least half its
health. Health-blind selectors keep the unconditional rule.
This commit is contained in:
2026-07-21 11:24:52 -04:00
parent 58061c7536
commit 0e17f71482
6 changed files with 95 additions and 4 deletions
+30 -4
View File
@@ -74,13 +74,36 @@ def backoff_delay(
def _demote_call_failures(
ordered: list[SourceConfig], attempt_fails: dict[str, int]
ordered: list[SourceConfig],
attempt_fails: dict[str, int],
health: Callable[[str], float] | None,
) -> list[SourceConfig]:
"""调用内降权(设计 §3.3): 本调用失败 ≥2 次的源移尾;全失败则保持原序。"""
"""调用内降权(设计 §3.3/§3.36): 失败 ≥2 次且存在可信替代才让位。
可信替代 = 某未失败候选 health ≥ 0.5 × 失败源 health——异构池里健康源
偶发失败不该被推向已知坏源(第三轮教训: 期望成功率 83% vs 10%)。
无健康视图(round_robin 等)保持无条件降权(冷启动保护)。
"""
demoted = [s for s in ordered if attempt_fails.get(s.name, 0) >= 2]
if not demoted or len(demoted) == len(ordered):
return ordered
return [s for s in ordered if attempt_fails.get(s.name, 0) < 2] + demoted
if health is not None:
demoted = _credible_demotions(ordered, demoted, attempt_fails, health)
if not demoted:
return ordered
names = {d.name for d in demoted}
return [s for s in ordered if s.name not in names] + demoted
def _credible_demotions(
ordered: list[SourceConfig],
demoted: list[SourceConfig],
attempt_fails: dict[str, int],
health: Callable[[str], float],
) -> list[SourceConfig]:
"""健康门槛过滤: 仅当存在"健康分 ≥ 失败源一半"的未失败候选,让位才有意义。"""
alts = [o for o in ordered if attempt_fails.get(o.name, 0) < 2]
return [s for s in demoted if any(health(o.name) >= 0.5 * health(s.name) for o in alts)]
def _failure_reason(exc: PolyGatewayError) -> str:
@@ -138,6 +161,7 @@ class RetryMW:
self._memo = cooldown_memo or SourceCooldownMemo(now=now)
# M2.5: 选源器可选健康喂数端口,构造期 isinstance 判定一次(设计 §3.2)
self._outcome_sink = selector if isinstance(selector, OutcomeAwareSelector) else None
self._health_view = self._outcome_sink.health if self._outcome_sink else None
# M2.5 §3.35: AIMD 自适应并发——429 收紧、成功回涨,超限调用排队不烧预算
self._pacer = pacer or AdaptivePacer(ceiling=64.0)
self._emitter = emitter
@@ -180,7 +204,9 @@ class RetryMW:
) -> tuple[tuple[SourceConfig, Permit, GateDecision] | None, int]:
stats = {s.name: await self._quota.stats(s) for s in self._sources}
gate_rejections = 0
ordered = _demote_call_failures(self._selector.order(self._sources, stats), attempt_fails)
ordered = _demote_call_failures(
self._selector.order(self._sources, stats), attempt_fails, self._health_view
)
for cand in ordered:
if self._memo.active(cand.name):
# 冷却备忘跳过也计入拒绝数,保住 circuit_open 判据(CHS 同款)
+2
View File
@@ -194,6 +194,8 @@ class OutcomeAwareSelector(Protocol):
def record_outcome(self, source_name: str, ok: bool) -> None: ...
def health(self, source_name: str) -> float: ...
@runtime_checkable
class StructuredOutputStrategy(Protocol):
+4
View File
@@ -63,6 +63,10 @@ class HealthAwareSelector:
prev = self._ewma.get(source_name, 1.0)
self._ewma[source_name] = prev + _EWMA_ALPHA * ((1.0 if ok else 0.0) - prev)
def health(self, source_name: str) -> float:
"""EWMA 裸值(OutcomeAwareSelector 端口): RetryMW 降权门槛用。"""
return self._ewma.get(source_name, 1.0)
def _score(self, name: str, stats: dict[str, SourceStats]) -> float:
ewma = max(self._ewma.get(name, 1.0), _SCORE_FLOOR)
inflight = stats[name].inflight if name in stats else 0