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.
This commit is contained in:
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from polygateway.errors import GovernanceBackendError
|
||||
from polygateway.errors import GovernanceBackendError, SourceNotConfiguredError
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from polygateway.ports import GateDecision, GateUpdate, ProviderGate
|
||||
@@ -22,43 +22,53 @@ class BreakerGate:
|
||||
async def try_enter(self, source: SourceConfig, owner: str) -> GateDecision:
|
||||
try:
|
||||
return await self._gate.try_enter(source.name, owner)
|
||||
except GovernanceBackendError:
|
||||
except (GovernanceBackendError, SourceNotConfiguredError):
|
||||
raise
|
||||
except Exception as exc:
|
||||
raise GovernanceBackendError(f"熔断后端故障(try_enter): {exc}", scope=self._scope) from exc
|
||||
raise GovernanceBackendError(
|
||||
f"熔断后端故障(try_enter): {exc}", scope=self._scope
|
||||
) from exc
|
||||
|
||||
async def record_success(
|
||||
self, entry: GateDecision, *, count_attempt: bool = True
|
||||
) -> GateUpdate:
|
||||
try:
|
||||
return await self._gate.record_success(entry, count_attempt=count_attempt)
|
||||
except GovernanceBackendError:
|
||||
except (GovernanceBackendError, SourceNotConfiguredError):
|
||||
raise
|
||||
except Exception as exc:
|
||||
raise GovernanceBackendError(f"熔断后端故障(record_success): {exc}", scope=self._scope) from exc
|
||||
raise GovernanceBackendError(
|
||||
f"熔断后端故障(record_success): {exc}", scope=self._scope
|
||||
) from exc
|
||||
|
||||
async def record_failure(
|
||||
self, entry: GateDecision, reason: str, force_open: bool
|
||||
) -> GateUpdate:
|
||||
try:
|
||||
return await self._gate.record_failure(entry, reason, force_open)
|
||||
except GovernanceBackendError:
|
||||
except (GovernanceBackendError, SourceNotConfiguredError):
|
||||
raise
|
||||
except Exception as exc:
|
||||
raise GovernanceBackendError(f"熔断后端故障(record_failure): {exc}", scope=self._scope) from exc
|
||||
raise GovernanceBackendError(
|
||||
f"熔断后端故障(record_failure): {exc}", scope=self._scope
|
||||
) from exc
|
||||
|
||||
async def release_probe(self, entry: GateDecision) -> GateUpdate:
|
||||
try:
|
||||
return await self._gate.release_probe(entry)
|
||||
except GovernanceBackendError:
|
||||
except (GovernanceBackendError, SourceNotConfiguredError):
|
||||
raise
|
||||
except Exception as exc:
|
||||
raise GovernanceBackendError(f"熔断后端故障(release_probe): {exc}", scope=self._scope) from exc
|
||||
raise GovernanceBackendError(
|
||||
f"熔断后端故障(release_probe): {exc}", scope=self._scope
|
||||
) from exc
|
||||
|
||||
async def retry_after_s(self, sources: tuple[str, ...]) -> float:
|
||||
try:
|
||||
return await self._gate.retry_after_s(sources)
|
||||
except GovernanceBackendError:
|
||||
except (GovernanceBackendError, SourceNotConfiguredError):
|
||||
raise
|
||||
except Exception as exc:
|
||||
raise GovernanceBackendError(f"熔断后端故障(retry_after_s): {exc}", scope=self._scope) from exc
|
||||
raise GovernanceBackendError(
|
||||
f"熔断后端故障(retry_after_s): {exc}", scope=self._scope
|
||||
) from exc
|
||||
|
||||
@@ -8,7 +8,7 @@ from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from polygateway.errors import GovernanceBackendError
|
||||
from polygateway.errors import GovernanceBackendError, SourceNotConfiguredError
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from polygateway.ports import Permit, RateLimiter
|
||||
@@ -26,31 +26,39 @@ class QuotaGate:
|
||||
async def try_acquire(self, source: SourceConfig) -> Permit | None:
|
||||
try:
|
||||
return await self._limiter.try_acquire(source.name, source.effective_est_tokens())
|
||||
except GovernanceBackendError:
|
||||
except (GovernanceBackendError, SourceNotConfiguredError):
|
||||
raise
|
||||
except Exception as exc:
|
||||
raise GovernanceBackendError(f"限流后端故障(try_acquire): {exc}", scope=self._scope) from 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:
|
||||
except (GovernanceBackendError, SourceNotConfiguredError):
|
||||
raise
|
||||
except Exception as exc:
|
||||
raise GovernanceBackendError(f"限流后端故障(source_stats): {exc}", scope=self._scope) from 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:
|
||||
except (GovernanceBackendError, SourceNotConfiguredError):
|
||||
raise
|
||||
except Exception as exc:
|
||||
raise GovernanceBackendError(f"限流后端故障(mark_progress): {exc}", scope=self._scope) from 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:
|
||||
except (GovernanceBackendError, SourceNotConfiguredError):
|
||||
raise
|
||||
except Exception as exc:
|
||||
raise GovernanceBackendError(f"限流后端故障(progress_age_s): {exc}", scope=self._scope) from exc
|
||||
raise GovernanceBackendError(
|
||||
f"限流后端故障(progress_age_s): {exc}", scope=self._scope
|
||||
) from exc
|
||||
|
||||
@@ -17,7 +17,11 @@ from typing import TYPE_CHECKING
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from polygateway.errors import GatewayUnavailableError, GovernanceBackendError
|
||||
from polygateway.errors import (
|
||||
GatewayUnavailableError,
|
||||
GovernanceBackendError,
|
||||
SourceNotConfiguredError,
|
||||
)
|
||||
from polygateway.middleware.cache import digest_messages
|
||||
from polygateway.types import canonical_sampling_json, merge_sampling
|
||||
|
||||
@@ -247,7 +251,7 @@ class TelemetryMW:
|
||||
started = self._now()
|
||||
try:
|
||||
response = await call_next(request)
|
||||
except (GatewayUnavailableError, GovernanceBackendError) as exc:
|
||||
except (GatewayUnavailableError, GovernanceBackendError, SourceNotConfiguredError) as exc:
|
||||
await self._emitter.emit_terminal_failure(
|
||||
request=request,
|
||||
call_id=str(uuid.uuid4()),
|
||||
|
||||
Reference in New Issue
Block a user