fix(通道流量阈值): AUG26-011 修复周期处理连接池自锁并补齐根池句柄验证与文档
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
This commit is contained in:
@@ -124,6 +124,8 @@ func (s *Service) processDueLock(ctx context.Context, item *DueLock, stats *Cycl
|
|||||||
// 只解锁不写复机事件、不调运营商:配置缺失时无法判断是否已跨期,解除通道锁交由既有复机链路
|
// 只解锁不写复机事件、不调运营商:配置缺失时无法判断是否已跨期,解除通道锁交由既有复机链路
|
||||||
// 与人工决定;anomaly_flag 与失败原因使运维可见并转人工核对。解锁与异常标记都是条件更新,
|
// 与人工决定;anomaly_flag 与失败原因使运维可见并转人工核对。解锁与异常标记都是条件更新,
|
||||||
// 重复执行不会产生第二次副作用。
|
// 重复执行不会产生第二次副作用。
|
||||||
|
// 两者 MUST 共用同一个事务句柄:解锁已持有该行锁,若异常标记改走服务自身连接池,另一条连接
|
||||||
|
// 会等待本事务的行锁(自锁),处理将挂死到语句超时并使该 cron 每分钟空转。
|
||||||
func (s *Service) processUnresolvableLock(ctx context.Context, item *DueLock, stats *CycleStats) error {
|
func (s *Service) processUnresolvableLock(ctx context.Context, item *DueLock, stats *CycleStats) error {
|
||||||
lock := &item.Lock
|
lock := &item.Lock
|
||||||
s.logger.Warn("通道阈值锁行周期归属不可判定,解锁并转人工核对",
|
s.logger.Warn("通道阈值锁行周期归属不可判定,解锁并转人工核对",
|
||||||
@@ -139,7 +141,7 @@ func (s *Service) processUnresolvableLock(ctx context.Context, item *DueLock, st
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
unlocked = true
|
unlocked = true
|
||||||
_, anomalyErr := s.markAnomaly(ctx, lock.ID, item.Reason)
|
_, anomalyErr := s.markAnomalyInTx(ctx, tx, lock.ID, item.Reason)
|
||||||
return anomalyErr
|
return anomalyErr
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -297,13 +297,25 @@ func (s *Service) markTaskConfirmed(ctx context.Context, lockID uint, task lockT
|
|||||||
return s.markTaskOutcome(ctx, lockID, task, domain.UnresolvedTaskStatuses(), domain.TaskStatusConfirmed, integrationID, "")
|
return s.markTaskOutcome(ctx, lockID, task, domain.UnresolvedTaskStatuses(), domain.TaskStatusConfirmed, integrationID, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
// markAnomaly 把锁标记为需人工核对并退出自动扫描。
|
// markAnomaly 把锁标记为需人工核对并退出自动扫描(使用服务自身连接池,调用方不得已持有该行锁)。
|
||||||
// 只应在窗口超期且结果无法确认时调用;已标记的锁不再重复查询。
|
// 只应在窗口超期且结果无法确认时调用;已标记的锁不再重复查询。
|
||||||
func (s *Service) markAnomaly(ctx context.Context, lockID uint, reason string) (bool, error) {
|
func (s *Service) markAnomaly(ctx context.Context, lockID uint, reason string) (bool, error) {
|
||||||
if s == nil || s.db == nil || lockID == 0 {
|
if s == nil || s.db == nil {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
result := s.db.WithContext(ctx).Model(&model.CarrierTrafficThresholdLock{}).
|
return s.markAnomalyInTx(ctx, s.db, lockID, reason)
|
||||||
|
}
|
||||||
|
|
||||||
|
// markAnomalyInTx 在调用方事务内把锁标记为需人工核对(anomaly_flag=0 → 1 条件更新)。
|
||||||
|
//
|
||||||
|
// 调用方已在同一事务内写过该行时 MUST 使用本方法:改用服务自身连接池会让另一条连接
|
||||||
|
// 等待本事务持有的行锁(自锁),表现为处理挂死到语句超时。返回 false 表示已被并发标记,
|
||||||
|
// 调用方按幂等处理。
|
||||||
|
func (s *Service) markAnomalyInTx(ctx context.Context, tx *gorm.DB, lockID uint, reason string) (bool, error) {
|
||||||
|
if tx == nil || lockID == 0 {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
result := tx.WithContext(ctx).Model(&model.CarrierTrafficThresholdLock{}).
|
||||||
Where("id = ? AND anomaly_flag = ?", lockID, domain.AnomalyFlagNone).
|
Where("id = ? AND anomaly_flag = ?", lockID, domain.AnomalyFlagNone).
|
||||||
Updates(map[string]any{
|
Updates(map[string]any{
|
||||||
"anomaly_flag": domain.AnomalyFlagManual,
|
"anomaly_flag": domain.AnomalyFlagManual,
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ period_start = M.AddDate(0, -1, 0) if now < M // 上月重置日 0 点
|
|||||||
|
|
||||||
1. **周期处理**:扫描待处理的持锁锁行(`status=locked`),按**锁行自身 carrier** 的 `data_reset_day` 分两种结果处理:
|
1. **周期处理**:扫描待处理的持锁锁行(`status=locked`),按**锁行自身 carrier** 的 `data_reset_day` 分两种结果处理:
|
||||||
- 已跨期(`period_start` 早于该 carrier 的当前周期起点,支持换运营商后旧锁归属):条件更新认领解锁(`status=locked → unlocked`,防并发重复);逐卡评估有效主套餐(`hasValidPackage`)+ 流量未耗尽(`isTrafficExhausted`)+ 实名 OK(`isRealnameOK`)+ 非风险 extend + 无其他停因;全满足写复机 Outbox 事件,任一不满足只解锁并把原因写入锁行。
|
- 已跨期(`period_start` 早于该 carrier 的当前周期起点,支持换运营商后旧锁归属):条件更新认领解锁(`status=locked → unlocked`,防并发重复);逐卡评估有效主套餐(`hasValidPackage`)+ 流量未耗尽(`isTrafficExhausted`)+ 实名 OK(`isRealnameOK`)+ 非风险 extend + 无其他停因;全满足写复机 Outbox 事件,任一不满足只解锁并把原因写入锁行。
|
||||||
- 周期归属不可判定(锁行引用的 carrier 已不存在/软删,或 `data_reset_day` 非法):**同样认领解锁**(按 spec「新周期对仍持锁卡解除通道锁」的语义)**并同事务 `anomaly_flag=1` + 安全原因**、Warn 日志转人工;不写复机事件、不调运营商。该分支是必须的出路:`ActiveLock` 对配置缺失按仍未生效处理(fail-closed,不放开复机),若周期处理也跳过这些行,持锁卡将永久禁止一切复机且无自动出路。
|
- 周期归属不可判定(锁行引用的 carrier 已不存在/软删,或 `data_reset_day` 非法):**同样认领解锁**(按 spec「新周期对仍持锁卡解除通道锁」的语义)**并同事务 `anomaly_flag=1` + 安全原因**、Warn 日志转人工;不写复机事件、不调运营商。解锁与异常标记 MUST 共用同一事务句柄(解锁已持有该行锁,异常标记若改走连接池的另一条连接会等待该行锁直到语句超时,表现为该锁停在 `locked` 且周期处理每分钟空转)。该分支是必须的出路:`ActiveLock` 对配置缺失按仍未生效处理(fail-closed,不放开复机),若周期处理也跳过这些行,持锁卡将永久禁止一切复机且无自动出路。
|
||||||
2. **恢复扫描**:扫描 `anomaly_flag=0` 且 `stop_status`/`resume_status` 处于**未决集合 `{submitted, unknown, failed}`** 的锁;**只查询网关状态回填,绝不重复发起停复机**;确认成功 → 回填任务状态(`confirmed` 只写一次,条件更新按未决集合为谓词)并补写卡状态(覆盖「Gateway 成功但 DB 更新失败」场景);自提交起超过 **30 分钟**(常量定义)仍不可查 → `anomaly_flag=1` + 安全失败原因,退出扫描转人工,不自动删除锁。
|
2. **恢复扫描**:扫描 `anomaly_flag=0` 且 `stop_status`/`resume_status` 处于**未决集合 `{submitted, unknown, failed}`** 的锁;**只查询网关状态回填,绝不重复发起停复机**;确认成功 → 回填任务状态(`confirmed` 只写一次,条件更新按未决集合为谓词)并补写卡状态(覆盖「Gateway 成功但 DB 更新失败」场景);自提交起超过 **30 分钟**(常量定义)仍不可查 → `anomaly_flag=1` + 安全失败原因,退出扫描转人工,不自动删除锁。
|
||||||
- **复机消费者**:结构同停机消费者,认领字段 `resume_submitted_at`,复用 `resumeCardWithRetry`,成功后写 `network_status=online`、`resumed_at`,并**只在该卡停因正是 `channel_threshold` 时清除停因**(不覆盖 arrears/manual 等其他停因)。
|
- **复机消费者**:结构同停机消费者,认领字段 `resume_submitted_at`,复用 `resumeCardWithRetry`,成功后写 `network_status=online`、`resumed_at`,并**只在该卡停因正是 `channel_threshold` 时清除停因**(不覆盖 arrears/manual 等其他停因)。
|
||||||
- 周期处理与恢复 Handler 审计上下文固定 `ActorKind=AuditActorScheduledJob`、`Source=AuditSourceScheduler`。
|
- 周期处理与恢复 Handler 审计上下文固定 `ActorKind=AuditActorScheduledJob`、`Source=AuditSourceScheduler`。
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
## 5. 周期处理与恢复 cron
|
## 5. 周期处理与恢复 cron
|
||||||
- [x] 5.1 周期处理 cron(`@every 1m` + `asynq.Unique(10m)` + 无 payload):扫描 period_start 过期持锁锁行(按锁行自身 carrier 的 `data_reset_day` 判断),条件更新认领解锁,逐卡评估(有效主套餐 + 流量未耗尽 + 实名 OK + 非风险 extend + 无其他停因),全满足写复机事件,否则只解锁。
|
- [x] 5.1 周期处理 cron(`@every 1m` + `asynq.Unique(10m)` + 无 payload):扫描 period_start 过期持锁锁行(按锁行自身 carrier 的 `data_reset_day` 判断),条件更新认领解锁,逐卡评估(有效主套餐 + 流量未耗尽 + 实名 OK + 非风险 extend + 无其他停因),全满足写复机事件,否则只解锁。
|
||||||
- [x] 5.2 恢复扫描 cron(同形态):扫描 stop/resume 为 submitted 的锁,只查询网关状态回填、绝不重复发起停复机;确认成功补写卡状态;自提交起超 30 分钟(常量)仍不可查 → `anomaly_flag` + 安全失败原因转人工,不自动删除锁。
|
- [x] 5.2 恢复扫描 cron(同形态):扫描未决子任务(`domain.UnresolvedTaskStatuses()` = `{submitted, unknown, failed}`,见 8.1)的锁,只查询网关状态回填、绝不重复发起停复机;确认成功补写卡状态;自提交起超 30 分钟(常量)仍不可查 → `anomaly_flag` + 安全失败原因转人工,不自动删除锁。
|
||||||
- [x] 5.3 worker 注册两个 cron 与装配;周期处理与恢复 Handler 审计上下文固定 `ActorKind=AuditActorScheduledJob`、`Source=AuditSourceScheduler`。
|
- [x] 5.3 worker 注册两个 cron 与装配;周期处理与恢复 Handler 审计上下文固定 `ActorKind=AuditActorScheduledJob`、`Source=AuditSourceScheduler`。
|
||||||
|
|
||||||
## 6. 停因与复机拒绝
|
## 6. 停因与复机拒绝
|
||||||
@@ -36,4 +36,5 @@
|
|||||||
- [x] 8.4(R2)按 ENG-AUDIT-001 显式登记锁行状态迁移(`unlocked`、`anomaly_flag=1`)的审计决定(不新建独立审计动作 + 承载方式),并在 design.md 补「审计决定」小节。
|
- [x] 8.4(R2)按 ENG-AUDIT-001 显式登记锁行状态迁移(`unlocked`、`anomaly_flag=1`)的审计决定(不新建独立审计动作 + 承载方式),并在 design.md 补「审计决定」小节。
|
||||||
- [x] 8.5(R4)以 `specs/package-lifecycle/spec.md` MODIFIED delta 建模「持通道阈值锁时拒绝复机」对既有套餐生命周期自动复机路径的行为变更,并更新 proposal 的 Modified Capabilities。
|
- [x] 8.5(R4)以 `specs/package-lifecycle/spec.md` MODIFIED delta 建模「持通道阈值锁时拒绝复机」对既有套餐生命周期自动复机路径的行为变更,并更新 proposal 的 Modified Capabilities。
|
||||||
- [x] 8.6 修正 design.md 与实现不符的表述(失败/未知与不可判定 carrier 的收敛路径、四入口拒绝顺序与返回、认领谓词)。
|
- [x] 8.6 修正 design.md 与实现不符的表述(失败/未知与不可判定 carrier 的收敛路径、四入口拒绝顺序与返回、认领谓词)。
|
||||||
- [x] 8.7 回归验证:B1(unknown/failed 扫描与收敛、超期 anomaly)、B2(carrier 缺失/重置日非法的解锁与异常)、R1(审计写入)在隔离库实测;gofmt/go build/go vet/openspec validate --strict/context-health 全通过。
|
- [x] 8.7 回归验证:B1(unknown/failed 扫描与收敛、超期 anomaly)与 B2(carrier 缺失/重置日非法的解锁与异常)**用生产等价句柄(Service.s.db 为根连接池)实测**——fixture 以已提交短事务创建、逐条断言后删除并核对 0 残留(回滚事务式写法会掩蔽连接池自锁,不再用于这两条路径);R1(审计写入)用回滚事务实测;gofmt/go build/go vet/openspec validate --strict/context-health 全通过。
|
||||||
|
- [x] 8.8(B3)修复连接池自锁:新增事务句柄版 `markAnomalyInTx`(`markAnomaly` 以自身连接池委托同一实现),`processUnresolvableLock` 在事务闭包内用 `tx` 标记异常,使解锁与 anomaly 同事务提交(否则解锁已持行锁、另一条连接上的 anomaly 更新会等待该行锁直到语句超时,表现为不可判定锁行停在 locked 且周期处理 cron 每分钟空转);并完成同类自锁全面排查(结论:本 Change 仅此一处)。
|
||||||
|
|||||||
Reference in New Issue
Block a user