feat: add AIMD adaptive concurrency pacing per source
P6 round 2 showed routing convergence turns account-level 429s into the binding constraint (62% throttle rate at full concurrency). Each source now carries a local AIMD limit: multiplicative cut on 429, additive growth on success. Over-limit picks queue via the existing quota-wait poll instead of burning retry budget or tripping the circuit-open verdict.
This commit is contained in:
@@ -98,6 +98,7 @@ def _harness(
|
||||
global_limits=_NO_GLOBAL,
|
||||
rng=lambda: 0.0,
|
||||
selector=None,
|
||||
pacer=None,
|
||||
):
|
||||
clock = clock or FakeClock()
|
||||
limiter = InMemoryLimiter(
|
||||
@@ -125,6 +126,7 @@ def _harness(
|
||||
now=clock,
|
||||
sleep=sleep,
|
||||
rng=rng,
|
||||
pacer=pacer,
|
||||
)
|
||||
return mw, limiter, gate, transport, sleep, clock
|
||||
|
||||
@@ -443,3 +445,49 @@ class TestM25Orchestration:
|
||||
await mw(_REQ)
|
||||
g = gate._gates["a"]
|
||||
assert g.a0 + g.a1 == 0
|
||||
|
||||
|
||||
class TestAdaptivePacing:
|
||||
"""AIMD 接线(设计 §3.35): 429 收紧准入,paced 源等待而非烧预算。"""
|
||||
|
||||
async def test_paced_source_waits_without_consuming_budget(self):
|
||||
from polygateway.sources import AdaptivePacer
|
||||
|
||||
pacer = AdaptivePacer(ceiling=32.0)
|
||||
for _ in range(200):
|
||||
pacer.on_backpressure("a") # limit → 1
|
||||
pacer.enter("a") # 模拟一个在途占满名额
|
||||
mw, _, _, transport, _, _ = _harness(
|
||||
[_src("a")], [_ok()], quota_full="fail_fast", pacer=pacer
|
||||
)
|
||||
with pytest.raises(AllSourcesExhausted) as ei:
|
||||
await mw(_REQ)
|
||||
assert ei.value.reason == "quota_exhausted" # 走配额等待通道,非 CircuitOpen
|
||||
assert ei.value.per_source_reasons.get("a") == "adaptive_paced"
|
||||
assert transport.calls == [] # 未发起尝试 → 不烧重试预算
|
||||
|
||||
async def test_429_cuts_limit_success_grows_it(self):
|
||||
from polygateway.sources import AdaptivePacer
|
||||
|
||||
pacer = AdaptivePacer(ceiling=32.0)
|
||||
mw, _, _, _, _, _ = _harness(
|
||||
[_src("a")],
|
||||
[TransientError("throttled", status_code=429), _ok()],
|
||||
pacer=pacer,
|
||||
)
|
||||
await mw(_REQ)
|
||||
# 429 削减一次(8→5.6),随后成功加性增长(5.6 + 1/5.6)
|
||||
assert pacer.limit("a") == pytest.approx(8.0 * 0.7 + 1.0 / (8.0 * 0.7))
|
||||
|
||||
async def test_inflight_returns_to_zero_after_call(self):
|
||||
from polygateway.sources import AdaptivePacer
|
||||
|
||||
pacer = AdaptivePacer(ceiling=32.0)
|
||||
mw, _, _, _, _, _ = _harness(
|
||||
[_src("a"), _src("b")],
|
||||
[TransientError("x"), _ok()],
|
||||
pacer=pacer,
|
||||
)
|
||||
await mw(_REQ)
|
||||
assert pacer._inflight.get("a", 0) == 0
|
||||
assert pacer._inflight.get("b", 0) == 0
|
||||
|
||||
Reference in New Issue
Block a user