Files
junhong_cmp_fiber/docs/verification/add-priority-polling-queue-verification.md
break aab56a6998
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 14m13s
feat(轮询优先队列): AUG26-016 卡轮询优先队列、人工入队与读侧接口,归档并同步主 Spec 与证据矩阵
新增 000228 成对迁移 tb_polling_priority_item:卡、任务类型、状态、触发类型、来源订单/套餐使用记录、
触发次数与来源集合、尝试次数、失败原因、人工原因与操作者、店铺快照与各时间列;以活动项部分唯一索引
uq_polling_priority_item_active(仅 deleted_at IS NULL AND status IN ('pending','processing') 占键位)
表达「同卡同任务类型至多一条活动项」,另有状态/时间索引与全列注释;down 守卫在存在活动项或未终态行时
拒绝回滚并给出中文原因。

新增优先轮询请求可靠事件 polling.priority.requested(载荷版本 v1、事件键前缀 prio:)与消费者:只在原
业务事务内追加、幂等键稳定;消费者按卡 × 纳入任务类型(realname/carddata/card_status/package)逐条
建项并在提交后下发执行提示,重复投递只合并触发次数、来源集合与最近触发时间,不新建行也不重复调用。
触发点为四类自动场景 purchase_activated / renewal_activated(按同载体更早套餐使用记录判定)/
queue_activated / addon_activated 与「无有效套餐」no_valid_package(仅在普通套餐轮询来源且存在待生效
套餐使用记录时追加;事件通道显式拒绝 manual_trigger);入队对象恒为卡,绑定设备资产在触发事务内冻结
在用卡快照逐卡建项,不使用设备当前卡槽口径。

轮询共享基类新增认领接缝:四个 Handler(realname/carddata/card_status/package)在并发信号量之后、调用
上游之前探测活动项——待执行条件认领、执行中且 90 秒租约未到期则跳过并延后、无活动项时行为与既有完全
等价;超租约允许相邻执行接管,尝试次数只在真正发起执行后累加,未达上限(3)回到活动态按既有间隔重排,
达上限或业务校验类失败进入失败终态并保留可安全展示原因;执行前校验卡自身与绑定设备的轮询开关。未引入
通用卡级锁与 Redis 活动标记,分片队列的出队、入队与移除路径未改动。

提示通道按任务类型独立键(polling:priority:{taskType}),与既有手动触发队列分离;调度器在同一周期内先
排空优先提示、再排空手动触发队列,提示排空不受分片背压跳过影响;未新建调度设施或异步任务类型。

新增人工优先入队与只读查询三条路由 POST /api/admin/polling-priority-items、
GET /api/admin/polling-priority-items、GET /api/admin/polling-priority-items/:id:人工入队复用既有轮询
权限判定(抽取为同包共享函数),原因必填,不受每日 500 次上限与 24 小时去重约束,重复抑制由活动项合并
承担;读侧按店铺快照下推数据范围,越权与不存在不可区分,不提供优先级分级、有效期或人工重触发入口。
新增 7 个审计动作(enqueue/claim/fail/retry/complete/dequeue/manual_denied)与资源
polling_priority_item,并按(操作者类型,来源)注册,人工侧与 Worker 侧均通过来源校验。

同步 OpenAPI 文档装配三处与路由注册;归档 Change 至
openspec/changes/archive/2026-09-17-add-priority-polling-queue/ 并同步主 Spec(新增
priority-polling-queue、polling-operations 追加单次执行互斥 Requirement 与三条路由索引)与上下文健康
证据(requirement-evidence 150 行、入口矩阵 http 403 / async 56)。

本机验证:junhong_cmp_test 与隔离 Redis DB 15,未连生产、未启动 Worker/API、未调用运营商上游;迁移
up/down/up 与 down 守卫实测(含 dirty=true 记账口径与 force 恢复),A–F 批 94 PASS、接缝 63 PASS、
提示通道 12 PASS、清理零残留 20 PASS。成功路径 Complete、真并发互斥、尝试上限第 3 次判定、HTTP 层权限
矩阵、通道阈值持锁复机边界与三类生效触发点生产集成留待测试部署验证(见
docs/verification/add-priority-polling-queue-verification.md 第 4 节)。自动化测试按项目决策为 N/A,
未新增 *_test.go。
2026-09-17 14:29:56 +08:00

