Files
junhong_cmp_fiber/internal/task/polling_package_handler.go
邹进 2fe7c1f8ac feat: 按运营商账户调度的分发器、卡任务执行器与模式切换
- 四个轮询处理器拆为步骤函数 Step(返回结果分类),旧 Asynq Handle 只做并发控制与原有重入队映射,行为不变;carrier 模式下旧 handler 直接结束
- 网关限流增加等待模式(ctx 标记),NSQ 消费者等名额到单步超时,超时按限流处理不计失败;记录限流等待次数
- 卡任务执行器:核对 token 并抢执行权,按配置步骤顺序串行执行到期检查(单步超时、总超时、panic 恢复、流量已触发评估时跳过套餐),执行中续期并 Touch,续期失败立即停止;收尾写回到期、连续失败、退避、延迟确认与告警
- 分发器:每秒按账户抢分发锁领取(执行上限按消费能力收紧、优先占比、按限流组剩余 QPS 估算),发布到 polling.card.{账户},发布失败释放;消费旧优先提示与手动触发;心跳与处理中标记到期计数
- NSQ 生产者与按账户动态订阅的消费者管理器,优雅退出宽限期
- 失败告警异步写 tb_polling_failure_alert(同卡同类型一条未处理,成功转已恢复),账户窗口内告警数超阈值产生账户级告警
- 模式监听:切 carrier 时旧分片调度停止出队,加锁初始化新结构后开始领取并订阅;回 legacy 时停止消费、重建旧分片队列;生命周期(API 与 Worker)在 carrier 模式写账户队列
2026-10-25 18:44:23 +09:00

