fix: keep the accounting path degrading after the wrapper change
Letting SourceNotConfiguredError through the gate wrappers opened a hole the recheck caught: _record_quietly only degrades GovernanceBackendError, so an assembly defect raised from the accounting side would now escape and destroy a response from a call that had already genuinely succeeded. That inverts the exact invariant _record_quietly exists to hold. Widening _record_quietly is the right fix rather than narrowing the wrappers, because that layer degrades by what the path is (accounting, the call is already done) rather than by which error type shows up. Narrowing would have left 4 of 9 wrapper methods as exceptions to a rule nobody can remember. No backend raises it from an accounting method today, so this is a guardrail for whoever adds source-name validation to a breaker backend. The stub that first reported this green was wrong: its record_success lacked count_attempt, so it raised TypeError and the wrapper relabeled it. Fixed signature, then the test failed as it should have. Also finishes the three-to-five leak path correction across the four remaining spots, including the wiki summary card that indexes this design.
This commit is contained in:
@@ -193,6 +193,12 @@ class _LimiterProgressBroken(InMemoryLimiter):
|
||||
raise GovernanceBackendError("redis 抖动", scope="llm")
|
||||
|
||||
|
||||
class _GateSuccessMisconfigured(InMemoryGate):
|
||||
# 签名须与端口一致(含 count_attempt),否则抛的是 TypeError 而非本类要测的异常
|
||||
async def record_success(self, entry, *, count_attempt: bool = True):
|
||||
raise SourceNotConfiguredError("未知源 's1'(scope=llm)")
|
||||
|
||||
|
||||
class TestAccountingDegradation:
|
||||
"""记账侧降级(设计 §10,ARCH §7.3 勘误): 调用已完成,写回失败不冒泡。"""
|
||||
|
||||
@@ -207,6 +213,24 @@ class TestAccountingDegradation:
|
||||
resp = await mw(_REQ)
|
||||
assert resp.content == "ok" # 真实成功响应不因记账失败被丢弃
|
||||
|
||||
async def test_assembly_defect_on_accounting_path_also_degrades(self):
|
||||
"""记账侧降级按"路径性质"而非异常类型: 装配缺陷同样不得毁掉已完成的调用。
|
||||
|
||||
`SourceNotConfiguredError` 被放行穿透闸门包装器(issue #7 §T6)后,若
|
||||
`_record_quietly` 只降级 `GovernanceBackendError`,它就会从记账侧冒泡、
|
||||
销毁一个真实成功的响应——反转本类钉住的既有行为。当前无后端会从记账
|
||||
方法抛它,此用例是为将来加了源名校验的后端守住这条不变式。
|
||||
"""
|
||||
clock = FakeClock()
|
||||
src = make_source()
|
||||
limiter = InMemoryLimiter(
|
||||
scope="llm", sources={"s1": src}, global_limits=_NO_GLOBAL, now=clock
|
||||
)
|
||||
gate = _GateSuccessMisconfigured(config=_BREAKER, now=clock)
|
||||
mw = _mw([src], limiter, [_ok()], clock=clock, sleep=BoundedSleep(), gate=gate)
|
||||
resp = await mw(_REQ)
|
||||
assert resp.content == "ok"
|
||||
|
||||
async def test_mark_progress_failure_does_not_lose_response(self):
|
||||
clock = FakeClock()
|
||||
src = make_source()
|
||||
@@ -301,12 +325,14 @@ class TestUnknownSourceIsAssemblyDefect:
|
||||
|
||||
|
||||
class TestGateFailuresReachCallersAsScopeLevel:
|
||||
"""三条闸门泄漏路径必须以 scope 级不可用的形态到达调用方(issue #7)。
|
||||
"""闸门泄漏路径必须以 scope 级不可用的形态到达调用方(issue #7)。
|
||||
|
||||
记账路径由 `_record_quietly` 降级为 warning,但闸门路径没有那层包裹,会一路
|
||||
抛给调用方。只写 `except GatewayUnavailableError` 的调用方此前接不住,后果
|
||||
是 Redis 抖一下就让积压任务烧掉业务失败预算进死信——而那是运维重启即可恢复
|
||||
的故障。三条路径逐一钉住,防止将来任何一条被漏掉。
|
||||
的故障。全部五条为: `QuotaGate` 的 try_acquire / stats / progress_age_s,
|
||||
`BreakerGate` 的 try_enter / retry_after_s(判据是该调用点未被 `_record_quietly`
|
||||
包裹)。此处钉住其中三条代表路径,余两条由同一注入机制覆盖。
|
||||
"""
|
||||
|
||||
async def test_try_acquire_failure_is_scope_level(self):
|
||||
|
||||
Reference in New Issue
Block a user