20 KiB
Raw Blame History

AUG26-016 卡轮询优先队列add-priority-polling-queue实施与验证记录

本文件记录 OpenSpec Change add-priority-polling-queue 的实施范围、已实测证据与未验证清单。 所有实测均在测试库 junhong_cmp_test隔离 Redis DB 15 上执行;未连接生产、未启动 Worker/API 进程、未调用运营商上游接口。

0. 结论概览

验证项 断言计数 结论
9.1 迁移 up/down/up + down 守卫 结构断言 4 组 + 探针 6 条 通过(含记账 dirty=true 的实测结论)
9.29.7 本机可安全验证部分AF + H5 批) 94 PASS / 0 FAIL 通过
接缝执行(认领/跳过/接管/范围校验,非导出接缝经真实 Handle 驱动) 63 PASS / 0 FAIL 通过
提示通道优先键与手动键、FIFO、RemoveFromAllQueues 边界、分片队列快照) 12 PASS / 0 FAIL 通过
清理与零残留PG 六类计数 + Redis 键 + 越界 DB 只读复核) 20 PASS / 0 FAIL 通过
成功路径 Complete、真并发、尝试上限第 3 次判定、package handler 接缝、HTTP 层、通道阈值持锁、三类生效触发点生产集成 未验证(见第 4 节)

1. 环境与命令

  • 目标库:junhong_cmp_testcxd.whcxd.cn:16159);迁移经 scripts/migrate.sh(读取 .envDB_*)。
  • 隔离 Rediscxd.whcxd.cn:16299 DB 15(部署库为 DB 6、本地沙盒为 DB 7二者对所有写入被显式拒绝
  • 环境装载:set -a; . ./.env; . ./.env.local; set +aDB_* 只在 .envRedis 参数只在 .env.local)。
  • 构建:go build -o /tmp/pf ./cmd/priority-fixture-tmp
  • 只读诊断dbhub MCP mcp__postgres_execute_sql_main(仅 SELECTRedis 只读用 redis-cli -n <db> EXISTS|DBSIZE|SCAN
  • 临时工具子命令:setup|a-store|b-consumer|c-trigger|d-read|e-enqueue|f-audit|g-h5|seam-verify|hint-verify|purge-stale-audits|cleanup|count|insert-fixture|delete-fixture|count-rows|index-probe。 该工具带有安全栏:DB_NAME 必须为 junhong_cmp_test,所有自建行带 AUG26016 标记,清理只按本批 ID 集合或标记删除。
  • 日志:每场景独立带时间戳文件 run<N>-<步骤>-<TS>.log(不覆盖);本文只摘录关键原始行。

2. 已验证清单(含点时间与期望/实际)

2.1 任务 9.1:迁移 up / down / up 与 down 守卫2026-09-16

步骤 期望 实际(原文摘录)
迁移前版本 227 当前迁移版本: 227
./scripts/migrate.sh up 应用到 228exit=0 228/u add_polling_priority_queue (344.53875ms)✓ 迁移操作完成
结构 23 列全注释、5 索引含活动项部分唯一索引、9 CHECK、0 外键 column_count=23commented_columns=23index_count=5c:9+p:1fk_count=0
活动项索引谓词 deleted_at IS NULL AND status IN ('pending','processing') 占键位 CREATE UNIQUE INDEX uq_polling_priority_item_active ... WHERE ((deleted_at IS NULL) AND ((status)::text = ANY ((ARRAY['pending','processing'])::text[])))
造 1 条活动 fixture 后 ./scripts/migrate.sh down 守卫拒绝回滚且结构零损伤 pq: 存在卡轮询优先队列活动项,拒绝回滚以避免丢失在途加急事实与认领租约状态down_exit=1;随后 table_regclass=tb_polling_priority_itemrow_count=1column_count=23index_count=5
守卫后的记账标记 —(实测为工具行为) {"version":"227","dirty":true}
记账修复 force <真实版本> 只改一行、不动 DDL ./scripts/migrate.sh force 228{"version":"228","dirty":false}
清 fixture 后 down 表/索引/relation 全部消失exit=0 228/d add_polling_priority_queue (221.332417ms){"table_regclass":null,"index_count":"0","relation_leftovers":"0"}
再次 up 回到 228 且结构完整 {"version":"228","dirty":false,"column_count":"23","index_count":"5","check_count":"9","fk_count":"0"}

实测结论(dirty=true 的口径)golang-migrate 在运行某个迁移文件之前先写入目标版本并标 dirty=true,成功后写回 dirty=falsedown 228 的目标版本是 227守卫 RAISE EXCEPTION 后库被留在 227, dirty=true。 这是迁移工具的记账行为,不是本 Change 的实现缺陷;恢复方式是 ./scripts/migrate.sh force <当前真实版本>(本例 228该命令只修一行记账、不触发 DDL 或数据变更。验证 down 守卫的正确预期应写成「守卫拒绝 + 结构零损伤 + 由维护者 force 回当前真实版本并确认 dirty=false」。

2.2 部分唯一索引语义与约束探针2026-09-16

[成功] P1 插入活动行 Aid=1 card_id=6012 task_type=carddata status=pending
[报错] P2 重复活动行(同卡同任务类型) → SQLSTATE=23505 constraint=uq_polling_priority_item_active
[成功] P3 软删 A 后插入活动行 Bid=3→ 软删行不占用活动项键位
[报错] P4 插入活动行 CB 仍活动) → SQLSTATE=23505 constraint=uq_polling_priority_item_active
[成功] P5 把 B 转终态completed/success后插入活动行 Did=5→ 终态行不占用活动项键位
[报错] P6 终态与结果一致性completed 但 result 为空) → SQLSTATE=23514 constraint=ck_polling_priority_item_terminal_result
索引与约束探针全部符合预期           exit=0

2.3 AF 批次Store / 消费者 / 触发分类 / 读侧 / 人工入队 / 审计——94 PASS / 0 FAIL

时间2026-09-17 09:46:49+08日志 run5-<场景>-20260917T094649.log(另有一轮同结果运行 run-e2e-*-20260917T092447.log)。

场景 断言 期望 → 实际(摘录)
A Store 生命周期 20 PASS A1 首次认领命中…→ status=processing claimed_at=2026-09-17 09:24:49+08A2 超租约(120s>90s)允许接管 → taken=trueA2 租约内(10s<90s)不允许接管 → taken=falseA3 两次 IncrementAttempt 后 attempt_count → 期望=2 实际=2A3 MarkFailed未累加尝试次数等同 FailFinal 路径)→ attempt_count=0A3 尝试上限路径 → attempt_count=1A4 分页归一化 Page=0/PageSize=999 → total=29 returned=20
B 消费者 15 PASS B5 设备载体在事务内冻结的绑定卡数 → 3B5 活动项行数3 卡 × 4 任务类型) → 12B6 重复投递只合并:行数不变、触发次数+1、最近触发时间刷新、触发类型集合去重B7 自动no_valid_package+ 人工manual_trigger合并为同一行B8 已终态后重复追加同一 event_idoutbox 仍只 1 行B9 同一 event_id 重投不产生重复审计行EventID 去重) → 1
C 触发分类与前置条件 13 PASS C10 → renewal_activated / purchase_activatedC11 存在 status=0 → HasPendingPackageUsage=trueC12 观测抑制为真 → 不写事件0 行)C12 无有效套餐 event_id ≤ 48 且主体与载体可区分 → prio:nvp:d:990016999:… len=31
D 读侧权限与范围 12 PASS D13 超管/平台 → total=18/18D13 代理 → total=16(平台卡与范围外卡各 1 行不可见);D13 企业 → code=1005D14 详情越权 vs 不存在 → 同一码与文案
E 人工入队用例 14 PASS E15 一次入队覆盖全部纳入类型 → created=4 merged=0E16 24h 内再次入队 → created=0 merged=4E17 原因缺失/超长 → CodeInvalidParamE17 拒绝审计 → 5 条 manual_deniedE17 被拒绝的请求不产生优先项变更 → 仍 4 行活动项
F 审计来源匹配 6 PASS F polling_priority.enqueue/claim/manual_denied 注册 → primary=polling_priority_itemorigins 含 account/admin_api 与 system_task/workerF 消费者审计 Actor=system_task / Source=worker
count残留计数 10 PASS 卡/设备/绑定/套餐使用/优先项/审计/outbox/两类孤儿审计资源 均 = 0,且 本批全部自建行已清理干净

