Files
PolyGateway/src/polygateway/middleware/ratelimit.py
T
iomgaa a57a5cea72 fix: let assembly defects pierce the gate wrappers
Independent verification caught that the split shipped in the previous
commit did not actually hold on the only path production uses. The gate
wrappers re-raise GovernanceBackendError but nothing else, so
SourceNotConfiguredError fell into the following `except Exception` and
came back out as a governance_backend_down failure with retry_after_s=5.0.
A misconfigured source name would still retry forever and never surface.

The existing tests missed it because both of them call the private _cfg()
directly, one layer below the wrapper the governance loops actually go
through. The regression test goes through QuotaGate.

telemetry.py has to widen its terminal catch in the same commit: once the
wrapper stops relabeling the error, it is no longer a GovernanceBackendError,
and it is raised before any attempt exists, so the path would have recorded
no telemetry at all.

Also corrects the leak path count from three to five. QuotaGate.stats and
BreakerGate.retry_after_s are not wrapped by _record_quietly either.
2026-08-06 05:57:50 -04:00

65 lines
2.4 KiB
Python

"""限流准入封装: 后端故障必须报错而非放行(降级方向铁律)。
非降级设计说明: 缓存/遥测挂 → 静默;限流挂 → 若放行,多 worker 会同时
失去闸门直接击穿网关,故一律 GovernanceBackendError 上抛。
"""
from __future__ import annotations
from typing import TYPE_CHECKING
from polygateway.errors import GovernanceBackendError, SourceNotConfiguredError
if TYPE_CHECKING:
from polygateway.ports import Permit, RateLimiter
from polygateway.types import SourceConfig, SourceStats
class QuotaGate:
"""RetryMW 面向限流后端的唯一入口;包装一切后端异常。"""
def __init__(self, limiter: RateLimiter, *, scope: str) -> None:
self._limiter = limiter
# 后端故障即 scope 级不可用,异常须携 scope 供调用方定位(issue #7 §3.3)
self._scope = scope
async def try_acquire(self, source: SourceConfig) -> Permit | None:
try:
return await self._limiter.try_acquire(source.name, source.effective_est_tokens())
except (GovernanceBackendError, SourceNotConfiguredError):
raise
except Exception as exc:
raise GovernanceBackendError(
f"限流后端故障(try_acquire): {exc}", scope=self._scope
) from exc
async def stats(self, source: SourceConfig) -> SourceStats:
try:
return await self._limiter.source_stats(source.name)
except (GovernanceBackendError, SourceNotConfiguredError):
raise
except Exception as exc:
raise GovernanceBackendError(
f"限流后端故障(source_stats): {exc}", scope=self._scope
) from exc
async def mark_progress(self) -> None:
try:
await self._limiter.mark_progress()
except (GovernanceBackendError, SourceNotConfiguredError):
raise
except Exception as exc:
raise GovernanceBackendError(
f"限流后端故障(mark_progress): {exc}", scope=self._scope
) from exc
async def progress_age_s(self) -> float:
try:
return await self._limiter.progress_age_s()
except (GovernanceBackendError, SourceNotConfiguredError):
raise
except Exception as exc:
raise GovernanceBackendError(
f"限流后端故障(progress_age_s): {exc}", scope=self._scope
) from exc