fix: 网关限流系统配置 Key 改为三段式,修复注册失败
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m35s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m35s
- 受控系统配置要求 Key 至少三段,原两段式 Key(如 polling.rate_limit_mode)注册被拒,后台不可见、限流器只能使用默认值 - Key 改为 polling.gateway.* 与 polling.stop_resume_record.*;新增迁移 000247 将已落库的旧 Key 改名并保留取值 - 后续提案中规划的开关同步改为三段式(polling.scheduler.mode、polling.stop_resume.queue_enabled)
This commit is contained in:
@@ -540,7 +540,7 @@
|
||||
|
||||
#### 7.5 系统参数(yaml)
|
||||
|
||||
默认 QPS、默认执行上限(`inflight_limit`、`sr_inflight_limit`)、默认 `max_in_flight`、设备判断 `max_in_flight`、分发间隔、卡任务总超时(各步骤超时之和 + 余量,见 3.3 节)、NSQ 地址(nsqd / nsqlookupd)、NSQ `msg_timeout`(须大于续期间隔,执行中靠 `Touch` 延长)、处理中标记 TTL 即租约时长(须大于 `msg_timeout` × 允许重投次数)、租约续期间隔(默认租约时长 / 3)、QPS 最长等待、优雅退出宽限期、重置日前加密的收尾提前量(11.10 节)、激活前流量结算总超时(11.9 节)、停复机记录表分区提前创建月数(6.2 节)。停复机出口全局开关 `stop_resume_queue_enabled` 不放 yaml,放受控系统配置 `polling.stop_resume_queue_enabled`(默认关闭),开关与回退即时生效、无需重新部署(第 9 节、14.1 节)。
|
||||
默认 QPS、默认执行上限(`inflight_limit`、`sr_inflight_limit`)、默认 `max_in_flight`、设备判断 `max_in_flight`、分发间隔、卡任务总超时(各步骤超时之和 + 余量,见 3.3 节)、NSQ 地址(nsqd / nsqlookupd)、NSQ `msg_timeout`(须大于续期间隔,执行中靠 `Touch` 延长)、处理中标记 TTL 即租约时长(须大于 `msg_timeout` × 允许重投次数)、租约续期间隔(默认租约时长 / 3)、QPS 最长等待、优雅退出宽限期、重置日前加密的收尾提前量(11.10 节)、激活前流量结算总超时(11.9 节)、停复机记录表分区提前创建月数(6.2 节)。停复机出口全局开关 `stop_resume_queue_enabled` 不放 yaml,放受控系统配置 `polling.stop_resume.queue_enabled`(默认关闭),开关与回退即时生效、无需重新部署(第 9 节、14.1 节)。
|
||||
|
||||
#### 7.6 业务事件检查场景(后台,全局,见 5.1 节)
|
||||
|
||||
@@ -571,9 +571,9 @@
|
||||
|
||||
| 全局开关(受控系统配置,后台即时生效,见 14.1 节) | 所属 change | 开启 | 关闭(回退) |
|
||||
|---|---|---|---|
|
||||
| `polling.rate_limit_mode`(`observe` / `enforce`) | 1 | 拦截:按限流组执行 | 只统计:计数但不拦截 |
|
||||
| `polling.scheduler_mode`(`legacy` / `carrier`) | 5 | 全部卡任务走新调度(按账户队列 + NSQ) | 全部回到旧分片调度 |
|
||||
| `polling.stop_resume_queue_enabled`(bool) | 6 | 全部自动停复机经指令队列与设备判断 | 各自动入口恢复当场执行 |
|
||||
| `polling.gateway.rate_limit_mode`(`observe` / `enforce`) | 1 | 拦截:按限流组执行 | 只统计:计数但不拦截 |
|
||||
| `polling.scheduler.mode`(`legacy` / `carrier`) | 5 | 全部卡任务走新调度(按账户队列 + NSQ) | 全部回到旧分片调度 |
|
||||
| `polling.stop_resume.queue_enabled`(bool) | 6 | 全部自动停复机经指令队列与设备判断 | 各自动入口恢复当场执行 |
|
||||
|
||||
- 新 key 前缀,与旧 key 不冲突;新旧调度不双跑:任一时刻只有 `scheduler_mode` 指定的一套调度在领取与执行。
|
||||
- 入口写入:所有写入轮询结构的入口(Initializer、生命周期、优先提示、手动触发、业务事件与读入口请求检查)按 `scheduler_mode` 写新结构或旧结构,不再按账户分流。
|
||||
@@ -834,9 +834,9 @@
|
||||
| 限流组(7.1) | `GET /polling-rate-limit-groups`(按运营商账户筛选,含全局组)、`POST`、`GET /:id`、`PUT /:id`、`PUT /:id/status`、`DELETE /:id` | 保存时校验:同账户同一操作只属于一个组、`reserved_qps < max_qps`、高优先级操作属于本组操作;停用提示"回落默认 QPS" |
|
||||
| 运营商账户调度参数(7.2) | `GET /polling-carrier-params`(列表:全部运营商账户,未配置的返回全局默认值并标记"默认")、`GET /:carrier_id`、`PUT /:carrier_id`(不存在则创建)、`DELETE /:carrier_id`(恢复全局默认) | 保存时按当前订阅进程数校验执行上限与 `max_in_flight`(3.2 节);不提供按账户切换调度的接口(不灰度,见第 9 节) |
|
||||
| 卡任务步骤、失败告警标量参数(7.3、7.4) | 复用现有 `GET /system-configs?module=polling`、`PUT /system-configs/:key` | 需改动:注册 `module = polling` 的配置项;`SystemConfigListRequest.Module` 枚举增加 `polling`;`step_order`、`backoff`、`alert_fail_count` 等 JSON 值现只校验 JSON 合法,需为注册项增加结构校验(如步骤只能是 4 种类型且不重复、退避序列递增且不超过检查间隔) |
|
||||
| 限流模式 | 复用 `/system-configs`,键 `polling.rate_limit_mode`(`observe` / `enforce`) | 全局生效,不按账户覆盖 |
|
||||
| 调度模式 | 复用 `/system-configs`,键 `polling.scheduler_mode`(`legacy` / `carrier`) | 切换触发第 9 节初始化流程;`GET /polling-stats/init-progress`(现有接口)查看新结构初始化进度 |
|
||||
| 停复机出口全局开关 | 复用 `/system-configs`,键 `polling.stop_resume_queue_enabled`(bool) | 从 yaml 改为受控系统配置:开关与回退需要即时生效,不能依赖重新部署(第 9 节) |
|
||||
| 限流模式 | 复用 `/system-configs`,键 `polling.gateway.rate_limit_mode`(`observe` / `enforce`) | 全局生效,不按账户覆盖 |
|
||||
| 调度模式 | 复用 `/system-configs`,键 `polling.scheduler.mode`(`legacy` / `carrier`) | 切换触发第 9 节初始化流程;`GET /polling-stats/init-progress`(现有接口)查看新结构初始化进度 |
|
||||
| 停复机出口全局开关 | 复用 `/system-configs`,键 `polling.stop_resume.queue_enabled`(bool) | 从 yaml 改为受控系统配置:开关与回退需要即时生效,不能依赖重新部署(第 9 节) |
|
||||
| 上游错误分类(7.4) | `GET /gateway-error-classifications`(按类别、操作、启用状态、关键字筛选)、`POST`、`PUT /:id`、`PUT /:id/status`、`DELETE /:id`;`POST /gateway-error-classifications/test`(输入一段上游返回信息,返回命中的规则与类别) | 测试接口用于运营新增规则前验证 |
|
||||
| 业务事件检查场景(7.6) | `GET /polling-check-scenes`、`PUT /polling-check-scenes/:scene` | 场景由代码预置(入口在代码里),不提供新增与删除,只改检查类型、确认延迟、冷却 |
|
||||
| 轮询配置(分层与重置日前加密,11.3、11.10) | 现有 `/polling-configs` 增加 `package_state`、`asset_status`、`pre_reset_window`、`pre_reset_carddata_interval` 四个字段,去掉 `protect_check_interval`;间隔支持 `0` = 不检查;新增 `PUT /polling-configs/priorities`(整体排序)、`GET /polling-configs/match-preview`(匹配预览)、`POST /polling-configs/recalculate-due` 与 `GET /polling-configs/recalculate-due/:task_id`(重算到期时间及进度) | 保存校验与三种间隔含义见 11.3 节;前端以"按间隔 / 不检查 / 继承"三态展示每种检查类型,并支持拖拽排序 |
|
||||
@@ -1079,7 +1079,7 @@
|
||||
- 同一任务因限流累计推迟超过上限(系统参数)时写告警,任务仍继续重试。
|
||||
- 只改 Asynq 服务配置与错误类型,不改各消费者业务代码;对所有 Asynq 任务生效,非限流错误的行为不变。
|
||||
|
||||
第 1 步的拦截模式:限流器支持"只统计"与"拦截"两种模式,由受控系统配置 `polling.rate_limit_mode`(`observe` / `enforce`,默认 `observe`)全局控制,不按账户覆盖。
|
||||
第 1 步的拦截模式:限流器支持"只统计"与"拦截"两种模式,由受控系统配置 `polling.gateway.rate_limit_mode`(`observe` / `enforce`,默认 `observe`)全局控制,不按账户覆盖。
|
||||
- 只统计:照常计数(各账户实际 QPS、本应被限流的次数),不拦截、不等待。
|
||||
- 拦截:按 4 节执行。
|
||||
- 上线顺序:change 1 先以只统计模式全量上线,采集至少一周各账户实际调用量与失败分类;按数据配好全部账户的限流组后,整体切到拦截模式。避免上线瞬间所有未配置的账户按默认 QPS 被拦截,导致整体轮询变慢。
|
||||
@@ -1201,7 +1201,7 @@
|
||||
- 停复机结果未知不沿用现有恢复扫描(现有普通停复机只有人工核对策略),在指令队列内按"确认延迟 → 查状态 → 已是目标状态结束 / 否则按单卡最小重试间隔重发"处理(第三部分第 6 节第 5 条)。
|
||||
- 到期即停不走套餐步骤,以"套餐到期"场景直接请求评估;设备判断队列保存并按并集合并请求场景,避免暂停轮询的卡或设备漏停(第三部分第 6.1、11.5 节)。
|
||||
- ~~跨账户设备:卡任务按账户灰度,停复机出口改为全局开关~~(已被同日"不灰度"取代)。
|
||||
- 不灰度(2026-10-24 已定):不按运营商账户、设备或卡灰度,每个 change 对全部账户整体切换;只保留 `polling.rate_limit_mode`、`polling.scheduler_mode`、`polling.stop_resume_queue_enabled` 三个全局开关用于整体回退;取消账户调度参数中的 `polling_enabled` 与 `rate_limit_mode`;跨账户设备(约 58%)仍为核心验收场景(第三部分第 9 节)。
|
||||
- 不灰度(2026-10-24 已定):不按运营商账户、设备或卡灰度,每个 change 对全部账户整体切换;只保留 `polling.gateway.rate_limit_mode`、`polling.scheduler.mode`、`polling.stop_resume.queue_enabled` 三个全局开关用于整体回退;取消账户调度参数中的 `polling_enabled` 与 `rate_limit_mode`;跨账户设备(约 58%)仍为核心验收场景(第三部分第 9 节)。
|
||||
- 分层配置的间隔区分三态:正数按间隔、`0` 不检查、空继承;保持空 = 继承不改变现有配置行为。优先级可后台调整(单条、整体排序),相同优先级按 ID 排序;提供匹配预览与"立即重算到期时间"(第三部分第 11.3 节)。
|
||||
- 所有后台配置、告警、不轮询清单、停复机记录都提供查询与修改接口,供前端建页面;标量参数复用 `/system-configs`(新增 `polling` 模块与 JSON 结构校验);停复机出口全局开关从 yaml 改为受控系统配置(第三部分第 14 节)。
|
||||
- 复机不看停机原因,包括运营商侧主动停机 `carrier_stopped`,只由本系统规则决定(第三部分第 12 节)。
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
-- 回滚网关限流配置 Key 改名:恢复为 000246 的两段式 Key,保留值,与 up 成对。
|
||||
|
||||
UPDATE tb_system_config SET config_key = 'polling.rate_limit_mode', updated_at = NOW() WHERE config_key = 'polling.gateway.rate_limit_mode';
|
||||
UPDATE tb_system_config SET config_key = 'polling.gateway_default_qps', updated_at = NOW() WHERE config_key = 'polling.gateway.default_qps';
|
||||
UPDATE tb_system_config SET config_key = 'polling.rate_limited_retry_delay_seconds', updated_at = NOW() WHERE config_key = 'polling.gateway.rate_limited_retry_delay_seconds';
|
||||
UPDATE tb_system_config SET config_key = 'polling.rate_limit_defer_alert_seconds', updated_at = NOW() WHERE config_key = 'polling.gateway.rate_limit_defer_alert_seconds';
|
||||
UPDATE tb_system_config SET config_key = 'polling.stop_resume_record_retention_months', updated_at = NOW() WHERE config_key = 'polling.stop_resume_record.retention_months';
|
||||
UPDATE tb_system_config SET config_key = 'polling.stop_resume_record_premake_months', updated_at = NOW() WHERE config_key = 'polling.stop_resume_record.premake_months';
|
||||
@@ -0,0 +1,28 @@
|
||||
-- 网关限流受控系统配置 Key 改为三段式(add-carrier-gateway-rate-limit 修正)。
|
||||
-- 背景:受控系统配置注册表要求 Key 至少三段(如 carrier_callback.ctcc_realname.enabled),000246 插入的 Key 只有两段,
|
||||
-- 代码注册时被拒绝("系统配置注册信息不完整"),后台看不到也改不了这些配置,限流器只能使用代码默认值。
|
||||
-- 本迁移只改 Key 名,保留已落库的值;新 Key 已存在时不覆盖(先删旧行避免唯一键冲突)。
|
||||
|
||||
UPDATE tb_system_config SET config_key = 'polling.gateway.rate_limit_mode', updated_at = NOW()
|
||||
WHERE config_key = 'polling.rate_limit_mode'
|
||||
AND NOT EXISTS (SELECT 1 FROM tb_system_config WHERE config_key = 'polling.gateway.rate_limit_mode');
|
||||
UPDATE tb_system_config SET config_key = 'polling.gateway.default_qps', updated_at = NOW()
|
||||
WHERE config_key = 'polling.gateway_default_qps'
|
||||
AND NOT EXISTS (SELECT 1 FROM tb_system_config WHERE config_key = 'polling.gateway.default_qps');
|
||||
UPDATE tb_system_config SET config_key = 'polling.gateway.rate_limited_retry_delay_seconds', updated_at = NOW()
|
||||
WHERE config_key = 'polling.rate_limited_retry_delay_seconds'
|
||||
AND NOT EXISTS (SELECT 1 FROM tb_system_config WHERE config_key = 'polling.gateway.rate_limited_retry_delay_seconds');
|
||||
UPDATE tb_system_config SET config_key = 'polling.gateway.rate_limit_defer_alert_seconds', updated_at = NOW()
|
||||
WHERE config_key = 'polling.rate_limit_defer_alert_seconds'
|
||||
AND NOT EXISTS (SELECT 1 FROM tb_system_config WHERE config_key = 'polling.gateway.rate_limit_defer_alert_seconds');
|
||||
UPDATE tb_system_config SET config_key = 'polling.stop_resume_record.retention_months', updated_at = NOW()
|
||||
WHERE config_key = 'polling.stop_resume_record_retention_months'
|
||||
AND NOT EXISTS (SELECT 1 FROM tb_system_config WHERE config_key = 'polling.stop_resume_record.retention_months');
|
||||
UPDATE tb_system_config SET config_key = 'polling.stop_resume_record.premake_months', updated_at = NOW()
|
||||
WHERE config_key = 'polling.stop_resume_record_premake_months'
|
||||
AND NOT EXISTS (SELECT 1 FROM tb_system_config WHERE config_key = 'polling.stop_resume_record.premake_months');
|
||||
|
||||
-- 新 Key 已存在时,残留的旧行不再被读取,删除以免后台列表混淆。
|
||||
DELETE FROM tb_system_config WHERE config_key IN (
|
||||
'polling.rate_limit_mode', 'polling.gateway_default_qps', 'polling.rate_limited_retry_delay_seconds',
|
||||
'polling.rate_limit_defer_alert_seconds', 'polling.stop_resume_record_retention_months', 'polling.stop_resume_record_premake_months');
|
||||
@@ -31,7 +31,7 @@
|
||||
1. **限流在每次 HTTP 尝试前执行**,而不是方法入口。方法入口只能扣一次,内部重试会漏计。实现为网关客户端的调用守卫 `gateway.CallGuard`(`BeforeAttempt` / `AfterAttempt`),在 `executeWithRetry` 每次尝试前、现有 `AttemptObserver` 之前调用;卡操作类型由 5 个卡方法写入 ctx。没有复用 ctx 里的 `AttemptObserver`,因为 ctx 只能放一个观察器,会与停复机、设备的审计观察器互相覆盖。限流拒绝发生在审计观察器之前,因此被拒绝的尝试不写集成日志。
|
||||
2. **滑动窗口 Lua(ZSET 毫秒时间戳)**,key `gateway:qps:{cid}:{group}`;一次脚本同时校验账户组与全局组、普通操作与预留额度、`high_waiting`。备选令牌桶:突发更平滑但"本秒剩余"口径与后续分发器领取不一致,放弃。
|
||||
3. **默认限流、显式豁免**:ctx 中只有豁免标记(带来源),没有"受限流"标记。备选"带标记才限流":新入口忘记打标记就漏网,与"不存在不限流的自动调用"冲突,放弃。
|
||||
4. **两种模式全局开关**:`polling.rate_limit_mode` 放受控系统配置(`tb_system_config`,`module = polling`),不按账户覆盖(不灰度,见方案第三部分第 9 节)。API 与 Worker 各自用独立注册表读取同一份配置数据,进程内缓存 5 秒,后台修改数秒内生效。
|
||||
4. **两种模式全局开关**:`polling.gateway.rate_limit_mode` 放受控系统配置(`tb_system_config`,`module = polling`),不按账户覆盖(不灰度,见方案第三部分第 9 节)。API 与 Worker 各自用独立注册表读取同一份配置数据,进程内缓存 5 秒,后台修改数秒内生效。
|
||||
- 限流组与错误分类规则同样在进程内缓存 5 秒(`GatewayRateLimitConfigTTL`),后台保存后本进程立即失效、其他进程 5 秒内刷新;未另建 Redis 配置缓存。
|
||||
- 运营商账户口径为本系统 `tb_carrier`(卡上的 `carrier_id`),与网关客户端自身的 appId 无关:已知卡的调用方用 `gateway.WithCarrier` 传入,否则守卫按 ICCID 查本系统卡表并缓存 10 分钟;查不到时按账户 0(未知账户)单独计量。
|
||||
- Redis 故障时限流失败开放(放行并记日志),避免限流组件故障阻断全部网关调用。
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
- 新增网关限流器:按"运营商账户 → 限流组 → 操作"对卡的 5 个网关操作(查流量、查状态、查实名、停机、复机)做每秒请求数限制,滑动窗口,跨 Worker 共享;**按每次实际 HTTP 尝试计量**(挂在 `AttemptObserver.BeforeAttempt`),网关内部重试同样计入。
|
||||
- 默认限流、显式豁免:只有后台人工点击停复机、通道阈值、自动续费、手动刷新 4 个入口打豁免标记,其余卡操作一律受限。
|
||||
- 限流组配置(新表 `tb_carrier_rate_limit_group`):组内操作、`max_qps`、高优先级操作与预留额度、可选全局组;未覆盖或组已停用的操作按默认 QPS,不存在不限流的自动调用。
|
||||
- 两种模式:受控系统配置 `polling.rate_limit_mode`,`observe`(只统计、不拦截,默认)与 `enforce`(拦截),全局生效、不按账户覆盖。上线先 `observe` 采集至少一周,配好全部账户限流组后整体切到 `enforce`。
|
||||
- 两种模式:受控系统配置 `polling.gateway.rate_limit_mode`,`observe`(只统计、不拦截,默认)与 `enforce`(拦截),全局生效、不按账户覆盖。上线先 `observe` 采集至少一周,配好全部账户限流组后整体切到 `enforce`。
|
||||
- 拦截时在 Asynq 任务内**不等待**:拿不到名额立即返回"限流"错误;轮询任务短延迟重入队且不计失败;Asynq 服务配置 `IsFailure` 对"限流"错误返回 false、`RetryDelayFunc` 返回固定短延迟,Outbox 消费者与观测序列因此不会因限流消耗重试次数、推迟数小时或归档。
|
||||
- **BREAKING** `StopCard` / `StartCard` 不再使用网关内部重试(`maxRetries = 0`),对全部停复机入口生效(含后台人工点击):避免超时后几百毫秒内对同一张卡连发 2~3 次撞上游单卡频率限制。
|
||||
- 网关返回结构化错误(错误类别、网关业务码与信息、耗时),不再只拼在错误字符串里。
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
|
||||
### Requirement: 只统计与拦截两种模式
|
||||
|
||||
系统 SHALL 按受控系统配置 `polling.rate_limit_mode` 全局决定限流模式:`observe` 时照常计数(实际 QPS、本应被拦截的次数)但不拦截、不等待;`enforce` 时按限流组拦截。默认值 MUST 为 `observe`。模式修改 MUST 即时生效,不需要重新部署。模式不得按运营商账户覆盖。
|
||||
系统 SHALL 按受控系统配置 `polling.gateway.rate_limit_mode` 全局决定限流模式:`observe` 时照常计数(实际 QPS、本应被拦截的次数)但不拦截、不等待;`enforce` 时按限流组拦截。默认值 MUST 为 `observe`。模式修改 MUST 即时生效,不需要重新部署。模式不得按运营商账户覆盖。
|
||||
|
||||
#### Scenario: 只统计模式下超出上限
|
||||
- **GIVEN** 模式为 `observe`,账户 A 查询组本秒已达上限
|
||||
@@ -67,7 +67,7 @@
|
||||
- **THEN** 系统照常发出请求,并把该次计入"本应被限流"次数
|
||||
|
||||
#### Scenario: 切换为拦截
|
||||
- **WHEN** 超级管理员把 `polling.rate_limit_mode` 改为 `enforce`
|
||||
- **WHEN** 超级管理员把 `polling.gateway.rate_limit_mode` 改为 `enforce`
|
||||
- **THEN** 下一次请求起按限流组拦截,无需重启进程
|
||||
|
||||
### Requirement: Asynq 任务内遇限流不等待且不计失败
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
- [x] 1.1 新增成对迁移:建 `tb_carrier_rate_limit_group`(`carrier_id` 可空表示全局组、`group_code`、`group_name`、`max_qps`、`operations`、`high_priority_operations`、`reserved_qps`、`status`、审计字段);down 删表。
|
||||
- [x] 1.2 新增成对迁移:建 `tb_gateway_error_classification`(关键字、匹配方式、类别、适用操作、优先级、启用状态),写入方案第 6.2 节的初始规则;down 删表。
|
||||
- [x] 1.3 新增成对迁移:建分区表 `tb_card_stop_resume_record`(按 `created_at` 月分区,主键 `(id, created_at)`,DEFAULT 分区,当前月起若干个月分区,索引 `(card_id, created_at)`、`(carrier_id, created_at)`、`(result, created_at)`);down 删表。
|
||||
- [x] 1.4 新增成对迁移:`tb_system_config` 插入 `module = polling` 配置项(`polling.rate_limit_mode` 默认 `observe`、`rate_limited_retry_delay`、限流累计推迟告警上限、默认 QPS、停复机记录保留月数与提前建分区月数);down 删除这些键。
|
||||
- [x] 1.4 新增成对迁移:`tb_system_config` 插入 `module = polling` 配置项(`polling.gateway.rate_limit_mode` 默认 `observe`、`rate_limited_retry_delay`、限流累计推迟告警上限、默认 QPS、停复机记录保留月数与提前建分区月数);down 删除这些键。
|
||||
- [x] 1.5 新增对应 model 与 Store;`internal/infrastructure/systemconfig` 注册上述键(类型、取值范围、枚举),`SystemConfigListRequest.Module` 枚举增加 `polling`;为 JSON 类型注册项增加结构校验接缝。
|
||||
|
||||
## 2. 网关结构化错误与停复机不重试
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
6. **限流等待模式**:NSQ 消费者使用第 1 个 Change 限流器的"等待至超时"模式(不占 Asynq 协程),等待超过单步超时返回限流超时,按短延迟重排不计失败。
|
||||
7. **请求检查取代观测序列**:5 处 `AppendSeriesRequested` 与 3 处读入口分发改为调用"请求检查(卡、类型、场景)";场景表 `tb_polling_check_scene` 预置 scene 行(订单支付、套餐激活、停复机成功、设备操作、自动续费购包、C 端资产详情、后台资产实时状态、实名链接),读入口默认配置冷却。观测序列中与卡检查无关的部分(设备信息同步 `query_device_info`)保持不变。
|
||||
8. **失败告警**:`tb_polling_failure_alert` 部分唯一索引(卡、检查类型、未关闭),告警写入走异步、失败只记日志;通知走现有 `alert_service.go`。停复机方向的告警在第 6 个 Change 接入同一张表。
|
||||
9. **整体切换**:`polling.scheduler_mode` 放受控系统配置;切换由 Worker 监听配置变更执行:停旧调度 → 初始化新结构(按批读库,进度复用 `/polling-stats/init-progress`)→ 开启分发。回退对称。旧 handler 执行前检查模式。
|
||||
9. **整体切换**:`polling.scheduler.mode` 放受控系统配置;切换由 Worker 监听配置变更执行:停旧调度 → 初始化新结构(按批读库,进度复用 `/polling-stats/init-progress`)→ 开启分发。回退对称。旧 handler 执行前检查模式。
|
||||
10. **监控**:队列与执行数读 Redis;QPS、耗时读第 1 个 Change 的分钟计数;告警读告警表。
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
- **BREAKING** 业务事件与读入口改为"请求检查":订单支付、套餐激活、停复机成功、设备操作、自动续费购包,以及 C 端资产详情、后台资产实时状态、实名链接,不再起观测序列;改为把对应检查设为立即到期放入优先队列,同卡多次请求合并,按场景配置延迟确认与冷却时长(新表 `tb_polling_check_scene`)。OpenAPI 查询保持不触发观测。
|
||||
- 优先轮询提示、手动触发队列改为写入账户队列;初始化、生命周期改为操作账户队列与卡到期信息。
|
||||
- 运营商账户调度参数(新表 `tb_carrier_polling_param`):执行上限、每进程 `max_in_flight`、优先占比等;步骤顺序、单步超时、退避、告警阈值放受控系统配置。
|
||||
- 整体切换:受控系统配置 `polling.scheduler_mode`(`legacy` / `carrier`),不按账户灰度;切换时旧任务直接结束、从数据库初始化全部账户新结构后开始领取;可整体回退。
|
||||
- 整体切换:受控系统配置 `polling.scheduler.mode`(`legacy` / `carrier`),不按账户灰度;切换时旧任务直接结束、从数据库初始化全部账户新结构后开始领取;可整体回退。
|
||||
- 监控改为按运营商账户;下线按 ZCARD 背压、旧并发计数与并发配置接口、旧提示通道。
|
||||
|
||||
## Capabilities
|
||||
@@ -31,7 +31,7 @@
|
||||
|
||||
- 依赖:第 1 个 Change(限流器,消费者使用等待模式)、第 3 个 Change(去掉保护期)已上线。
|
||||
- 部署:测试环境与生产新增 nsqd + nsqlookupd;`go.mod` 新增 NSQ 客户端依赖;yaml 增加 NSQ 地址、`msg_timeout`、租约、续期间隔、分发间隔、卡任务总超时、优雅退出宽限期。
|
||||
- 数据库:新增 `tb_carrier_polling_param`、`tb_polling_check_scene`(预置场景)、`tb_polling_failure_alert`;`tb_system_config` 插入步骤与告警参数、`polling.scheduler_mode`;`tb_polling_concurrency_config` 保留不删、表注释标记废弃。
|
||||
- 数据库:新增 `tb_carrier_polling_param`、`tb_polling_check_scene`(预置场景)、`tb_polling_failure_alert`;`tb_system_config` 插入步骤与告警参数、`polling.scheduler.mode`;`tb_polling_concurrency_config` 保留不删、表注释标记废弃。
|
||||
- 代码:重写 `internal/polling/queue_manager.go`、`scheduler.go`、`initializer.go`、`internal/task/polling_*_handler.go`(拆为步骤函数)、`polling_base.go`、`internal/service/polling/monitoring_service.go`;修改 `lifecycle_service.go`、`priority_manual_service.go`、`manual_trigger_service.go`、`polling_priority_claim.go`;业务事件 5 处与读入口 3 处改为请求检查;`cmd/worker/main.go`、`internal/bootstrap/*` 装配分发器与 NSQ 消费者;删除 `concurrency_service.go` 及其接口。
|
||||
- 接口:新增 `/polling-carrier-params`、`/polling-check-scenes`、`/polling-failure-alerts`(列表、汇总、详情、处理)、`/iot-cards/:id/polling-state`;`/polling-stats` 按账户;下线 `/polling-concurrency`。前端新增调度参数、场景、失败告警页,监控页按账户,下线并发配置页。
|
||||
- 行为:业务事件后的检查不再固定三连查,按场景配置;读入口受冷却与限流约束。
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
### Requirement: 按运营商账户队列调度且不丢卡
|
||||
|
||||
系统 SHALL 在 `polling.scheduler_mode = carrier` 时,为每个运营商账户维护优先队列与普通队列,每张参与轮询的卡在其所属账户中 MUST 只占一个成员(优先标记决定在哪个队列),score 为该卡各检查类型中最早的到期时间。领取时系统 MUST 写带租约的处理中标记(写成功才算领取到)并把 score 推后到租约到期时间,卡不移出队列;处理结束(成功或失败)由收尾按派发 token 校验后删除处理中标记、写回各检查类型到期时间与失败计数,并把 score 设为下次最早到期时间。处理中崩溃时,租约到期后该卡 MUST 能被重新领取。优先提示、手动触发、生命周期事件 MUST 只修改到期时间与 score,不得新增重复成员。
|
||||
系统 SHALL 在 `polling.scheduler.mode = carrier` 时,为每个运营商账户维护优先队列与普通队列,每张参与轮询的卡在其所属账户中 MUST 只占一个成员(优先标记决定在哪个队列),score 为该卡各检查类型中最早的到期时间。领取时系统 MUST 写带租约的处理中标记(写成功才算领取到)并把 score 推后到租约到期时间,卡不移出队列;处理结束(成功或失败)由收尾按派发 token 校验后删除处理中标记、写回各检查类型到期时间与失败计数,并把 score 设为下次最早到期时间。处理中崩溃时,租约到期后该卡 MUST 能被重新领取。优先提示、手动触发、生命周期事件 MUST 只修改到期时间与 score,不得新增重复成员。
|
||||
|
||||
#### Scenario: 执行中进程崩溃
|
||||
- **WHEN** 卡任务执行到一半 Worker 崩溃
|
||||
@@ -105,7 +105,7 @@
|
||||
|
||||
### Requirement: 整体切换与回退
|
||||
|
||||
系统 SHALL 由受控系统配置 `polling.scheduler_mode`(`legacy` / `carrier`)全局决定卡轮询使用旧分片调度还是新调度,MUST NOT 按运营商账户灰度,任一时刻只有一套调度在领取与执行。切到 `carrier` 时:旧调度停止出队,旧 Asynq 轮询任务执行前检查模式并直接结束;系统从数据库按批初始化全部账户新结构,初始化完成前新分发器不领取。回到 `legacy` 时:新分发器停止领取,执行中任务按收尾规则完成,系统重新初始化旧分片队列后旧调度恢复。所有写入轮询结构的入口按当前模式写新或旧结构。
|
||||
系统 SHALL 由受控系统配置 `polling.scheduler.mode`(`legacy` / `carrier`)全局决定卡轮询使用旧分片调度还是新调度,MUST NOT 按运营商账户灰度,任一时刻只有一套调度在领取与执行。切到 `carrier` 时:旧调度停止出队,旧 Asynq 轮询任务执行前检查模式并直接结束;系统从数据库按批初始化全部账户新结构,初始化完成前新分发器不领取。回到 `legacy` 时:新分发器停止领取,执行中任务按收尾规则完成,系统重新初始化旧分片队列后旧调度恢复。所有写入轮询结构的入口按当前模式写新或旧结构。
|
||||
|
||||
#### Scenario: 切换期间旧任务
|
||||
- **WHEN** 切到 `carrier` 时 Asynq 中仍有排队的旧轮询任务
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
- [ ] 1.1 新增成对迁移:`tb_carrier_polling_param`(`carrier_id` 唯一、`inflight_limit`、`card_max_in_flight`、`stop_resume_max_in_flight`、`sr_inflight_limit`、`priority_max_ratio`、`stop_resume_min_interval`、`stop_resume_confirm_delay`、审计字段)。
|
||||
- [ ] 1.2 新增成对迁移:`tb_polling_check_scene`(`scene` 唯一、`check_types`、`confirm_delays`、`cooldown`),预置 8 个场景,读入口场景带默认冷却。
|
||||
- [ ] 1.3 新增成对迁移:`tb_polling_failure_alert`(卡、运营商账户、检查类型、连续失败、首次与最近失败、最近错误、状态、处理人与时间;部分唯一索引 `(card_id, check_type) WHERE status = 未处理`)。
|
||||
- [ ] 1.4 新增成对迁移:`tb_system_config` 插入 `polling.scheduler_mode`(默认 `legacy`)、步骤顺序、单步超时、卡任务总超时余量、退避序列、告警阈值、账户级告警窗口与阈值;`tb_polling_concurrency_config` 表注释标记废弃。
|
||||
- [ ] 1.4 新增成对迁移:`tb_system_config` 插入 `polling.scheduler.mode`(默认 `legacy`)、步骤顺序、单步超时、卡任务总超时余量、退避序列、告警阈值、账户级告警窗口与阈值;`tb_polling_concurrency_config` 表注释标记废弃。
|
||||
- [ ] 1.5 `pkg/config` 与 `defaults/config.yaml`:NSQ 地址、`msg_timeout`、租约、续期间隔、分发间隔、优雅退出宽限期、默认执行上限与 `max_in_flight`;`go.mod` 引入 NSQ 客户端。
|
||||
- [ ] 1.6 注册受控系统配置项并实现 JSON 结构校验(步骤类型合法且不重复、退避序列递增)。
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
|
||||
## 8. 整体切换
|
||||
|
||||
- [ ] 8.1 Worker 监听 `polling.scheduler_mode`:切 `carrier` 时停旧调度出队、初始化新结构后开启分发;切 `legacy` 时停止领取、等待执行中任务收尾、重建旧分片队列后恢复旧调度。
|
||||
- [ ] 8.1 Worker 监听 `polling.scheduler.mode`:切 `carrier` 时停旧调度出队、初始化新结构后开启分发;切 `legacy` 时停止领取、等待执行中任务收尾、重建旧分片队列后恢复旧调度。
|
||||
- [ ] 8.2 旧轮询 handler 执行前检查模式,为 `carrier` 时直接结束。
|
||||
- [ ] 8.3 所有写入轮询结构的入口按当前模式写新或旧结构。
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
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 内,同样改为请求评估)。
|
||||
7. **全局开关**:`polling.stop_resume.queue_enabled` 受控系统配置;所有入口通过一个"请求评估"门面按开关决定入队还是旧路径当场执行,避免分散判断。停复机与设备判断的分发不受 `scheduler_mode` 限制:开关开启后对全部账户领取(`scheduler_mode = legacy` 时套餐步骤仍在旧 handler 内,同样改为请求评估)。
|
||||
8. **退款完成语义**:请求写入成功即标记 `asset_reset`,补偿扫描只覆盖"请求未写入"的情况,不再因停机失败重放。
|
||||
9. **到期即停入口**:`package_activation_handler.go` 到期失效处理处判断无其他有效主套餐后请求评估(场景"套餐到期");接续激活路径不变。
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
- **BREAKING** 退款后资产处理不再调用 `ManualStopCard`:改为请求评估,请求写入成功即视为资产后处理完成,停机失败由指令队列退避与告警处理,不再触发 Outbox 重投与补偿扫描。
|
||||
- **BREAKING** 激活、重置、订单后的复机去掉裸 goroutine,改为事务提交后请求评估。
|
||||
- 运营商账户实名管控修改后对受影响的卡批量请求评估(0 → 1 在线未实名,1 → 0 停机未实名),按账户 QPS 平滑执行。
|
||||
- 全局开关 `polling.stop_resume_queue_enabled`(受控系统配置,默认关闭):开启后所有账户的自动停复机经指令队列,不按账户灰度;关闭即回退为各入口当场执行。
|
||||
- 全局开关 `polling.stop_resume.queue_enabled`(受控系统配置,默认关闭):开启后所有账户的自动停复机经指令队列,不按账户灰度;关闭即回退为各入口当场执行。
|
||||
- 停复机方向的失败接入失败告警表;监控增加停复机成功、失败、重试与指令队列长度。
|
||||
|
||||
## Capabilities
|
||||
@@ -29,7 +29,7 @@
|
||||
## Impact
|
||||
|
||||
- 依赖:第 1 个(限流、错误分类、停复机记录)、第 4 个(新规则转正)、第 5 个(账户调度、NSQ、失败告警表)Change 已上线。
|
||||
- 数据库:`tb_system_config` 插入 `polling.stop_resume_queue_enabled` 与结果未知告警阈值;新的成对迁移。
|
||||
- 数据库:`tb_system_config` 插入 `polling.stop_resume.queue_enabled` 与结果未知告警阈值;新的成对迁移。
|
||||
- 代码:`internal/service/iot_card/stop_resume_service.go`(`EvaluateAndAct` 只判断、生成指令;抽出执行部分)、`stop_resume_unified.go`;`internal/infrastructure/cardobservation/event.go`;`internal/service/package/activation_service.go`、`reset_service.go`、`internal/service/order/service.go`;`internal/service/refund/service.go` `stopAsset` 与 `internal/infrastructure/commissiondelivery/event.go` 补偿扫描;`internal/task/package_activation_handler.go`(到期即停);`internal/service/carrier/service.go`;分发器与 NSQ 消费者新增停复机与设备判断;`RedisPollingDeviceOpLockKey` 轮询路径替换。
|
||||
- 接口:调度参数中的停复机参数生效;`/polling-failure-alerts` 包含停复机方向;`/iot-cards/:id/polling-state` 返回当前指令;监控增加停复机指标。
|
||||
- 行为:自动停复机比现在晚约 1~2 秒;后台人工点击、通道阈值、自动续费的停复机仍直接执行。
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
|
||||
### Requirement: 自动停复机统一经指令队列
|
||||
|
||||
在 `polling.stop_resume_queue_enabled` 开启时,系统 SHALL 让套餐步骤、Outbox 卡观测消费者(流量增加、网络状态变化、实名变化)、套餐激活、流量重置、订单支付后、退款后资产处理、套餐到期、运营商账户实名管控修改这些自动入口只请求评估:按统一判断函数(暂停轮询 → 停机条件 → 复机条件 → 通道阈值锁)得出目标状态,需要动作时生成停复机指令并放入卡所属运营商账户的停复机队列,MUST NOT 在入口当场调用网关。请求评估本身只写共享缓存;写入失败 MUST 记日志,并由该卡下一次套餐步骤兜底。后台人工点击、通道阈值、自动续费发起的停复机 MUST 照旧直接执行。开关关闭时各入口 MUST 恢复当场执行;队列中未执行的指令保留,再次开启后继续执行,执行前重新判断。开关 MUST 全局生效,不按运营商账户灰度。
|
||||
在 `polling.stop_resume.queue_enabled` 开启时,系统 SHALL 让套餐步骤、Outbox 卡观测消费者(流量增加、网络状态变化、实名变化)、套餐激活、流量重置、订单支付后、退款后资产处理、套餐到期、运营商账户实名管控修改这些自动入口只请求评估:按统一判断函数(暂停轮询 → 停机条件 → 复机条件 → 通道阈值锁)得出目标状态,需要动作时生成停复机指令并放入卡所属运营商账户的停复机队列,MUST NOT 在入口当场调用网关。请求评估本身只写共享缓存;写入失败 MUST 记日志,并由该卡下一次套餐步骤兜底。后台人工点击、通道阈值、自动续费发起的停复机 MUST 照旧直接执行。开关关闭时各入口 MUST 恢复当场执行;队列中未执行的指令保留,再次开启后继续执行,执行前重新判断。开关 MUST 全局生效,不按运营商账户灰度。
|
||||
|
||||
#### Scenario: 订单支付后复机
|
||||
- **WHEN** 客户支付后套餐生效,卡处于停机且满足复机条件
|
||||
- **THEN** 订单事务提交后系统生成复机指令,由指令执行器调用上游,支付流程不等待也不起 goroutine
|
||||
|
||||
#### Scenario: 开关关闭
|
||||
- **WHEN** 超级管理员关闭 `polling.stop_resume_queue_enabled`
|
||||
- **WHEN** 超级管理员关闭 `polling.stop_resume.queue_enabled`
|
||||
- **THEN** 各自动入口恢复当场执行,已在队列中的指令保留不删
|
||||
|
||||
### Requirement: 同卡一条指令与版本号
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
## 1. 迁移与配置
|
||||
|
||||
- [ ] 1.1 新增成对迁移:`tb_system_config` 插入 `polling.stop_resume_queue_enabled`(默认 false)、结果未知告警阈值、停复机退避序列与告警阈值;注册受控系统配置项。
|
||||
- [ ] 1.1 新增成对迁移:`tb_system_config` 插入 `polling.stop_resume.queue_enabled`(默认 false)、结果未知告警阈值、停复机退避序列与告警阈值;注册受控系统配置项。
|
||||
|
||||
## 2. 统一判断与请求评估
|
||||
|
||||
|
||||
@@ -129,7 +129,7 @@ const (
|
||||
RateLimitExemptManualRefresh = "manual_refresh"
|
||||
)
|
||||
|
||||
// 限流模式(polling.rate_limit_mode)。
|
||||
// 限流模式(polling.gateway.rate_limit_mode)。
|
||||
const (
|
||||
// RateLimitModeObserve 只统计不拦截。
|
||||
RateLimitModeObserve = "observe"
|
||||
@@ -142,17 +142,17 @@ const (
|
||||
// SystemConfigModulePolling 是轮询与网关限流配置模块。
|
||||
SystemConfigModulePolling = "polling"
|
||||
// SystemConfigPollingRateLimitMode 网关限流模式。
|
||||
SystemConfigPollingRateLimitMode = "polling.rate_limit_mode"
|
||||
SystemConfigPollingRateLimitMode = "polling.gateway.rate_limit_mode"
|
||||
// SystemConfigPollingGatewayDefaultQPS 默认每秒请求上限。
|
||||
SystemConfigPollingGatewayDefaultQPS = "polling.gateway_default_qps"
|
||||
SystemConfigPollingGatewayDefaultQPS = "polling.gateway.default_qps"
|
||||
// SystemConfigPollingRateLimitedRetryDelay Asynq 任务遇限流后的固定重试延迟(秒)。
|
||||
SystemConfigPollingRateLimitedRetryDelay = "polling.rate_limited_retry_delay_seconds"
|
||||
SystemConfigPollingRateLimitedRetryDelay = "polling.gateway.rate_limited_retry_delay_seconds"
|
||||
// SystemConfigPollingRateLimitDeferAlert 限流累计推迟告警阈值(秒)。
|
||||
SystemConfigPollingRateLimitDeferAlert = "polling.rate_limit_defer_alert_seconds"
|
||||
SystemConfigPollingRateLimitDeferAlert = "polling.gateway.rate_limit_defer_alert_seconds"
|
||||
// SystemConfigPollingStopResumeRecordRetention 停复机记录保留月数。
|
||||
SystemConfigPollingStopResumeRecordRetention = "polling.stop_resume_record_retention_months"
|
||||
SystemConfigPollingStopResumeRecordRetention = "polling.stop_resume_record.retention_months"
|
||||
// SystemConfigPollingStopResumeRecordPremake 停复机记录提前建分区月数。
|
||||
SystemConfigPollingStopResumeRecordPremake = "polling.stop_resume_record_premake_months"
|
||||
SystemConfigPollingStopResumeRecordPremake = "polling.stop_resume_record.premake_months"
|
||||
)
|
||||
|
||||
// TaskTypeStopResumeRecordPartition 停复机记录分区维护定时任务(每日执行,使用数据清理队列)。
|
||||
|
||||
Reference in New Issue
Block a user