Files
junhong_cmp_fiber/internal/polling/carrierqueue/sr_dispatch.go
邹进 a4e3fe4d8f
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m24s
fix: 停复机指令队列审查问题
- 开关开启但分发器心跳缺失时回退为当场执行,避免指令无人消费
- 待确认指令先查上游状态再判断;执行期间被覆盖时把待确认带到新指令
- 同方向失败退避中重复写入不提前执行时间
- 领取容量把执行上限与本秒剩余 QPS 分开计算
- 队列复机共用单卡复机锁;执行上下文取消后按结果未知确认
- 消费者并行停止;读取最近调用时间失败时不覆盖;成功但终结日志失败按成功处理

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

122 lines
4.9 KiB
Go
Raw Permalink 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 (
"context"
"encoding/json"
"strconv"
"time"
"go.uber.org/zap"
"github.com/break/junhong_cmp_fiber/pkg/constants"
)
// StopResumeMessage 是停复机指令 NSQ 消息体。
type StopResumeMessage struct {
CardID uint `json:"card_id"`
CarrierID uint `json:"carrier_id"`
Token string `json:"token"`
Version int64 `json:"version"`
Action string `json:"action"`
Pending bool `json:"pending"`
DispatchedAt int64 `json:"dispatched_at"`
}
// DeviceEvalMessage 是设备判断 NSQ 消息体。
type DeviceEvalMessage struct {
DeviceID uint `json:"device_id"`
Token string `json:"token"`
Version string `json:"version"`
Scenes []string `json:"scenes"`
DispatchedAt int64 `json:"dispatched_at"`
}
// deviceEvalLimit 是全局同时执行的设备判断上限:判断只读数据库与写指令,毫秒级。
const deviceEvalLimit = 50
// stopResumeLease 是停复机指令与设备判断的租约:单次执行只调一次网关,远小于该值。
func (d *Dispatcher) stopResumeLease() time.Duration {
return 2 * time.Minute
}
// dispatchStopResume 在卡任务之前领取账户停复机指令:领取数 = min(停复机所在限流组本秒剩余, 执行上限 − 执行中)。
func (d *Dispatcher) dispatchStopResume(ctx context.Context, carrierID uint) {
params := d.ParamsFor(carrierID)
limit := params.SRInflightLimit
processes := d.cfg.SubscriberProcesses
if processes <= 0 {
processes = 1
}
if consume := params.SRMaxInFlight * processes; consume > 0 && limit > consume {
limit = consume
}
budget := -1
if d.budgets != nil {
ops := []string{constants.GatewayOpStop, constants.GatewayOpStart}
if groups, _, limited := d.budgets.Budget(ctx, carrierID, ops); limited {
for _, remaining := range groups {
if budget < 0 || remaining < budget {
budget = remaining
}
}
}
}
if limit <= 0 || budget == 0 {
return
}
claimed, expired, err := d.store.ClaimStopResume(ctx, SRClaimInput{
CarrierID: carrierID, Lease: d.stopResumeLease(), InflightLimit: limit, Budget: budget, MinInterval: params.SRMinInterval,
TokenPrefix: d.instanceID + ":sr:" + strconv.FormatInt(time.Now().UnixNano(), 36),
})
if err != nil {
d.logger.Warn("运营商账户停复机指令领取失败", zap.Uint("carrier_id", carrierID), zap.Error(err))
return
}
if expired > 0 {
d.logger.Warn("停复机指令处理中标记到期(执行崩溃或超时)", zap.Uint("carrier_id", carrierID), zap.Int64("count", expired),
zap.String("metric", "stop_resume.claim_expired"))
}
topic := constants.NSQStopResumeTopic(carrierID)
for _, item := range claimed {
body, _ := json.Marshal(StopResumeMessage{CardID: item.CardID, CarrierID: carrierID, Token: item.Token, Version: item.Version,
Action: item.Action, Pending: item.Pending, DispatchedAt: time.Now().UnixMilli()})
if err := d.publisher.Publish(topic, body); err != nil {
releaseCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 2*time.Second)
if releaseErr := d.store.ReleaseStopResume(releaseCtx, item.CardID, carrierID, item.Token); releaseErr != nil {
d.logger.Warn("发布失败后释放停复机指令失败", zap.Uint("card_id", item.CardID), zap.Error(releaseErr))
}
cancel()
d.logger.Warn("发布停复机指令失败,已释放", zap.Uint("card_id", item.CardID), zap.String("topic", topic), zap.Error(err))
}
}
}
// dispatchDeviceEvals 领取到期设备判断并发布;全局分发锁保证每轮只有一个实例领取。
func (d *Dispatcher) dispatchDeviceEvals(ctx context.Context, lockTTL time.Duration) {
ok, err := d.store.redis.SetNX(ctx, constants.RedisDeviceEvalDispatchLockKey, d.instanceID, lockTTL).Result()
if err != nil || !ok {
return
}
claimed, expired, err := d.store.ClaimDeviceEvals(ctx, d.stopResumeLease(), deviceEvalLimit,
d.instanceID+":dev:"+strconv.FormatInt(time.Now().UnixNano(), 36))
if err != nil {
d.logger.Warn("设备判断领取失败", zap.Error(err))
return
}
if expired > 0 {
d.logger.Warn("设备判断处理中标记到期(执行崩溃或超时)", zap.Int64("count", expired), zap.String("metric", "stop_resume.device_eval_expired"))
}
for _, item := range claimed {
body, _ := json.Marshal(DeviceEvalMessage{DeviceID: item.DeviceID, Token: item.Token, Version: item.Version, Scenes: item.Scenes,
DispatchedAt: time.Now().UnixMilli()})
if err := d.publisher.Publish(constants.NSQDeviceEvalTopic, body); err != nil {
releaseCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 2*time.Second)
if releaseErr := d.store.ReleaseDeviceEval(releaseCtx, item.DeviceID, item.Token); releaseErr != nil {
d.logger.Warn("发布失败后释放设备判断失败", zap.Uint("device_id", item.DeviceID), zap.Error(releaseErr))
}
cancel()
d.logger.Warn("发布设备判断失败,已释放", zap.Uint("device_id", item.DeviceID), zap.Error(err))
}
}
}