Files
junhong_cmp_fiber/openspec/changes/add-stop-resume-command-queue/design.md
邹进 02a842f066
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m35s
fix: 网关限流系统配置 Key 改为三段式,修复注册失败
- 受控系统配置要求 Key 至少三段,原两段式 Key(如 polling.rate_limit_mode)注册被拒,后台不可见、限流器只能使用默认值
- Key 改为 polling.gateway.* 与 polling.stop_resume_record.*;新增迁移 000247 将已落库的旧 Key 改名并保留取值
- 后续提案中规划的开关同步改为三段式(polling.scheduler.mode、polling.stop_resume.queue_enabled)
2026-10-24 16:17:34 +09:00

38 lines
4.3 KiB
Markdown
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.
## Context
完整设计见 `docs/engineering/轮询按运营商账户限流技术方案.md` 第三部分第 6、6.1、6.2、11.5、12 节,第 9 节停复机出口开关。
现状:`StopResumeService.EvaluateAndAct`(`stop_resume_service.go:113`)判断后当场调网关,`stopCardWithRetry` / `resumeCardWithRetry` 3 次、每次 sleep 2 秒;设备维度原因抢 `RedisPollingDeviceOpLockKey`(SetNX 30 秒,抢不到跳过)逐张停;`resumeAfterActivation`(4 处)、`reset_service.go:400`、`order/service.go:2196` 起 goroutine 调 `ResumeCardIfStopped`;退款 `refund/service.go:1258` 调 `ManualStopCard`,失败由 Outbox 与 `RecoverRefundPostProcessing` 反复补发;套餐到期由套餐轮询发现。第 5 个 Change 已提供账户队列、分发器、NSQ、失败告警表;第 1 个 Change 已提供限流、错误分类与停复机记录。
## Goals / Non-Goals
**Goals:** 自动停复机一个出口、不风暴、不翻转、不丢;设备下各卡一致;暂停轮询的卡套餐失效照常停。
**Non-Goals:** 不改停复机业务规则(第 4 个 Change 已定口径);不改后台人工点击、通道阈值、自动续费的停复机;不做提前停机、停机前续费等老系统规则。
## Decisions
1. **Redis 结构**:账户 `polling:carrier:{cid}:stop_resume`(ZSET)与 `:sr_running`;卡 `polling:card:{id}:stop_resume_cmd`(Hash:动作、原因、场景、来源、版本、失败计数、首次失败、最近错误、待确认)、`:sr_claim`、`:sr_exec`、`:sr_last_call`;设备 `polling:device_eval:queue`(ZSET)、`polling:device:{id}:eval_ver`、`:eval_scenes`(Set)、`:eval_claim`、`:eval_lock`;分发锁 `polling:device_eval:dispatch_lock`。
2. **统一判断函数**:从 `EvaluateAndAct`、`resumeSingleCard`、`resumeDeviceCards`、`ResumeCardIfStopped` 抽出"输入卡与资产数据 + 场景 → 目标状态与原因",入口与执行器共用;执行部分抽出供执行器调用一次网关。
3. **版本号**:覆盖指令 `HINCRBY ver`;领取时随消息带版本;收尾 Lua 比较版本决定移除或保留。
4. **单卡最小重试间隔按卡合计**:上游"卡状态变更每十分钟只允许调用 1 次"对停机与复机共同计数;领取 Lua 校验 `sr_last_call`,执行器调用前写。
5. **结果未知在队列内闭环**:不复用现有人工核对策略(`GatewayCardCommandUnknownRecoveryStrategy` 只有人工核对);确认查询属于查状态,计入停复机所在限流组,不受单卡最小间隔约束。
6. **设备判断**:`ZADD LT` 合并;`SADD eval_scenes`;领取带版本与场景快照;收尾 `SREM` 已处理场景并比较版本。锁内只读与写指令,毫秒级;与旧路径的 `RedisPollingDeviceOpLockKey` 互斥。
7. **全局开关**:`polling.stop_resume.queue_enabled` 受控系统配置;所有入口通过一个"请求评估"门面按开关决定入队还是旧路径当场执行,避免分散判断。停复机与设备判断的分发不受 `scheduler_mode` 限制:开关开启后对全部账户领取(`scheduler_mode = legacy` 时套餐步骤仍在旧 handler 内,同样改为请求评估)。
8. **退款完成语义**:请求写入成功即标记 `asset_reset`,补偿扫描只覆盖"请求未写入"的情况,不再因停机失败重放。
9. **到期即停入口**:`package_activation_handler.go` 到期失效处理处判断无其他有效主套餐后请求评估(场景"套餐到期");接续激活路径不变。
## Risks / Trade-offs
- [停复机比现在晚 1~2 秒] → 已接受;停复机先于卡任务领取并可预留 QPS。
- [请求评估写 Redis 失败] → 记日志,由该卡下一次套餐步骤兜底;暂停轮询的卡套餐失效停机按已定口径不补偿。
- [流量重置日集中复机] → 按账户停复机 QPS 与执行上限平滑执行,监控队列长度。
- [开关切换中旧路径与新路径并存] → 设备锁互斥;开关关闭时队列指令保留,重新开启时执行前重新判断。
## Migration Plan
1. 迁移插入开关(默认关闭)与告警阈值。
2. 测试环境部署,开关关闭时行为与现状一致;开启后按 tasks 第 10 节核对;演练开、关、再开。
3. 生产发布后开启开关,首日监控停复机成功率、队列长度与失败告警。
4. 回退:关闭开关。