fix: settle cancelled attempts against the source estimate
取消发生在"端口已开始、结算尚未确定"时,原先按 settle(0) 把入场预扣整笔 退还,等于把可能已被上游计费的用量退回闸里;启用调用期限后库自身会常规性 触发该路径,故先修记账再启用。 改动只落在取消路径的取值上(设计 §6.3 矩阵 S3/S5/S7): - actual 初值保持 0,另设函数内局部阶段变量 settlement_known(不进任何签名); - 成功路径算出用量后置位,真实 usage 恰为 0 同样算"已知",不被取消覆写; - 已处理领域失败分支把结算决定前移到其第一个 await 之前(同级 except CancelledError 接不住本块 await 上的取消,它直穿 finally), 故 SourceDead 的既有 0 在取消下被保住,瞬时失败仍是 est,值与 1.3.5 逐字相同; - 未被四分类接住的异常不经上述分支,仍按 0 退全款(S8,本版不扩大语义); - OCR 的 0 token 是事实而非未知,settle(0) 不变,只补注释。 取消窗口一律用真实 asyncio.Event 钉死(不再 sleep 撞窗口),并加防越界回归: RuntimeError 逃逸仍结 0、真实 usage 为 0 的成功仍结 0。
This commit is contained in:
@@ -72,10 +72,13 @@ class ScriptedTransport:
|
||||
def __init__(self, hang: bool = False):
|
||||
self.hang = hang
|
||||
self.calls: list[str] = []
|
||||
# 取消用例的确定性窗口(同 test_retry FakeTransport): 进入挂起即置位
|
||||
self.entered = asyncio.Event()
|
||||
|
||||
async def complete(self, *, messages, source, stream, overlay, call_id, reasoning_effort):
|
||||
self.calls.append(source.name)
|
||||
if self.hang:
|
||||
self.entered.set()
|
||||
await asyncio.Event().wait()
|
||||
return TransportResult(
|
||||
content="ok",
|
||||
@@ -225,7 +228,32 @@ async def test_cancel_in_flight_releases_lease(clients):
|
||||
assert (await limiter.source_stats("s1")).inflight == 0
|
||||
|
||||
|
||||
# —— 掉线方向(fail-closed 集成证据)——
|
||||
async def test_cancel_in_flight_keeps_the_reservation(clients):
|
||||
"""1.3.6 §6.3 S3 在**真实 Redis** 上: 端口已开始、用量未知 → 保留 est 预扣。
|
||||
|
||||
内存后端与 Lua 后端的 `settle(delta)` 算术必须同口径——取消时凭空退款
|
||||
在分布式部署下就是几个 worker 一起击穿 TPM 闸。本用例不改 Lua、不改契约套件。
|
||||
"""
|
||||
a_cli, _ = clients
|
||||
scope = f"t{uuid4().hex[:8]}"
|
||||
sources = [make_source(max_concurrency=1, tpm=1000, est_tokens=400)]
|
||||
limiter = _limiter(a_cli, scope, sources, GlobalLimits(0, 0, 0))
|
||||
transport = ScriptedTransport(hang=True)
|
||||
client = _client(
|
||||
scope,
|
||||
sources,
|
||||
limiter,
|
||||
RedisGate(config=_CFG, redis=a_cli, scope=scope),
|
||||
transport,
|
||||
)
|
||||
task = asyncio.create_task(client.chat([{"role": "user", "content": "hi"}]))
|
||||
await transport.entered.wait()
|
||||
task.cancel()
|
||||
with pytest.raises(asyncio.CancelledError):
|
||||
await task
|
||||
stats = await limiter.source_stats("s1")
|
||||
assert stats.tpm_used == 400 # 预扣保留, 不回退到 0
|
||||
assert stats.inflight == 0
|
||||
|
||||
|
||||
async def test_redis_down_admission_fails_closed():
|
||||
|
||||
Reference in New Issue
Block a user