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:
2026-08-06 04:53:52 -04:00
parent dd540496a1
commit 45073486a7
13 changed files with 179 additions and 49 deletions
+2 -2
View File
@@ -15,7 +15,7 @@ import time
import uuid
from typing import TYPE_CHECKING
from polygateway.errors import GovernanceBackendError
from polygateway.errors import SourceNotConfiguredError
from polygateway.types import GlobalLimits, SourceConfig, SourceStats
if TYPE_CHECKING:
@@ -89,7 +89,7 @@ class InMemoryLimiter:
def _cfg(self, source_key: str) -> SourceConfig:
cfg = self._sources.get(source_key)
if cfg is None:
raise GovernanceBackendError(f"未知源 {source_key!r}(scope={self._scope})")
raise SourceNotConfiguredError(f"未知源 {source_key!r}(scope={self._scope})")
return cfg
def _window(self) -> int: