feat: add the {SCOPE}__CIRCUIT_OPEN admission policy key
Limiter rejections have always chosen between waiting and failing fast; breaker rejections had no such choice. The new key is the missing cell of that matrix, shaped exactly like QUOTA_FULL so there is nothing new to learn. It defaults to fail_fast: flipping the default would move every existing deployment's worst-case wall clock from milliseconds to the stall window, which is the wrong direction to impose on anyone. Single-source scopes are the ones that want wait, and they now have a way to say so. The two keys stay separate despite sharing a domain, because a full quota is "queue for your share" (your turn always comes) while an open circuit is "wait for the source to recover" (it might not). Policy validation collapses into SourceAdmission, the only consumer. The three client constructors used to each carry their own copy of the quota_full check; adding a second key there would have made eight copies of the same two lines. Rejection timing and message are unchanged -- admission is built inside those constructors. This commit only wires the key through; the control flow that reads it lands next.
This commit is contained in:
@@ -29,6 +29,9 @@ from loguru import logger
|
||||
from polygateway.errors import AllSourcesExhausted, CircuitOpenError
|
||||
from polygateway.sources import SourceCooldownMemo
|
||||
|
||||
# 两个准入策略键共用的值域;校验只此一处,不在各客户端重复
|
||||
_POLICIES = frozenset({"wait", "fail_fast"})
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
|
||||
@@ -137,6 +140,7 @@ class SourceAdmission:
|
||||
breaker: BreakerGate,
|
||||
backpressure: BackpressurePolicy,
|
||||
quota_full: str,
|
||||
circuit_open: str,
|
||||
memo: SourceCooldownMemo | None = None,
|
||||
pacer: AdaptivePacer | None = None,
|
||||
health_view: Callable[[str], float] | None = None,
|
||||
@@ -144,8 +148,9 @@ class SourceAdmission:
|
||||
sleep: Callable[[float], object] = asyncio.sleep,
|
||||
rng: Callable[[], float] = random.random,
|
||||
) -> None:
|
||||
if quota_full not in ("wait", "fail_fast"):
|
||||
raise ValueError(f"quota_full 必须是 wait|fail_fast: {quota_full!r}")
|
||||
for name, value in (("quota_full", quota_full), ("circuit_open", circuit_open)):
|
||||
if value not in _POLICIES:
|
||||
raise ValueError(f"{name} 必须是 wait|fail_fast: {value!r}")
|
||||
self._scope = scope
|
||||
self._sources = sources
|
||||
self._selector = selector
|
||||
@@ -153,6 +158,7 @@ class SourceAdmission:
|
||||
self._breaker = breaker
|
||||
self._bp = backpressure
|
||||
self._quota_full = quota_full
|
||||
self._circuit_open = circuit_open
|
||||
self._memo = memo or SourceCooldownMemo(now=now)
|
||||
self._pacer = pacer
|
||||
self._health_view = health_view
|
||||
|
||||
@@ -178,6 +178,7 @@ class RetryMW:
|
||||
retry: RetryPolicy,
|
||||
backpressure: BackpressurePolicy,
|
||||
quota_full: str = "wait",
|
||||
circuit_open: str = "fail_fast",
|
||||
cooldown_memo: SourceCooldownMemo | None = None,
|
||||
pacer: AdaptivePacer | None = None,
|
||||
emitter: object | None = None,
|
||||
@@ -185,8 +186,6 @@ class RetryMW:
|
||||
sleep: Callable[[float], Awaitable[None]] = asyncio.sleep,
|
||||
rng: Callable[[], float] = random.random,
|
||||
) -> None:
|
||||
if quota_full not in ("wait", "fail_fast"):
|
||||
raise ValueError(f"quota_full 必须是 wait|fail_fast: {quota_full!r}")
|
||||
self._scope = scope
|
||||
self._sources = list(sources)
|
||||
# 记账写回与 pacer 结算仍在 `_attempt` 内,故这三者由本类持有并与
|
||||
@@ -212,6 +211,7 @@ class RetryMW:
|
||||
breaker=self._breaker,
|
||||
backpressure=backpressure,
|
||||
quota_full=quota_full,
|
||||
circuit_open=circuit_open,
|
||||
memo=cooldown_memo,
|
||||
pacer=self._pacer,
|
||||
health_view=self._outcome_sink.health if self._outcome_sink else None,
|
||||
|
||||
Reference in New Issue
Block a user