feat: add health-aware P2C selector as default
Score is success-rate EWMA over (1 + inflight) with a 0.05 exploration floor so quarantined sources can prove recovery; EWMA climb doubles as slow-start. New optional OutcomeAwareSelector port feeds attempt outcomes.
This commit is contained in:
@@ -7,6 +7,7 @@ RetryMW 的进程本地状态——熔断开路的源在本地记冷却截止,
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
import time
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
@@ -15,6 +16,9 @@ if TYPE_CHECKING:
|
||||
|
||||
from polygateway.types import SourceConfig, SourceStats
|
||||
|
||||
_EWMA_ALPHA = 0.2 # 健康 EWMA 步长: 约 10 次成功从谷底爬回 0.9(天然 slow-start)
|
||||
_SCORE_FLOOR = 0.05 # 探索地板: 塌陷源保有微量被选概率,恢复靠真实成功自证
|
||||
|
||||
|
||||
class RoundRobinSelector:
|
||||
"""轮转起点后移(CHS selector.py:20 同款);单 client 内游标推进。"""
|
||||
@@ -41,6 +45,43 @@ class LeastInflightSelector:
|
||||
return sorted(sources, key=lambda s: stats[s.name].inflight if s.name in stats else 0)
|
||||
|
||||
|
||||
class HealthAwareSelector:
|
||||
"""健康感知选源(M2.5 设计 §3.2;蓝本 Envoy least-request + gRPC WRR)。
|
||||
|
||||
score = max(ewma_success, 地板) / (1 + inflight)。头名经 P2C(随机取
|
||||
两源比分,高者先)引入探索;其余按分数降序。健康态为进程本地(业界
|
||||
共识: Envoy/Finagle/gRPC 全本地),属 client 实例,不违反纯 asyncio 中立。
|
||||
已知取舍(设计 §3.2): 仅头名随机化,多 worker 溢出会集中到同一次优源。
|
||||
"""
|
||||
|
||||
def __init__(self, *, rng: Callable[[], float] = random.random) -> None:
|
||||
self._rng = rng
|
||||
self._ewma: dict[str, float] = {}
|
||||
|
||||
def record_outcome(self, source_name: str, ok: bool) -> None:
|
||||
"""尝试结果喂数(OutcomeAwareSelector 端口);初始 1.0 乐观起步。"""
|
||||
prev = self._ewma.get(source_name, 1.0)
|
||||
self._ewma[source_name] = prev + _EWMA_ALPHA * ((1.0 if ok else 0.0) - prev)
|
||||
|
||||
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
|
||||
return ewma / (1.0 + inflight)
|
||||
|
||||
def order(
|
||||
self, sources: list[SourceConfig], stats: dict[str, SourceStats]
|
||||
) -> list[SourceConfig]:
|
||||
if len(sources) < 2:
|
||||
return list(sources)
|
||||
ranked = sorted(sources, key=lambda s: self._score(s.name, stats), reverse=True)
|
||||
# P2C: 随机取两源比分,胜者提为头名(平分取采样序首位)
|
||||
i = int(self._rng() * len(sources)) % len(sources)
|
||||
j = int(self._rng() * len(sources)) % len(sources)
|
||||
a, b = sources[i], sources[j]
|
||||
head = a if self._score(a.name, stats) >= self._score(b.name, stats) else b
|
||||
return [head] + [s for s in ranked if s.name != head.name]
|
||||
|
||||
|
||||
class SourceCooldownMemo:
|
||||
"""进程本地的源冷却备忘(CHS governance.py:107 同款)。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user