fix: _release_inflight 改用 Lua 原子释放,floor 0 防幽灵误删

TTL 过期后迟到结算的 DECRBY 会把 inflight 打到负数,旧代码 DEL 整个
key → 误删别的请求的预留。改用 RELEASE_SCRIPT 在 Redis 内原子
DECRBY + floor 0:结果 <=0 则 DEL,否则 SET new_val + EXPIRE 保留 TTL。

_release_inflight eval 补第三个参数 PREBLOCK_INFLIGHT_TTL,与
RELEASE_SCRIPT 的 ARGV[2] 对应。

验证:新增场景6(幽灵释放),6 场景 ALL PASS。
This commit is contained in:
vibe-code
2026-08-18 10:42:21 +00:00
parent 2502a6b507
commit 69d144ba34
+25 -9
View File
@@ -138,6 +138,24 @@ redis.call('EXPIRE', KEYS[2], tonumber(ARGV[3]))
return 1
"""
# Lua 原子释放:结算时配对扣减 inflight,floor 0 防止迟到结算的幽灵释放
# 误删别的请求的预留(TTL 过期后迟到 DECRBY 导致负数 → DEL 全部)。
# SET 后补 EXPIRE,保留剩余预留的 TTL(不因 SET 重置为永久)。
# KEYS[1]=inflight_key, ARGV[1]=reserve, ARGV[2]=inflight TTL
# 返回: 扣减后剩余 inflight 值(>=0)
RELEASE_SCRIPT = """
local cur = tonumber(redis.call('GET', KEYS[1]) or '0')
local reserve = tonumber(ARGV[1])
local new_val = cur - reserve
if new_val <= 0 then
redis.call('DEL', KEYS[1])
return 0
end
redis.call('SET', KEYS[1], new_val)
redis.call('EXPIRE', KEYS[1], tonumber(ARGV[2]))
return new_val
"""
# stable_limit 缺失时(首次同步前/新增模型/TTL 过期/Redis 数据丢失)的保守兜底限额。
# 免费池常态 ~2M(首开赠送/昨日采多少返多少),若 fallback 裸奔到采集上限 2M,
# 余额不足或已耗尽的模型会被烧穿扣费。900k 对 0 余额模型仍是 9 倍优惠,
@@ -927,20 +945,18 @@ def _atomic_reserve(clean_model, api_base, effective_limit, reserve):
def _release_inflight(clean_model, api_base, reserve):
"""结算配对释放:请求完成后把 pre-hook 预扣的 reserve 从 inflight 账户扣除。
reserve<=0 或 inflight 已过期(TTL 兜底释放)时无操作。失败/丢失的预留
由 TTL 自动归零,这里只做正常结算路径的精确回收。
用 Lua 原子脚本做 DECRBY + floor 0,防止 TTL 过期后迟到结算的幽灵释放
把 DECRBY 打到负数 → 误删别的请求的预留。reserve<=0 或脚本失败时无操作,
inflight 残留由 TTL 兜底归零。
"""
if not redis_client or not reserve:
return
try:
inflight_key = get_inflight_key(clean_model, api_base)
cur = float(redis_client.get(inflight_key) or 0)
if cur <= 0:
return
# DECRBY 到负数说明 inflight 已部分过期,直接 DEL 归零即可
new_val = redis_client.decrby(inflight_key, int(reserve))
if int(new_val) <= 0:
redis_client.delete(inflight_key)
redis_client.eval(
RELEASE_SCRIPT, 1, inflight_key, int(reserve),
int(PREBLOCK_INFLIGHT_TTL),
)
except Exception as e:
log(f"❌ _release_inflight failed: {e}", level="ERROR")