feat: suppress consecutive-channel opening on evidently healthy sources
Round 8 forensics caught the healthy source circuit-opened by five random empty completions (~20% ambient failure rate makes a 5-streak land every ~3000 attempts), blacking out the only good source for 60s. When the window holds min_calls samples below the failure-rate threshold, a streak is noise and no longer opens the gate; cold-start and low-traffic semantics are unchanged and sudden death of a warm source is still caught by the rate channel.
This commit is contained in:
@@ -290,3 +290,36 @@ class TestRetryAfter:
|
||||
gate = gate_factory(_CFG)
|
||||
await _open_gate(gate, "s1") # s1 开路;s2 健康
|
||||
assert await gate.retry_after_s(("s1", "s2")) == 0.0
|
||||
|
||||
|
||||
class TestConsecutiveSuppression:
|
||||
"""迭代 6: 窗口证据充足且健康时,连败是噪声,不开路(设计 §3.39)。"""
|
||||
|
||||
async def test_streak_suppressed_on_evidently_healthy_source(self, gate_factory):
|
||||
# 20 成功垫底(窗口样本充足、失败率低)后 3 连败(阈值 3)→ 不开路
|
||||
cfg = BreakerConfig(fail_threshold=3, cooldown_s=60.0, probe_ttl_s=120.0, min_calls=10)
|
||||
gate = gate_factory(cfg)
|
||||
for _ in range(20):
|
||||
entry = await gate.try_enter("s1", "w")
|
||||
await gate.record_success(entry)
|
||||
await _fail(gate, reason="timeout", n=3)
|
||||
assert (await gate.try_enter("s1", "w")).allowed
|
||||
|
||||
async def test_streak_fires_when_window_insufficient(self, gate_factory):
|
||||
# 冷启动(窗口样本不足)3 连败照常开路——连续通道本职保留
|
||||
cfg = BreakerConfig(fail_threshold=3, cooldown_s=60.0, probe_ttl_s=120.0, min_calls=10)
|
||||
gate = gate_factory(cfg)
|
||||
update = await _fail(gate, reason="timeout", n=3)
|
||||
assert update.state is GateState.OPEN
|
||||
|
||||
async def test_warm_source_going_dead_caught_by_rate(self, gate_factory):
|
||||
# 温热源猝死: 连败被抑制,但失败率窗口随失败累积必然接管
|
||||
cfg = BreakerConfig(
|
||||
fail_threshold=3, cooldown_s=60.0, probe_ttl_s=120.0, min_calls=4, fail_rate=0.6
|
||||
)
|
||||
gate = gate_factory(cfg)
|
||||
for _ in range(3):
|
||||
entry = await gate.try_enter("s1", "w")
|
||||
await gate.record_success(entry)
|
||||
update = await _fail(gate, reason="timeout", n=5) # 5/8 = 0.625 ≥ 0.6
|
||||
assert update.state is GateState.OPEN
|
||||
|
||||
Reference in New Issue
Block a user