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:
+25
-9
@@ -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")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user