Files
break a0de08d789
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m6s
避免套餐过期后排队权益永久失联
线上保持现有纯 Asynq 架构,以公平孤儿扫描和提交后投递消除永久饥饿及事务可见性竞态。

Constraint: 线上保持现有纯 Asynq 架构,不引入 Outbox、迁移或新任务基础设施。

Rejected: 事务内投递或扩大扫描 LIMIT | 无法消除竞态和永久饥饿。

Confidence: high

Scope-risk: narrow

Directive: 后续分支整合时按目标分支的套餐接续架构独立处理,不混用本热修实现。

Tested: go build ./...(退出码 0);git diff --check;openspec validate fix-main-package-activation-starvation --strict。

Not-tested: 按用户要求未新增、修改或运行自动化测试;线上 SQL、查询计划和日志待部署后核验。
2026-08-03 09:58:05 +08:00

3.3 KiB

MODIFIED Requirements

Requirement: 当前主套餐过期后自动激活下一个

系统 SHALL 在生效中或已用完主套餐到期时,先提交旧主套餐及关联加油包的状态事务,再投递同一载体队首待生效套餐的现有 Asynq 激活任务;系统 MUST NOT 在旧套餐事务提交前投递任务。

Scenario: 事务提交后投递队首套餐

  • WHEN 轮询处理一个到期的 status=1status=2 主套餐,且存在队首待生效套餐
  • THEN 系统先提交旧主套餐 status=3 和关联加油包 status=4 的事务
  • AND 提交成功后查询稳定队首并投递 package:queue:activation
  • AND 消费者读取时旧主套餐不再处于 status IN (1,2)

Scenario: 过期事务失败

  • WHEN 更新旧主套餐或关联加油包失败
  • THEN 事务回滚
  • AND 系统不得投递下一套餐激活任务
  • AND 下一轮过期扫描仍可重新处理

Scenario: 提交后入队失败

  • WHEN 旧套餐事务已经提交,但 Asynq 入队失败或进程退出
  • THEN 队首套餐保持 status=0
  • AND 周期孤儿扫描重新发现并补投该套餐

Scenario: 无待生效套餐

  • WHEN 旧套餐过期提交后不存在有效队首待生效套餐
  • THEN 系统不投递激活任务
  • AND 沿用既有无套餐停机检查

ADDED Requirements

Requirement: 孤儿待生效套餐必须公平恢复

系统 SHALL 在数据库中先选择每个卡或设备载体唯一的队首待生效主套餐,并排除仍有 status IN (1,2) 占位主套餐的载体,最后才执行单轮 100 个真实孤儿上限。队首顺序 MUST 为 priority ASC, created_at ASC, id ASC

Scenario: 固定窗口全部为非孤儿

  • WHEN 排序靠前的 100 条待生效记录均有占位主套餐,窗口之后存在真实孤儿
  • THEN 数据库先排除前 100 条非孤儿
  • AND 窗口后的真实孤儿进入本轮候选并被投递

Scenario: 同一载体有多条 pending

  • WHEN 一个真实孤儿载体存在多条待生效主套餐
  • THEN 本轮只选择 priority 最小、created_at 最早、id 最小的一条
  • AND 该载体只占一个恢复名额

Scenario: 生效中或已用完套餐占位

  • WHEN pending 所属载体存在 status=1status=2 主套餐
  • THEN 该载体不得进入孤儿候选

Requirement: Asynq 激活结果必须准确且可重试

系统 SHALL 仅在本次实际把套餐推进为 status=1 时记录新激活成功。Redis 激活锁冲突 MUST 返回错误,使 Asynq 按既有 MaxRetry(3) 重试,不得确认未执行任务成功。

Scenario: 锁冲突触发重试

  • WHEN 消费者未取得载体级套餐激活锁
  • THEN Service 返回套餐激活冲突错误
  • AND Handler 将错误返回 Asynq
  • AND 本次不记录激活成功

Scenario: 本次实际激活

  • WHEN 套餐为待生效、载体无占位主套餐且满足激活条件
  • THEN Service 返回 activated=true
  • AND Handler 记录包含套餐使用记录和触发类型的成功日志

Scenario: 条件暂不满足

  • WHEN Service 复检发现占位套餐或等待实名条件
  • THEN Service 返回 activated=false且不修改套餐
  • AND Handler 记录未激活原因,不记录成功