fix: pin retry_after_s to the next certain retry moment
retry_after_s never had a written definition, so each backend improvised and they drifted apart. It now answers exactly one question: how long until a retry is *certainly* worth attempting. OPEN has such a moment (the cooldown deadline); HALF_OPEN does not, because the probe can come back at any time -- so it reports 0.0, which already means "retry now" elsewhere in the library. Six exits are brought in line. The half-open rejection is the one issue 14 reported: it returned the probe lease remainder, a deadlock-guard value derived from 2x the slowest timeout, so a 60s cooldown told callers to wait 600s. Worse, retry.py fed that number into the source cooldown memo, whose set_until only moves forward -- a source stayed skipped in-process for the whole lease even after its probe succeeded and the gate closed. That now writes an already-expired deadline, so the memo goes back to recording only real OPEN cooldowns. The other five were pre-existing memory/redis divergences hidden by a contract-test blind spot (the suite pinned that a second caller gets rejected, never what number it got): redis reported the probe TTL on grant and the lease remainder on fenced-out writes, where memory has always reported 0. Contract cases now pin all four half-open exits on both backends, with 1:1 real-wait variants for redis since the fake-clock ones skip there.
This commit is contained in:
@@ -327,3 +327,49 @@ async def test_variant_probe_rate_limited_releases_not_hangs(redis_client):
|
||||
assert update.applied
|
||||
nxt = await gate.try_enter("s1", "w2")
|
||||
assert nxt.allowed and nxt.is_probe # 立即可再探,不等 probe_ttl
|
||||
|
||||
|
||||
# —— issue #14: retry_after_s = 距离**确定**可再试的时刻,HALF_OPEN 无确定时刻 ——
|
||||
|
||||
|
||||
@pytestmark_slow
|
||||
async def test_variant_half_open_rejection_reports_no_certain_wait(redis_client):
|
||||
gate = _gate(redis_client)
|
||||
await _open_gate(gate)
|
||||
await asyncio.sleep(_CFG.cooldown_s + 1)
|
||||
probe = await gate.try_enter("s1", "w1")
|
||||
assert probe.is_probe
|
||||
blocked = await gate.try_enter("s1", "w2")
|
||||
assert not blocked.allowed and blocked.state is GateState.HALF_OPEN
|
||||
assert blocked.retry_after_s == 0.0
|
||||
|
||||
|
||||
@pytestmark_slow
|
||||
async def test_variant_probe_grant_reports_no_certain_wait(redis_client):
|
||||
gate = _gate(redis_client)
|
||||
await _open_gate(gate)
|
||||
await asyncio.sleep(_CFG.cooldown_s + 1)
|
||||
probe = await gate.try_enter("s1", "w1")
|
||||
assert probe.allowed and probe.is_probe
|
||||
assert probe.retry_after_s == 0.0
|
||||
|
||||
|
||||
@pytestmark_slow
|
||||
async def test_variant_retry_after_zero_while_probe_in_flight(redis_client):
|
||||
gate = _gate(redis_client)
|
||||
await _open_gate(gate)
|
||||
await asyncio.sleep(_CFG.cooldown_s + 1)
|
||||
assert (await gate.try_enter("s1", "w1")).is_probe
|
||||
assert await gate.retry_after_s(("s1",)) == 0.0
|
||||
|
||||
|
||||
@pytestmark_slow
|
||||
async def test_variant_fenced_write_in_half_open_reports_no_certain_wait(redis_client):
|
||||
gate = _gate(redis_client)
|
||||
stale = await gate.try_enter("s1", "slow-worker") # epoch 0 的旧 entry
|
||||
await _open_gate(gate) # 他人开路,epoch 推进
|
||||
await asyncio.sleep(_CFG.cooldown_s + 1)
|
||||
assert (await gate.try_enter("s1", "w1")).is_probe # 门此刻 HALF_OPEN
|
||||
update = await gate.record_success(stale)
|
||||
assert not update.applied and update.state is GateState.HALF_OPEN
|
||||
assert update.retry_after_s == 0.0
|
||||
|
||||
Reference in New Issue
Block a user