diff --git a/volc_limit_sync.py b/volc_limit_sync.py index 6ea912d..f4e74e2 100644 --- a/volc_limit_sync.py +++ b/volc_limit_sync.py @@ -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")