fix: 停复机指令队列审查问题
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m24s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m24s
- 开关开启但分发器心跳缺失时回退为当场执行,避免指令无人消费 - 待确认指令先查上游状态再判断;执行期间被覆盖时把待确认带到新指令 - 同方向失败退避中重复写入不提前执行时间 - 领取容量把执行上限与本秒剩余 QPS 分开计算 - 队列复机共用单卡复机锁;执行上下文取消后按结果未知确认 - 消费者并行停止;读取最近调用时间失败时不覆盖;成功但终结日志失败按成功处理 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ package task
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
stderrors "errors"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
@@ -95,7 +96,7 @@ func (e *StopResumeExecutor) Handle(ctx context.Context, body []byte, _ func())
|
||||
e.countOutcome(finishCtx, msg.CarrierID, outcome)
|
||||
result, err := e.store.FinishStopResume(finishCtx, carrierqueue.SRFinish{
|
||||
CardID: msg.CardID, CarrierID: msg.CarrierID, Token: msg.Token, Version: msg.Version,
|
||||
Done: outcome.done, NextAt: outcome.nextAt, Set: outcome.set, Del: outcome.del,
|
||||
Done: outcome.done, Unknown: outcome.set[carrierqueue.SRFieldPending] == "1", NextAt: outcome.nextAt, Set: outcome.set, Del: outcome.del,
|
||||
})
|
||||
switch {
|
||||
case err != nil:
|
||||
@@ -129,75 +130,92 @@ func (e *StopResumeExecutor) execute(ctx context.Context, msg carrierqueue.StopR
|
||||
if decision.Gone {
|
||||
return srEnded()
|
||||
}
|
||||
if decision.Action != cmd.Action {
|
||||
// 条件不再满足或已是目标状态:结束指令,不调用上游。
|
||||
e.logger.Info("停复机指令执行前重新判断不再需要,结束指令", zap.Uint("card_id", msg.CardID),
|
||||
zap.String("action", cmd.Action), zap.String("decided", decision.Action), zap.Strings("scenes", cmd.Scenes))
|
||||
e.recovered(cmd, decision.Card)
|
||||
return srEnded()
|
||||
}
|
||||
card := decision.Card
|
||||
reason := decision.Reason
|
||||
params := e.params(card.CarrierID)
|
||||
settings := e.settings.Current(ctx)
|
||||
gatewayCtx := gateway.WithRateLimitWait(ctx, 20*time.Second)
|
||||
|
||||
if !cmd.Pending {
|
||||
return e.call(gatewayCtx, card, cmd, reason, params, settings)
|
||||
// 待确认(上次结果未知):先查上游状态再按规则判断,避免上游已生效而库里未知时直接结束指令。
|
||||
clearedPending := false
|
||||
if cmd.Pending {
|
||||
if outcome, settled := e.confirm(gatewayCtx, card, cmd, settings); settled {
|
||||
return outcome
|
||||
}
|
||||
clearedPending = true
|
||||
}
|
||||
if confirmed, next := e.confirm(gatewayCtx, card, cmd, reason, params, settings); confirmed != nil {
|
||||
return *confirmed
|
||||
} else if next != nil {
|
||||
return *next
|
||||
if decision.Action != cmd.Action {
|
||||
// 条件不再满足或已是目标状态:结束指令,不调用上游。
|
||||
e.logger.Info("停复机指令执行前重新判断不再需要,结束指令", zap.Uint("card_id", msg.CardID),
|
||||
zap.String("action", cmd.Action), zap.String("decided", decision.Action), zap.Strings("scenes", cmd.Scenes))
|
||||
e.recovered(cmd, card)
|
||||
return srEnded()
|
||||
}
|
||||
// 确认不是目标状态且已满足单卡最小间隔:清除待确认后重新调用。
|
||||
result := e.call(gatewayCtx, card, cmd, reason, params, settings)
|
||||
if !result.done && result.set[carrierqueue.SRFieldPending] != "1" {
|
||||
if clearedPending {
|
||||
// 确认不是目标状态:未到单卡最小间隔则推后,否则清除待确认后重新调用。
|
||||
last, lastErr := e.store.LastStopResumeCall(ctx, card.ID)
|
||||
if allowed := last.Add(params.SRMinInterval); lastErr != nil || (!last.IsZero() && allowed.After(time.Now())) {
|
||||
if lastErr != nil {
|
||||
allowed = time.Now().Add(time.Minute)
|
||||
}
|
||||
retry := srRetry(allowed)
|
||||
retry.del = []string{carrierqueue.SRFieldPending}
|
||||
return retry
|
||||
}
|
||||
}
|
||||
result := e.call(gatewayCtx, card, cmd, decision.Reason, params, settings)
|
||||
if clearedPending && !result.done && result.set[carrierqueue.SRFieldPending] != "1" {
|
||||
result.del = append(result.del, carrierqueue.SRFieldPending)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// confirm 处理待确认指令:查一次卡状态。已是目标状态返回结束;查询失败保持待确认按退避重查;
|
||||
// 不是目标状态时清除待确认,未到单卡最小间隔则推后,否则返回 (nil, nil) 由调用方重新发起停复机。
|
||||
func (e *StopResumeExecutor) confirm(ctx context.Context, card *model.IotCard, cmd carrierqueue.CommandState, reason string,
|
||||
params carrierqueue.CarrierParams, settings carrierqueue.Settings) (*srOutcome, *srOutcome) {
|
||||
// confirm 处理待确认指令:查一次卡状态(受限流、不受单卡最小间隔约束)。
|
||||
// 已是目标状态:按成功回写并结束;若此时规则要求相反动作,写入新指令(版本变大,收尾保留新指令)。
|
||||
// 查询失败:保持待确认按退避重查,连续未知达阈值告警,不发起停复机。
|
||||
// 不是目标状态:返回 settled=false,由调用方按最新判断继续。
|
||||
func (e *StopResumeExecutor) confirm(ctx context.Context, card *model.IotCard, cmd carrierqueue.CommandState,
|
||||
settings carrierqueue.Settings) (srOutcome, bool) {
|
||||
status, known, err := e.service.QueryCommandCardStatus(ctx, card)
|
||||
if gateway.IsRateLimited(err) {
|
||||
retry := srRetry(time.Now().Add(stopResumeRateLimitedDelay))
|
||||
return nil, &retry
|
||||
return srRetry(time.Now().Add(stopResumeRateLimitedDelay)), true
|
||||
}
|
||||
if err != nil || !known {
|
||||
retry := srRetry(time.Now().Add(settings.SRBackoffFor(cmd.Unknown)))
|
||||
unknown := cmd.Unknown + 1
|
||||
retry := srRetry(time.Now().Add(settings.SRBackoffFor(unknown)))
|
||||
retry.set[carrierqueue.SRFieldUnknown] = strconv.Itoa(unknown)
|
||||
message := "确认查询未得到明确状态"
|
||||
if err != nil {
|
||||
retry.set[carrierqueue.SRFieldError] = truncateRunes(constants.StopResumeFailureUnknown+"|确认查询失败: "+err.Error(), 300)
|
||||
message = "确认查询失败: " + err.Error()
|
||||
}
|
||||
return nil, &retry
|
||||
}
|
||||
if status == targetNetworkStatus(cmd.Action) {
|
||||
if confirmErr := e.service.ConfirmCommandState(ctx, card, cmd.Action, reason); confirmErr != nil {
|
||||
e.logger.Warn("确认已是目标状态但回写失败,稍后再确认", zap.Uint("card_id", card.ID), zap.Error(confirmErr))
|
||||
retry := srRetry(time.Now().Add(time.Minute))
|
||||
return nil, &retry
|
||||
retry.set[carrierqueue.SRFieldError] = truncateRunes(constants.StopResumeFailureUnknown+"|"+message, 300)
|
||||
if unknown >= settings.SRUnknownAlert {
|
||||
e.reportFailure(card, cmd, constants.StopResumeFailureUnknown, message, unknown, cmd.WrittenAt)
|
||||
}
|
||||
e.logger.Info("停复机结果未知,确认查询已是目标状态,按成功结束", zap.Uint("card_id", card.ID), zap.String("action", cmd.Action))
|
||||
e.recovered(cmd, card)
|
||||
done := srDone()
|
||||
return &done, nil
|
||||
return retry, true
|
||||
}
|
||||
last, _ := e.store.LastStopResumeCall(ctx, card.ID)
|
||||
if allowed := last.Add(params.SRMinInterval); !last.IsZero() && allowed.After(time.Now()) {
|
||||
retry := srRetry(allowed)
|
||||
retry.del = []string{carrierqueue.SRFieldPending}
|
||||
return nil, &retry
|
||||
if status != targetNetworkStatus(cmd.Action) {
|
||||
return srOutcome{}, false
|
||||
}
|
||||
return nil, nil
|
||||
if confirmErr := e.service.ConfirmCommandState(ctx, card, cmd.Action, cmd.Reason); confirmErr != nil {
|
||||
e.logger.Warn("确认已是目标状态但回写失败,稍后再确认", zap.Uint("card_id", card.ID), zap.Error(confirmErr))
|
||||
return srRetry(time.Now().Add(time.Minute)), true
|
||||
}
|
||||
e.logger.Info("停复机结果未知,确认查询已是目标状态,按成功结束", zap.Uint("card_id", card.ID), zap.String("action", cmd.Action))
|
||||
e.recovered(cmd, card)
|
||||
if next, decideErr := e.service.DecideCommand(ctx, card.ID, cmd.Scenes); decideErr == nil && next.Card != nil &&
|
||||
next.Action != "" && next.Action != cmd.Action {
|
||||
if _, writeErr := e.store.WriteCommand(ctx, carrierqueue.StopResumeCommand{CardID: card.ID, CarrierID: card.CarrierID,
|
||||
Action: next.Action, Reason: next.Reason, Scenes: cmd.Scenes, Source: "confirm"}); writeErr != nil {
|
||||
e.logger.Warn("确认后按规则写入相反指令失败", zap.Uint("card_id", card.ID), zap.Error(writeErr))
|
||||
}
|
||||
}
|
||||
return srDone(), true
|
||||
}
|
||||
|
||||
// call 调用一次停机或复机并按结果分类处理。
|
||||
func (e *StopResumeExecutor) call(ctx context.Context, card *model.IotCard, cmd carrierqueue.CommandState, reason string,
|
||||
params carrierqueue.CarrierParams, settings carrierqueue.Settings) srOutcome {
|
||||
previousCall, _ := e.store.LastStopResumeCall(ctx, card.ID)
|
||||
previousCall, previousErr := e.store.LastStopResumeCall(ctx, card.ID)
|
||||
now := time.Now()
|
||||
if err := e.store.MarkStopResumeCall(ctx, card.ID, now, 2*params.SRMinInterval); err != nil {
|
||||
return srRetry(now.Add(time.Minute))
|
||||
@@ -209,13 +227,16 @@ func (e *StopResumeExecutor) call(ctx context.Context, card *model.IotCard, cmd
|
||||
e.recovered(cmd, card)
|
||||
return srDone()
|
||||
}
|
||||
if gateway.IsRateLimited(err) {
|
||||
// 本账户限流:请求未发出,恢复最近调用时间,短延迟重排,不计失败。
|
||||
e.restoreLastCall(ctx, card.ID, previousCall, params)
|
||||
if gateway.IsRateLimited(err) || stderrors.Is(err, iot_card_svc.ErrCommandBusy) {
|
||||
// 本账户限流或该卡复机正在其他路径执行:请求未发出,恢复最近调用时间(读取失败时保守保留),短延迟重排,不计失败。
|
||||
if previousErr == nil {
|
||||
e.restoreLastCall(ctx, card.ID, previousCall, params)
|
||||
}
|
||||
return srRetry(time.Now().Add(stopResumeRateLimitedDelay))
|
||||
}
|
||||
classification := e.classify(ctx, cmd.Action, err)
|
||||
if result.Unknown || classification.MaybeAccepted {
|
||||
// 执行上下文已取消(退出或超时)时请求可能已发出:按结果未知确认,不计失败。
|
||||
if result.Unknown || classification.MaybeAccepted || ctx.Err() != nil {
|
||||
return e.unknown(card, cmd, err, params, settings)
|
||||
}
|
||||
message := truncateRunes(classification.Category+"|"+err.Error(), 300)
|
||||
|
||||
Reference in New Issue
Block a user