2.4 H5 加固:事件通道拒绝 manual_trigger2026-09-17 09:24:47

internal/infrastructure/prioritypolling/event.goAppendPriorityRequested 在资源类型校验后显式拒绝 trigger_type='manual_trigger'(人工入队由 internal/service/polling/priority_enqueue_service.go 直写事实表,不经事件通道)。

[PASS] H5-1 经事件通道投递 manual_trigger 返回参数错误 → code=1001 err=优先轮询事件不接受人工入队触发类型 manual_trigger人工入队请调用人工入队用例
[PASS] H5-3 该 event_id 在 tb_outbox_event 新增 0 行 → 期望=0 实际=0
[PASS] H5-4 tb_outbox_event 总行数不变(拒绝发生在写入前) → 期望=19453 实际=19453

2.5 接缝执行seam-verify——63 PASS / 0 FAIL

时间2026-09-17 10:16:34+08隔离 Redis DB 15打印 db=15未执行 FlushDB。 驱动方式:不启动 Worker/Asynq/HTTPtask.NewPollingBase + asynq.NewTask 直接调用 PollingRealnameHandler / PollingCarddataHandler / PollingCardStatusHandler 的真实 Handle integration 注入真实仓库(非 nil使「零上游」由真实计数断言证明。 每个任务类型realname / carddata / card_status依次断言 (a)(e)

断言组 期望 → 实际(摘录)
计数器真对照 写入 1 行 tb_integration_log 后按夹具卡统计 +1 → 期望=1 实际=1对照行已删除:计数回到原值 → 期望=0 实际=0
(a) 无活动项 = 基线等价 Handle 正常返回 → err=<nil>不产生优先项行 → 0零上游调用 → 0仍按既有路径重入队(分片 ZSET 命中) → key=polling:shard:0:queue:polling:realname member=11696
(b) pending = 本次领取 claim 审计 +1 → delta=1status=failed claimed_at=2026-09-17 10:16:41+08 attempt=0 reason="流量查询能力未配置"fail+dequeue 各 +1零上游调用 → 0
(c) 租约内 = 跳过且零上游 状态/认领时间/尝试次数/失败原因全部不变 → processingclaimed_at 前后相同claim=0 fail=0 dequeue=0零上游调用 → 0重新入队ZSET 命中)
(d) 超租约 = 接管 claimed_at=2026-09-17 10:16:42+08(距运行 <30s已刷新claim delta=1零上游调用 → 0
(e) 卡不在轮询范围 status=failed reason="卡已不在轮询范围内" attempt=0零上游调用 → 0

收尾(同一进程内):

[清理] 隔离 Redis 已删除本套件使用的 12 个键(未执行 FlushDB
[清理] 审计事实(按本批 ID 集合精确删除):事件=27资源行=27
[清理] 本功能动作码审计保留条数非本批写入未删除0
[清理] 优先项=0Outbox(prio:)=0套餐使用=0绑定=3卡=6设备=1
[PASS] 清理后计数 … 应为 09 条)+ 本批全部自建行已清理干净
结果:全部断言通过

2.6 提示通道hint-verify——12 PASS / 0 FAIL

时间2026-09-17 10:16:34+08隔离 Redis DB 15、未 FlushDB、全程前后 polling:shard:* 快照 {}

[PASS] ① EnqueuePriority 只写优先提示键LLEN → 期望=3 实际=3
[PASS] ① 手动触发键未被 EnqueuePriority 触碰LLEN → 期望=0 实际=0
[PASS] ① EnqueueManual 只写手动触发键LLEN → 期望=1 实际=1
[PASS] ① 优先提示键未被 EnqueueManual 触碰LLEN → 期望=3 实际=3
[PASS] ② RPush 3 元素后 LPopCount 弹出顺序为先进先出 → 期望=[990016101 990016102 990016103] 实际=[990016101 990016102 990016103]
[PASS] ③ 边界登记RemoveFromAllQueues 不清理优先提示键LLEN 保持 1
[PASS] ③ 边界登记RemoveFromAllQueues 不清理手动触发键LLEN 保持 1
[PASS] ④ 本子命令前后 polling:shard:* 键集合与成员完全一致diff 为空) → before={} after={}
[PASS] 收尾 删除本子命令使用的键数 → 2两个键 EXISTS=0未残留