137 lines
6.0 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 task
import (
"context"
"time"
"github.com/hibiken/asynq"
"go.uber.org/zap"
priorityapp "github.com/break/junhong_cmp_fiber/internal/application/prioritypolling"
iot_card_svc "github.com/break/junhong_cmp_fiber/internal/service/iot_card"
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
"github.com/break/junhong_cmp_fiber/pkg/constants"
)
// PollingPackageHandler 套餐检查任务处理器
// 职责:触发 EvaluateAndAct 检查套餐状态并判断停复机
// 不做:直接停复机决策,委托给 StopResumeService.EvaluateAndAct
// 注:套餐状态由 PackageActivationHandler/DataResetHandler/UsageService 维护,
// 本 Handler 无需查 Gateway,只负责周期性触发再评估。
type PollingPackageHandler struct {
base *PollingBase
iotCardStore *postgres.IotCardStore
stopResumeSvc iot_card_svc.StopResumeServiceInterface
}
// NewPollingPackageHandler 创建套餐检查任务处理器
func NewPollingPackageHandler(
base *PollingBase,
iotCardStore *postgres.IotCardStore,
stopResumeSvc iot_card_svc.StopResumeServiceInterface,
) *PollingPackageHandler {
return &PollingPackageHandler{
base: base,
iotCardStore: iotCardStore,
stopResumeSvc: stopResumeSvc,
}
}
// Handle 处理套餐检查任务(旧分片调度)。按运营商账户调度时直接结束。
func (h *PollingPackageHandler) Handle(ctx context.Context, t *asynq.Task) error {
cardID, ok := parseTaskPayload(t.Payload(), h.base.logger)
if !ok {
return nil
}
if h.base.carrierSchedulerActive(ctx) {
return nil
}
ctx = withPollingWorkerAuditContext(ctx, constants.TaskTypePollingPackage, "套餐状态轮询任务", t.ResultWriter().TaskID())
if !h.base.acquireConcurrency(ctx, constants.TaskTypePollingPackage) {
h.base.logger.Debug("并发已满,重新入队", zap.Uint("card_id", cardID))
return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingPackage)
}
defer h.base.releaseConcurrency(ctx, constants.TaskTypePollingPackage)
return h.base.applyLegacyOutcome(ctx, cardID, constants.TaskTypePollingPackage, h.Step(ctx, cardID))
}
// Step 执行一次套餐检查步骤(按规则重新判断停复机),返回结果分类;不负责重入队。
// 调用方负责在 ctx 中设置 Worker 审计上下文。旧分片调度与按运营商账户调度共用。
func (h *PollingPackageHandler) Step(ctx context.Context, cardID uint) StepOutcome {
startTime := time.Now()
// 优先轮询互斥接缝:存在活动优先项时本轮即优先执行或被跳过;无活动项时行为与既有实现一致。
priority, skip, priorityErr := h.base.beginPollingPriority(ctx, cardID, constants.TaskTypePollingPackage)
if priorityErr != nil {
h.base.logger.Error("查询活动优先轮询项失败", zap.Uint("card_id", cardID), zap.Error(priorityErr))
h.base.updateStats(ctx, constants.TaskTypePollingPackage, false, time.Since(startTime))
return h.base.stepFail(ctx, "", "查询活动优先轮询项失败", priorityErr)
}
if skip {
h.base.logger.Debug("卡轮询优先项已被其它执行持有,本轮跳过并延后",
zap.Uint("card_id", cardID), zap.String("task_type", constants.TaskTypePollingPackage))
return stepDefer(0)
}
card, cacheErr := h.base.getCardWithCache(ctx, cardID)
if cacheErr != nil {
if isNotFound(cacheErr) {
priority.FailFinal(ctx, "卡不存在")
return StepOutcome{Result: StepCardGone}
}
h.base.logger.Error("获取卡信息失败", zap.Uint("card_id", cardID), zap.Error(cacheErr))
// 本地读卡失败属可恢复失败:与其余三个轮询 handler 统一按尝试上限收敛,避免加急事实被静默丢弃。
priority.MarkExecution(ctx)
priority.FailRetryable(ctx, "获取卡信息失败")
h.base.updateStats(ctx, constants.TaskTypePollingPackage, false, time.Since(startTime))
return h.base.stepFail(ctx, "", "获取卡信息失败", cacheErr)
}
// 执行前轮询范围校验:优先通道不得重启已停止轮询的卡。
if !h.base.priorityInPollingScope(ctx, priority, card) {
return stepSkip()
}
if h.stopResumeSvc == nil {
priority.FailFinal(ctx, "套餐评估能力未配置")
h.base.updateStats(ctx, constants.TaskTypePollingPackage, true, time.Since(startTime))
return stepSkip()
}
// 停复机评估就是本任务的上游执行动作:先累加尝试次数再发起。
priority.MarkExecution(ctx)
freshCard, loadErr := h.iotCardStore.GetByID(ctx, cardID)
if loadErr != nil {
h.base.logger.Warn("套餐检查:加载卡信息失败",
zap.Uint("card_id", cardID), zap.Error(loadErr))
priority.FailRetryable(ctx, "获取卡信息失败")
h.base.updateStats(ctx, constants.TaskTypePollingPackage, true, time.Since(startTime))
return h.base.stepFail(ctx, "", "获取卡信息失败", loadErr)
}
h.base.logger.Info("套餐检查:执行停复机评估",
zap.Uint("card_id", cardID),
zap.Int("network_status", freshCard.NetworkStatus),
zap.String("stop_reason", freshCard.StopReason))
if h.base.verboseLog {
h.base.logger.Info("套餐检查详情",
zap.Uint("card_id", cardID),
zap.String("iccid", freshCard.ICCID),
zap.Int("network_status", freshCard.NetworkStatus),
zap.String("stop_reason", freshCard.StopReason))
}
// 标记本次评估来自普通套餐轮询:无有效套餐场景据此才允许追加优先轮询请求,
// 其余调用链(观测消费者、手动实名等)不打标即不写。
evaluationCtx := priorityapp.WithPollingPriorityTrigger(ctx)
if evalErr := h.stopResumeSvc.EvaluateAndAct(evaluationCtx, freshCard); evalErr != nil {
h.base.logger.Warn("套餐检查后停复机评估失败",
zap.Uint("card_id", cardID), zap.Error(evalErr))
priority.FailRetryable(ctx, "停复机评估失败")
h.base.updateStats(ctx, constants.TaskTypePollingPackage, true, time.Since(startTime))
outcome := h.base.stepFail(ctx, constants.GatewayOpStart, "停复机评估失败", evalErr)
return outcome
}
priority.Complete(ctx)
h.base.updateStats(ctx, constants.TaskTypePollingPackage, true, time.Since(startTime))
return stepOK()
}