docs: correct how a wait-mode call actually dies on a dead source

Branch review caught the docs claiming something the code does not do.
CHANGELOG, README and the design's behaviour matrix all said a
force-opened source under circuit_open=wait waits out the full stall
window. It does not: the probe let through after each cooldown is a
real attempt, so it burns a max_attempts slot like any other, and a
401 source usually runs out of retry budget first -- reason is
retry_exhausted, not stalled. Which budget wins depends on
max_attempts against the cooldowns and the stall window.

The behaviour is right; only the prose was wrong. Charging the probe
to the retry budget is exactly the split issue #8 settled: the
question is who spends max_attempts, and a probe does send a real
request. A test now pins it so the claim cannot drift again.

Also drops the planned "woke up" log line. Each wait round already
logs on entry with its duration, and a still-blocked wake-up logs the
next round immediately, so a second line would only double the volume.
This commit is contained in:
2026-08-20 01:00:47 -04:00
parent 5a025b6e5d
commit c5b2b3fade
5 changed files with 45 additions and 8 deletions
+34
View File
@@ -16,6 +16,7 @@ from polygateway.errors import (
CircuitOpenError,
GatewayUnavailableError,
GovernanceBackendError,
SourceDeadError,
SourceNotConfiguredError,
TransientError,
)
@@ -742,6 +743,39 @@ class TestCircuitOpenPolicy:
with pytest.raises(asyncio.CancelledError):
await task
async def test_wait_does_not_exempt_probes_from_the_retry_budget(self):
"""wait 档不豁免重试预算: 探针是**真实尝试**,失败照样烧 max_attempts。
故 force_open 的源(401/403/欠费一击即熔,不看任何阈值)在 wait 档下并
**不是**"等满 stall 窗口才死"——两个预算哪个先耗尽就以哪个的 reason
失败。这里 max_attempts=3 而冷却只累计 120s < stall_window=300s,故
先到的是重试预算。参数换成"冷却累计超过 stall 预算"则先到 stalled
(见 test_wait_still_dies_when_cooldown_outlasts_the_stall_budget)。
这与 issue #8 确立的划分一致: 划分依据是"谁消耗重试预算",探针发出了
真实请求,理应记在重试预算上而不是 stall 账上。
"""
clock = FakeClock()
src = make_source()
sleep = BoundedSleep()
async def advance(_n):
clock.advance(sleep.delays[-1])
sleep._side_effect = advance
mw = _mw(
[src],
self._free_limiter(clock, src),
[SourceDeadError("401"), SourceDeadError("401"), SourceDeadError("401")],
clock=clock,
sleep=sleep,
circuit_open="wait",
)
with pytest.raises(AllSourcesExhausted) as ei:
await mw(_REQ)
assert ei.value.reason == "retry_exhausted"
assert clock.t - 1000.0 < _STALL # 远未等满 stall 窗口
async def test_half_open_rejection_does_not_blacklist_a_recovered_source(self):
"""issue #14 §1.3 回归: 探针成功后本进程立即可再选该源。