2.7 清理与零残留PG + Redis 只读复核2026-09-17 10:17 起)

指标 运行前 运行后
tb_polling_priority_item 0 0
标记夹具卡 / 设备(AUG26016% 0 / 0 0 / 0
polling_priority.% 审计 0 0
prio: outbox 0 0
孤儿审计资源(polling_priority_item / iot_card+polling_card 0 / 0 0 / 0
schema_migrations 228 / dirty=false 228 / dirty=false
Redis DB 15 polling:* 键数 0 0
Redis DB 15 本套件 12 键 EXISTS 0
Redis DB 6 夹具卡专属键(polling:card:11696traffic:sync:lock:card:11696 0 / 0(无本 Change 痕迹)

库内全量孤儿审计资源另有 88 行,分组为 employee_collection_bill=37employee_collection_application=16employee_collection_application_attempt=16package_traffic_alert=16package_traffic_alert_rule=3—— 全部属于其它能力,本 Change 两类均为 0。

2.8 一次真实失败与其修复(如实记录)

首次接缝运行2026-09-17 10:11:34run6-seam-verify-20260917T101134.log)接缝断言 61/61 通过, 但收尾残留检查 2 条 FAILpolling_priority.* 审计残留 27 条claim 9 / fail 9 / dequeue 9 created_at 2026-09-17T02:11:37Z02:11:49Z。 根因是验证工具自身的记账缺陷:场景逐个任务类型收尾会删除优先项行,而审计清理依赖按优先项 ID 反查,删行后即漏删。 修复(仅临时工具,未放宽任何断言):运行期采集优先项 ID 并在清理时作为额外 ID 集合传入;另加带时间窗护栏的 purge-stale-audits 兜底子命令(必须显式给 AUG26016_PURGE_SINCE,窗口外的行一律拒绝删除并列出)。 修复后重跑 63/63 全绿27 条残留按兜底路径精确删除(事件=27资源行=27删除后残留=0)。

3. 工程门禁收尾复跑脚手架删除后2026-09-17 10:26

命令 原始结果 退出码
gofmt -l . 仅列出 6 个历史未格式化文件:internal/model/dto/package_dto.gointernal/model/order_package_invalidate_task.gointernal/model/personal_customer_device.gointernal/model/personal_customer_iccid.gointernal/model/personal_customer_phone.gointernal/query/h5popup/query.go;本 Change 改动文件(internal/infrastructure/prioritypolling/event.gointernal/task/*internal/polling/*internal/service/*pkg/constants/*migrations/000228_* 等)均未出现 → 无新增未格式化文件 0
go build ./cmd/api ./cmd/worker 无输出(仅 Go 模块缓存 stat 写入诊断,不影响构建) 0
go vet ./... 无诊断输出 0
go run cmd/gendocs/main.go(连续两次) 两次均输出「成功在以下位置生成 OpenAPI 文档」;md5(docs/admin-openapi.yaml) = 696dba47834f93faacba93c745353fdd(两次一致;该文件被 .gitignore 忽略,是本地生成产物) 0 / 0
openspec validate add-priority-polling-queue --strict Change 'add-priority-polling-queue' is valid 0
openspec doctor --json "root": {… "healthy": true, "status": []}"status": [] 0
./scripts/context-health.sh Context 健康检查通过 0

自动化测试按项目决策为 N/A未创建任何 *_test.gocontext-health.sh 亦校验仓库无 *_test.go)。

4. 未验证清单(明确未覆盖,不得据本文推断)

未验证原因
优先项成功路径 Completeprocessing → completed 需真实上游调用成功;本轮所有执行都停在「能力未配置」失败分支,未制造成功上游响应
真并发下的认领互斥 无并发执行环境;本轮以「租约内执行中 → 跳过且零上游」的单线程等价路径覆盖,未做同卡同类型真并发竞态
尝试上限第 3 次判定 需稳定失败的真实上游;本轮止于 FailFinal(尝试次数不累加),未覆盖 FailRetryable 连续 3 次的收敛
package handler 的接缝 Handle 依赖 t.ResultWriter().TaskID(),本地 asynq.Task 会 panic未驱动
HTTP 层权限矩阵 未启动 API权限判定仅覆盖服务层与查询层读侧范围、越权=不存在同一响应),未取得真实状态码/msg
9.7 通道阈值持锁边界 tb_carrier_traffic_threshold_lock 活动锁与真实停复机评估路径;本轮未造锁、未驱动停复机
三类生效触发点的生产集成 新购/续购/加油包/排队顺延的触发分类与事件构造已单测C 场景),但未经真实订单/支付/激活链路端到端验证
queue_activated / addon_activated 正向落库 trigger_type C 场景只对 purchase_activated/renewal_activated 做了正向落库;排队顺延与加油包仅验证了事件构造
调度器排空顺序与分片背压 需 Worker + 既有部署环境;本文只覆盖提示通道的生产/消费语义
迁移 down 守卫的自动化 未引入自动化测试(项目决策 N/A仅为手工实测记录

5. 边界声明(避免误读)

  1. tb_integration_log 全表计数会被在跑的测试部署推高:观测窗口内该表由 5,905,167 增至 5,905,619+452 来源是并发运行的既有测试部署DB 6 心跳/并发/27 卡在跑),不是本套件产生;本套件在该表只插入并删除 1 行对照行(复查 = 0
  2. 「零上游调用」的判据是夹具卡维度tb_integration_log WHERE resource_type='iot_card' AND resource_id='<夹具卡ID>' 每条场景运行前后均为 015 条独立断言),并由 counterControl 对照证明该计数口径能发现写入。 这不等于「整库无上游调用」——测试部署的正常上游调用一直在发生。
  3. 隔离 Redis DB 15 并非本套件独占:其中仍有他人 7 个键(auth:refresh:…auth:user:127:tokensauth:token:482e6fdc-…asynq:queuesasynq:{export:*}×3)。因此验证工具默认不执行 FlushDB 只按清单删除自己使用的键DB 0/6/7 在所有写入路径上被显式拒绝DB 6/7 仅做过 EXISTS/DBSIZE/SCAN 只读探测)。
  4. 成功路径与并发结论不得外推:本轮所有执行都走「能力未配置」分支,因此「零上游」「状态收敛」的结论仅适用于该分支; 成功路径 Complete、真并发互斥、尝试上限收敛见第 4 节未验证清单。

6. 收尾处置

  • 验证脚手架 cmd/priority-fixture-tmp/main.go / verify.go / seam.go / hint.go / purge.go为一次性工具 验证完成后整目录删除rm -rf cmd/priority-fixture-tmpDELETED_EXIT=0。 删除确认:ls cmd/ 只剩 api/audit-coverage/audit-retention-simulate/foundation-check/gendocs/migration-finalize/worker find . -name 'priority-fixture-tmp*' -not -path './.git/*' 无输出(仓库根目录与全仓均无同名二进制); git status --porcelain | grep -i fixture 无输出(工作区已不含该目录)。
  • 测试库 tb_polling_priority_item 与本 Change 动作码审计、prio: outbox、标记夹具均已清零见 2.7 本文引用的逐场景日志保存在 /tmp/aug26-016/(会话级临时目录,非仓库产物)。
  • 未提交、未 push。2026-09-17 归档Change 目录移至 openspec/changes/archive/2026-09-17-add-priority-polling-queue/(含 .openspec.yamldelta spec 已同步进主 Specs新增 openspec/specs/priority-polling-queue/spec.md,并在 openspec/specs/polling-operations/spec.md 追加「优先轮询项与普通轮询的单次执行互斥」与三条优先队列路由);tasks.md 的 9.19.7 按用户口径统一勾选,第 4 节未验证清单不因勾选而改变。