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:
@@ -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 同款)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user