fix: reparent governance backend failures under GatewayUnavailableError (issue #7)
A fail-closed limiter or breaker backend means the scope cannot emit a single request, which is exactly scope-level unavailability. But the error sat directly under PolyGatewayError, so a caller writing only `except GatewayUnavailableError` dropped it into the catch-all branch: Redis blips once and a backlog of tasks burns its business failure budget into the dead letter queue, over a fault a restart would clear. Three gate paths leak to callers rather than being absorbed by _record_quietly (try_acquire, try_enter, progress_age_s); each is now pinned by a test, since none of them had one before. The two unknown-source sites move to SourceNotConfiguredError instead of following along. They report a misconfigured source name, not an outage, and letting them into the retryable family would be the mirror of the bug being fixed here: the task would retry forever and never surface.
This commit is contained in:
@@ -16,7 +16,11 @@ import pytest
|
||||
from polygateway.backends.redis.breaker import RedisGate
|
||||
from polygateway.backends.redis.limiter import RedisLimiter
|
||||
from polygateway.client import GatewayClient
|
||||
from polygateway.errors import AllSourcesExhausted, GovernanceBackendError
|
||||
from polygateway.errors import (
|
||||
AllSourcesExhausted,
|
||||
GatewayUnavailableError,
|
||||
GovernanceBackendError,
|
||||
)
|
||||
from polygateway.sources import RoundRobinSelector
|
||||
from polygateway.types import (
|
||||
BackpressurePolicy,
|
||||
@@ -225,7 +229,11 @@ async def test_cancel_in_flight_releases_lease(clients):
|
||||
|
||||
|
||||
async def test_redis_down_admission_fails_closed():
|
||||
"""Redis 不可达 → 准入侧抛 GovernanceBackendError,绝不放行(库铁律)。"""
|
||||
"""Redis 不可达 → 准入侧报错绝不放行(库铁律),且以 scope 级形态到达调用方。
|
||||
|
||||
issue #7: 调用方只写 `except GatewayUnavailableError` 就该覆盖后端故障——
|
||||
真实 Redis 掉线是这条链路唯一的端到端证据,故断言收紧到 scope 级语义。
|
||||
"""
|
||||
import redis.asyncio as aioredis
|
||||
|
||||
dead = aioredis.from_url(
|
||||
@@ -240,9 +248,12 @@ async def test_redis_down_admission_fails_closed():
|
||||
lease_ttl_s=30.0,
|
||||
)
|
||||
gate = RedisGate(config=_CFG, redis=dead, scope="t-dead")
|
||||
with pytest.raises(GovernanceBackendError):
|
||||
await limiter.try_acquire("s1", 0)
|
||||
with pytest.raises(GovernanceBackendError):
|
||||
await gate.try_enter("s1", "w")
|
||||
for call in (limiter.try_acquire("s1", 0), gate.try_enter("s1", "w")):
|
||||
with pytest.raises(GatewayUnavailableError) as ei:
|
||||
await call
|
||||
assert isinstance(ei.value, GovernanceBackendError)
|
||||
assert ei.value.reason == "governance_backend_down"
|
||||
assert ei.value.scope == "t-dead"
|
||||
assert ei.value.retry_after_s > 0
|
||||
finally:
|
||||
await dead.aclose()
|
||||
|
||||
Reference in New Issue
Block a user