Files
junhong_cmp_fiber/openspec/changes/archive/2026-04-16-fix-polling-config-multi-match/proposal.md
huang 5d9be1d7e4
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m27s
fix: 修正轮询配置多匹配逻辑,支持同卡匹配多个配置并按 priority 合并 interval
核心变更:
- MatchConfig 改为 MatchConfigs,返回所有匹配配置
- MergedTaskIntervals 按 task type 合并各配置,选取最高优先级(非 nil 且最小 priority 值)
- hasAnyEnabledInterval 过滤所有 interval 均为 NULL 的配置
- calcInitialDelay 重构为纯函数,接收 interval 参数
- 移除 getEnabledTaskTypes 和 getIntervalByTaskType(被 MergedTaskIntervals 替代)
- scheduler.go 新增心跳 key + 顶层 panic recovery + Init 完成守卫
- initializer.go 批量失败日志升级为 Error,逐条检查 Pipeline 命令错误
- 数据迁移:禁用 id=29 的轮询配置(所有 interval 均为 NULL)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 14:27:47 +08:00

5.0 KiB
Raw Blame History

Why

本次变更包含两个独立但相关的修复,均涉及轮询系统的核心可靠性:

问题一(配置多匹配):当前 PollingConfigManager.MatchConfig(card) 的实现是"第一个匹配的配置直接返回,后续配置全部忽略"。这个设计在配置优先级体系下存在致命缺陷:高 priority 的 catch-all 配置(如 id=29priority=1所有 interval 全为 NULL会匹配所有卡直接跳过更具体的配置如 id=28priority=25card_status_check_interval=600导致整个轮询系统失效

问题二(调度器稳定性):轮询调度器(Scheduler)的主循环 scheduleLoop 没有顶层 panic recovery 和心跳上报机制。一旦 processActivationTasks 等路径发生 panic整个调度 goroutine 静默终止——所有分片队列持续积压,但 Worker 进程看起来正常,无任何告警。同时,初始化完成前调度器无谓地空轮询;initBatch 失败时仅记录 Warn 掩盖了卡初始化缺失的严重性Pipeline 执行只检查整体 error 而不逐条检查 cmder部分卡初始化失败会静默丢失。

本次修复将匹配逻辑从"独占式单匹配"改为"组合式多匹配",并同步修复上述调度器稳定性问题。

What Changes

配置多匹配修复

  • 修改 MatchConfigMatchConfigs:返回所有满足条件的配置(按 priority ASC 排序),而非第一个
  • 修改 matchConfigConditions:跳过所有 interval 均为 NULL 的配置(这些配置不应参与轮询匹配)
  • 修改 initBatch:对每种 task type从所有匹配配置中选取最高优先级的非 NULL interval
  • 修改 enqueueCardlifecycle_service.go):同上,合并多配置的 interval
  • 修改 getEnabledTaskTypes → 移除:不再需要,MergedTaskIntervals 已包含启用的 task type 列表
  • 重构 calcInitialDelay:修改函数签名接受 interval int 参数,移除对 *model.PollingConfig 的依赖(因为新逻辑中 interval 来源是 MergedTaskIntervals 合并结果,不再属于单一 config
  • 修改 requeueCardinternal/task/polling_base.go):同上,使用 MergedTaskIntervals 替代 MatchConfig + getIntervalByTaskType
  • 移除 getIntervalByTaskTypeinternal/task/polling_utils.goMergedTaskIntervals 已提供按 task type 查 interval 的能力,该辅助函数不再需要
  • 更新 polling-config-manager spec:反映新的多匹配语义

调度器稳定性修复

  • 新增 scheduleLoop 顶层 panic recovery:主调度 goroutine 崩溃时记录 Error 日志,防止调度静默停止
  • 新增 Scheduler 心跳机制:每次 tick 写入 polling:scheduler:heartbeatTTL=2×ScheduleInterval供告警规则检测调度器存活
  • 新增 Init 完成守卫processShardSchedule 首先检查 initializer.IsCompleted()Init 未完成时跳过出队,消除启动期无效轮询噪音
  • initBatch 失败日志 Warn→Error:批量初始化失败意味着部分卡未进入轮询队列,应使用 Error 级别
  • Pipeline 错误逐条检查pipe.Exec() 后遍历 []redis.Cmder 逐条检查 cmd.Err(),记录具体失败的 ZADD 命令,防止部分卡初始化失败静默丢失
  • 新增 RedisPollingSchedulerHeartbeatKey()pkg/constants/redis.go):心跳 Key 统一管理

Capabilities

Modified Capabilities

  • polling-config-manager:原有的"第一个匹配独占"语义改为"所有匹配配置 + 按 task type 选取最高优先级 interval"。这是需求级别变更(不仅是实现细节),需要更新 spec 中的 MatchConfig 描述和 Scenario。
  • polling-scheduler新增心跳上报、Init 完成守卫、顶层 panic recovery调度器可观测性和稳定性提升。

Impact

  • 受影响文件
    • internal/polling/config_manager.go:新增 MatchConfigsMergedTaskIntervalshasAnyEnabledInterval
    • internal/polling/initializer.goinitBatch interval 选取逻辑;日志级别 Warn→ErrorPipeline 逐条错误检查
    • internal/polling/lifecycle_service.goenqueueCard interval 选取逻辑,移除 getEnabledTaskTypes
    • internal/polling/scheduler.go:新增顶层 panic recovery、心跳写入、Init 守卫;新增 initializer 字段
    • internal/task/polling_base.gorequeueCard interval 选取逻辑(稳态路径,每次轮询任务完成后执行
    • internal/task/polling_utils.go:移除 getIntervalByTaskType(被 MergedTaskIntervals 替代)
    • pkg/constants/redis.go:新增 RedisPollingSchedulerHeartbeatKey()
  • 不受影响API 接口、DB schema、Asynq 任务提交逻辑(queue_manager.go、handler 层
  • 预期收益
    1. 配置系统按预期工作id=28suspended + card_status_check_interval=600能正确对停机卡生效
    2. 调度器故障可被告警规则(检查心跳 key及时发现不再静默失效
    3. 启动期不再产生无效轮询噪音
    4. 卡初始化失败可见,不再静默丢失