Files
junhong_cmp_fiber/internal/polling/carrierqueue/sr_scripts.go
邹进 934a3a27a1
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
feat: 停复机指令队列与设备维度判断
自动停复机统一经停复机指令队列执行(全局开关 polling.stop_resume.queue_enabled,默认关闭):
- 统一判断函数 decideCard,新旧路径共用;暂停轮询的卡在套餐到期、退款场景照常停机
- 同卡一条指令、版本号覆盖;领取校验单卡最小重试间隔;先于卡任务领取,不受 scheduler_mode 限制
- 执行器只调用一次网关,按错误分类退避与告警,结果未知在队列内确认
- 设备维度判断合并请求与场景,版本变化再判断,旧路径持设备锁时推后
- 套餐激活、重置、订单、流量扣减、套餐到期、退款、实名管控修改改为请求评估
- 监控增加停复机队列与近 1 小时执行计数,单卡轮询状态返回当前指令
- 迁移 000255 插入开关与告警、退避参数

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 00:36:26 +09:00

226 lines
9.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package carrierqueue
import "github.com/redis/go-redis/v9"
// 停复机指令 Hash 字段(见 constants.RedisCardStopResumeCmdKey)。
const (
srFieldAction = "action" // stop / start
srFieldReason = "reason" // 停机原因(复机为空)
srFieldScenes = "scenes" // 触发场景(逗号分隔)
srFieldSource = "source" // 请求来源
srFieldCarrier = "carrier" // 所属运营商账户
srFieldVersion = "ver" // 指令版本,每次写入 HINCRBY
srFieldFail = "fail" // 连续失败次数
srFieldFirst = "ffail" // 首次失败(毫秒)
srFieldError = "err" // 最近错误(类别|信息)
srFieldPending = "pending" // 1 表示结果未知、待确认
srFieldUnknown = "unknown" // 连续结果未知次数
srFieldAt = "at" // 最近写入时间(毫秒)
)
// srWriteLua 写入或覆盖停复机指令:方向相反时清零失败与待确认,递增版本,放入账户停复机队列。
// 指令执行中(存在处理中标记)时只更新指令内容与版本,由收尾按版本保留;待确认中且方向不变时保持确认时间。
//
// KEYS[1] 指令 Hash;KEYS[2] 账户停复机队列;KEYS[3] 处理中标记。
// ARGV:action、reason、scenes、source、carrier、now_ms、card_id。返回新版本号。
const srWriteLua = `
local prev = redis.call('HGET', KEYS[1], 'action')
if prev and prev ~= ARGV[1] then
redis.call('HDEL', KEYS[1], 'fail', 'ffail', 'err', 'pending', 'unknown')
end
redis.call('HSET', KEYS[1], 'action', ARGV[1], 'reason', ARGV[2], 'scenes', ARGV[3], 'source', ARGV[4], 'carrier', ARGV[5], 'at', ARGV[6])
local ver = redis.call('HINCRBY', KEYS[1], 'ver', 1)
if redis.call('EXISTS', KEYS[3]) == 1 then return ver end
if redis.call('HGET', KEYS[1], 'pending') == '1' then
redis.call('ZADD', KEYS[2], 'NX', tonumber(ARGV[6]), ARGV[7])
else
redis.call('ZADD', KEYS[2], 'LT', tonumber(ARGV[6]), ARGV[7])
end
return ver
`
// srClaimLua 领取账户到期停复机指令:先清理过期执行中,按容量领取;未到单卡最小重试间隔的推后到可调用时间。
// 待确认的指令只查状态,不受最小间隔约束。指令已不存在的成员直接移出队列。
//
// KEYS[1] 账户停复机队列;KEYS[2] 账户停复机执行中计数。
// ARGV:now_ms、lease_ms、容量(执行上限与剩余 QPS 的较小值)、token 前缀、单卡最小间隔 ms。
// 返回 {过期数, card_id, token, ver, action, pending, ...}。
const srClaimLua = `
local now = tonumber(ARGV[1])
local lease = tonumber(ARGV[2])
local limit = tonumber(ARGV[3])
local prefix = ARGV[4]
local minInterval = tonumber(ARGV[5])
local expired = redis.call('ZREMRANGEBYSCORE', KEYS[2], '-inf', now)
local result = {expired}
local capacity = limit - redis.call('ZCARD', KEYS[2])
if capacity <= 0 then return result end
local ids = redis.call('ZRANGEBYSCORE', KEYS[1], '-inf', now, 'LIMIT', 0, capacity * 2 + 10)
local taken = 0
for _, id in ipairs(ids) do
if taken >= capacity then break end
local cmdKey = 'polling:card:' .. id .. ':stop_resume_cmd'
local fields = redis.call('HMGET', cmdKey, 'action', 'ver', 'pending')
if not fields[1] then
redis.call('ZREM', KEYS[1], id)
else
local pending = fields[3] == '1'
local blockedUntil = 0
if not pending then
local last = tonumber(redis.call('GET', 'polling:card:' .. id .. ':sr_last_call') or '0')
if last > 0 and last + minInterval > now then blockedUntil = last + minInterval end
end
if blockedUntil > 0 then
redis.call('ZADD', KEYS[1], 'XX', blockedUntil, id)
else
local claimKey = 'polling:card:' .. id .. ':sr_claim'
local token = prefix .. ':' .. id
if redis.call('SET', claimKey, token, 'NX', 'PX', lease) then
redis.call('ZADD', KEYS[1], 'XX', now + lease, id)
redis.call('ZADD', KEYS[2], now + lease, token)
taken = taken + 1
table.insert(result, id)
table.insert(result, token)
table.insert(result, fields[2] or '0')
table.insert(result, fields[1])
table.insert(result, pending and '1' or '0')
else
local ttl = redis.call('PTTL', claimKey)
if ttl < 0 then ttl = lease end
redis.call('ZADD', KEYS[1], 'XX', now + ttl, id)
end
end
end
end
return result
`
// srFinishLua 停复机指令收尾:token 不一致返回 0 不做任何修改;版本已变大(执行期间写入了新指令)时保留新指令、立即可领取(返回 2);
// 否则 done=1 删除指令并移出队列,done=0 写入字段并按 next_ms 重新排队(返回 1)。
//
// KEYS[1] 指令 Hash;KEYS[2] 处理中标记;KEYS[3] 执行权;KEYS[4] 执行中计数;KEYS[5] 账户停复机队列。
// ARGV:token、card_id、领取时版本、done(1/0)、next_ms、now_ms、set 字段对数 n、字段1、值1、…、删除字段…。
const srFinishLua = `
if redis.call('GET', KEYS[2]) ~= ARGV[1] then return 0 end
redis.call('DEL', KEYS[2])
redis.call('DEL', KEYS[3])
redis.call('ZREM', KEYS[4], ARGV[1])
local cardId = ARGV[2]
if (redis.call('HGET', KEYS[1], 'ver') or '0') ~= ARGV[3] then
if redis.call('EXISTS', KEYS[1]) == 1 then
redis.call('ZADD', KEYS[5], tonumber(ARGV[6]), cardId)
else
redis.call('ZREM', KEYS[5], cardId)
end
return 2
end
if ARGV[4] == '1' then
redis.call('DEL', KEYS[1])
redis.call('ZREM', KEYS[5], cardId)
return 1
end
local n = tonumber(ARGV[7])
local idx = 8
for i = 1, n do
redis.call('HSET', KEYS[1], ARGV[idx], ARGV[idx + 1])
idx = idx + 2
end
while idx <= #ARGV do
redis.call('HDEL', KEYS[1], ARGV[idx])
idx = idx + 1
end
redis.call('ZADD', KEYS[5], tonumber(ARGV[5]), cardId)
return 1
`
// srReleaseLua 发布失败:按 token 删除处理中标记、移出执行中计数,score 恢复为当前时间。
// KEYS[1] 处理中标记;KEYS[2] 执行中计数;KEYS[3] 队列。ARGV:token、member、now_ms。
const srReleaseLua = `
if redis.call('GET', KEYS[1]) ~= ARGV[1] then return 0 end
redis.call('DEL', KEYS[1])
redis.call('ZREM', KEYS[2], ARGV[1])
redis.call('ZADD', KEYS[3], 'XX', tonumber(ARGV[3]), ARGV[2])
return 1
`
// deviceEvalRequestLua 请求设备判断:递增版本、合并场景、设备进入判断队列(同一设备一个成员,ZADD LT 不推后已有更早时间)。
// KEYS[1] 版本;KEYS[2] 场景集合;KEYS[3] 设备判断队列;KEYS[4] 处理中标记。ARGV:device_id、now_ms、场景…。
const deviceEvalRequestLua = `
redis.call('INCR', KEYS[1])
for i = 3, #ARGV do redis.call('SADD', KEYS[2], ARGV[i]) end
if redis.call('EXISTS', KEYS[4]) == 1 then return 1 end
redis.call('ZADD', KEYS[3], 'LT', tonumber(ARGV[2]), ARGV[1])
return 1
`
// deviceEvalClaimLua 领取到期设备判断:写处理中标记并记下版本与场景快照。
// KEYS[1] 设备判断队列;KEYS[2] 执行中计数。ARGV:now_ms、lease_ms、上限、token 前缀。
// 返回 {过期数, device_id, token, ver, scenes, ...}。
const deviceEvalClaimLua = `
local now = tonumber(ARGV[1])
local lease = tonumber(ARGV[2])
local limit = tonumber(ARGV[3])
local expired = redis.call('ZREMRANGEBYSCORE', KEYS[2], '-inf', now)
local result = {expired}
local capacity = limit - redis.call('ZCARD', KEYS[2])
if capacity <= 0 then return result end
local ids = redis.call('ZRANGEBYSCORE', KEYS[1], '-inf', now, 'LIMIT', 0, capacity * 2 + 10)
local taken = 0
for _, id in ipairs(ids) do
if taken >= capacity then break end
local claimKey = 'polling:device:' .. id .. ':eval_claim'
local token = ARGV[4] .. ':' .. id
if redis.call('SET', claimKey, token, 'NX', 'PX', lease) then
redis.call('ZADD', KEYS[1], 'XX', now + lease, id)
redis.call('ZADD', KEYS[2], now + lease, token)
taken = taken + 1
local ver = redis.call('GET', 'polling:device:' .. id .. ':eval_ver') or '0'
local scenes = redis.call('SMEMBERS', 'polling:device:' .. id .. ':eval_scenes')
table.insert(result, id)
table.insert(result, token)
table.insert(result, ver)
table.insert(result, table.concat(scenes, ','))
else
local ttl = redis.call('PTTL', claimKey)
if ttl < 0 then ttl = lease end
redis.call('ZADD', KEYS[1], 'XX', now + ttl, id)
end
end
return result
`
// deviceEvalFinishLua 设备判断收尾:只移除本次已处理的场景;版本变大时立即再判断(返回 2);
// defer_ms > 0 表示本次未判断(旧路径持设备锁),推后再判断,场景不移除。
// KEYS[1] 处理中标记;KEYS[2] 执行权;KEYS[3] 执行中计数;KEYS[4] 版本;KEYS[5] 场景集合;KEYS[6] 队列。
// ARGV:token、device_id、领取时版本、now_ms、defer_ms、已处理场景…。
const deviceEvalFinishLua = `
if redis.call('GET', KEYS[1]) ~= ARGV[1] then return 0 end
redis.call('DEL', KEYS[1])
redis.call('DEL', KEYS[2])
redis.call('ZREM', KEYS[3], ARGV[1])
local now = tonumber(ARGV[4])
local deferMs = tonumber(ARGV[5])
if deferMs > 0 then
redis.call('ZADD', KEYS[6], now + deferMs, ARGV[2])
return 3
end
for i = 6, #ARGV do redis.call('SREM', KEYS[5], ARGV[i]) end
if (redis.call('GET', KEYS[4]) or '0') ~= ARGV[3] then
redis.call('ZADD', KEYS[6], now, ARGV[2])
return 2
end
redis.call('ZREM', KEYS[6], ARGV[2])
redis.call('DEL', KEYS[5])
return 1
`
var (
srWriteScript = redis.NewScript(srWriteLua)
srClaimScript = redis.NewScript(srClaimLua)
srFinishScript = redis.NewScript(srFinishLua)
srReleaseScript = redis.NewScript(srReleaseLua)
deviceRequestScript = redis.NewScript(deviceEvalRequestLua)
deviceClaimScript = redis.NewScript(deviceEvalClaimLua)
deviceFinishScript = redis.NewScript(deviceEvalFinishLua)
)