Files
lislgosms/docs/phase-4-send-pipeline-redesign.md
T
2026-09-16 18:27:29 +08:00

31 KiB
Raw Blame History

CMPP 发送链路重新设计方案

更新日期:2026-08-25 状态:已在测试环境完成实施与正价50 TPS验收 适用范围:耐久 Inbox 完成业务校验后,到 Gateway 供应商 Submit、结果回调、回执和计费结算的完整链路

1. 结论

现阶段不应继续在 BullMQ 发送 Worker 内做小幅并发、缓存或微批补丁。已完成的耐久 Inbox、入口幂等、路由/报备查询收敛、开放会话热点移除、无消费者 BullMQ 副本移除和计费锁缩短应保留。

若以后继续追求完整供应商提交 100~500 条/秒,建议作为独立项目,重新设计为四个隔离阶段:

  1. Inbox 业务处理与计费预留;
  2. 批量路由规划与提交事实持久化;
  3. PostgreSQL Submit Outbox 批量发布;
  4. 独立的提交结果、回执、计费结算与任务进度回调。

核心不是把若干 BullMQ Job 临时拼成数组,而是把“应提交什么”先作为 PostgreSQL 事实一次性落库,再由无业务判断的发布器向 Gateway 供给命令。这样才能同时减少数据库往返、消除数据库到 Redis 的崩溃窗口,并隔离供应商提交与回调写入的资源竞争。

2. 原方案中应保留的部分

  • 正价测试使用 325 分而不是 0 单价,暴露了真实计费锁竞争;
  • pg_stat_statements 和固定低基数阶段指标能区分入口、Worker、Gateway 与回调瓶颈;
  • 路由、在线连接和签名报备合并查询,取消最终通道重复报备查询;
  • CMPP 单号码内部任务不再逐消息执行整批状态 GROUP BY
  • 提交事务不再更新 CmppSubmitSession.submitTotal 热点行;
  • 删除没有消费者的 gateway.submit.queue BullMQ 同步副本,保留 Gateway 实际消费的 Redis Stream
  • 账户 advisory lock 移到事务末尾账务段后,平均等待由约 443.963ms 降至约 5.420ms
  • 验收以完整供应商提交、队列排空、消息唯一性和账务恒等式为准,没有用入口 SubmitResp 冒充完整吞吐。

这些改动降低了单消息成本和锁等待,但没有改变发送链的逐消息架构,因此未把完整供应商吞吐提升到目标档位。

3. 原方案存在的问题

3.1 微批位置选错

原 P2 在 BullMQ Worker 已领取单个 Job 后,用 5ms 窗口聚合最多 20 条消息。这个位置太晚:任务领取、调度和并发槽已经发生,批次规模还受 Worker 并发上限约束,无法稳定形成 32/64 条批次。

批内慢消息还会形成最慢项栅栏,使本来可以独立完成的消息互相等待。实测微批完整供应商提交约 22.20 条/秒,未优于对照,证明这不是正确的批处理边界。

3.2 只批量加载,没有批量处理主成本

原微批只合并了消息读取,运营商识别、路由、在线/报备判断、SmsSubmitRecord 创建、消息更新、Stream 发布、结果/回执/计费和任务进度仍逐消息执行。省下一个 WHERE id IN (...) 查询不足以抵消批次协调开销。

3.3 PostgreSQL 与 Redis 仍不是原子边界

当前链路先提交 PostgreSQL,再发布 Redis Stream。进程若在两步之间退出,会留下数据库已进入待提交状态、但 Gateway 未收到命令的窗口。若 Redis 已成功而数据库未记录发布结果,重试又可能重复发布。

Redis pipeline 只能减少网络往返,不能解决跨介质一致性。必须增加 PostgreSQL Outbox,并以稳定 submitId、命令幂等键和 Gateway 去重实现至少一次发布。

3.4 提交供给与回调写入争用资源

供应商 Submit 结果、分片结果、最终回执、下游投递和计费结算会同时写 PostgreSQL。回调越快,数据库写入越密集,越容易抢占发送 Worker 的连接和 CPU。仅提高 Worker 并发只会把等待转移到数据库池。

3.5 实时业务事实不能用长期缓存替代

通道启停、真实连接、签名报备、应用/企业状态、余额和号码频控都可能变化。正确做法是批次内共享只读快照,并在提交事实落库前用数据库条件最终校验;不能建立跨批次长期缓存决定是否发送。

3.6 压测口径和夹具容易误导

入口接收、Inbox 完成、供应商首次 Submit 和最终回执是不同阶段。入口 100 条/秒成功,不代表完整供应商提交达到 100 条/秒。历史待投递回执、重复号码频控、缺失运营商规则,以及模拟器重启后数据库连接状态尚未恢复,都会污染结论。

4. 目标架构

4.1 阶段 AInbox 与业务预留

保留现有耐久 Inbox。业务 Worker 批量领取时继续执行应用、企业、签名、模板、黑名单、日限额、号码频控和正价余额预留。

  • 使用 FOR UPDATE SKIP LOCKED 在短事务内领取 32 条,验证稳定后最多 64 条;
  • 领取事务只更新租约,业务查询和外部操作不放在持锁事务内;
  • 日限、频控和冻结继续保留逐消息稳定业务键及数据库唯一约束;
  • 同企业账户锁只覆盖最终余额与流水写入。

4.2 阶段 B:批量路由规划与提交事实

按应用、运营商、签名组合批量读取候选,在一个短事务内为每条可发送消息持久化:

  • 独立 SmsSubmitRecord
  • 目标通道、submitId 和消息 submit_queued 状态;
  • 一条 GatewaySubmitOutbox 命令事实。

事务提交前,SQL 再次约束应用/企业 active、通道 active、连接 connected、签名报备 approved。任一消息失效只拒绝该消息,不回滚整批其他消息。供应商网络调用、Redis 写入和任务进度聚合都不进入该事务。

4.3 阶段 CPostgreSQL Submit Outbox

建议新增表:

GatewaySubmitOutbox
- id / submitId(唯一)
- messageRecordId / channelId
- payload / schemaVersion
- status: pending | publishing | published | dead
- attemptCount / nextAttemptAt
- leaseOwner / leaseExpiresAt
- streamEntryId / publishedAt / lastError
- createdAt / updatedAt

发布器只做:

  1. FOR UPDATE SKIP LOCKED 原子领取一批 pending 行;
  2. 使用 Redis pipeline XADD,每条命令携带唯一 submitId
  3. 批量回写 published 或可重试失败。

“Redis 已成功、PostgreSQL 回写前崩溃”会产生重复发布,因此 Gateway 必须按 submitId 去重,结果事件继续按 resultEventId 幂等。Outbox 扫描器负责恢复数据库已提交但未发布的命令。

4.4 阶段 D:回调与计费隔离

提交结果、回执和下游投递使用独立回调进程/连接池:

  • 单分片只处理聚合结果,多分片保留逐片事实与最终聚合;
  • accepted 结算复用冻结事实,保持 released/charged 幂等且净余额正确;
  • rejected/timeout 以唯一 retryOfSubmitRecordId 认领组内补发;
  • 最终失败退款使用稳定幂等键;
  • 任务进度从消息事实异步刷新,CMPP 单消息任务继续直接写计数;
  • 回调积压不得阻塞 Outbox 向 Gateway 供给命令。

4.5 连接池预算

角色 建议初始数据库槽 说明
API/入口 1624 保证客户 Submit 和查询接口可用
Inbox/路由规划 1624 批量查询和短事务持久化
结果/回执/计费 1216 吸收供应商回调峰值
Outbox 发布器 48 只领取、发布和回写
运维/迁移保留 不少于 8 健康检查、诊断和恢复

实际值必须结合 PostgreSQL CPU、max_connections、PgBouncer 模式和 pg_stat_statements 复测,不能把表中数字直接当生产配置。

5. 实施步骤

第 0 步:固定基线和测试夹具

固定专用号段、三运营商比例、隔离应用和六供应商账号;每次故障注入后同时确认模拟器 6/6 和数据库六通道 connected;建立 PostgreSQL、运行源码和环境/systemd 恢复资产。

第 1 步:Outbox 迁移与影子写入

只新增表、索引和指标;原 Stream 发布仍生效,同时影子写 Outbox 但不发布;对比每个 submitId 的原命令与影子 payload。回退只需关闭影子写,不删除历史表。

第 2 步:Outbox 发布器影子验证

发布到不连接真实 Gateway 的影子 Stream,验证领取、租约恢复、重复发布、死信和批量大小;做发布前/后崩溃注入,证明可恢复且幂等。

第 3 步:单应用灰度切换

一个隔离应用切换到正式 Outbox;保留旧直接发布回退开关,但同一消息任何时刻只能有一个发布路径;先做正价 smoke 和全部发送拦截/补发回归。

第 4 步:批量路由与持久化

初始批量 32,按应用、运营商、签名共享查询;批量插入 Submit/Outbox,逐消息保留唯一键和失败结果;使用 EXPLAIN (ANALYZE, BUFFERS)pg_stat_statements 验证真实 SQL。

第 5 步:回调与连接池隔离

拆分 Submit/Receipt 回调连接池;集合式处理可安全合并的流水、账单和任务进度,同时保留逐消息幂等键和确定账务顺序。

第 6 步:容量验收

smoke → 20 → 30 → 50 → 100 → 200 → 300 → 500 逐档执行。每档完全排空后再升档;只有完整供应商提交达到目标、无丢重、账务一致且数据库稳定才继续。

6. 功能开关与回退

建议提供:

  • SEND_SUBMIT_OUTBOX_SHADOW_ENABLED
  • SEND_SUBMIT_OUTBOX_PUBLISH_ENABLED
  • SEND_ROUTE_BATCH_ENABLED
  • SEND_ROUTE_BATCH_SIZE
  • SEND_CALLBACK_POOL_ENABLED

回退时先停止新领取,等待在途事务完成,关闭正式 Outbox 发布并恢复旧直接发布;按 submitId 对账 Outbox 与 Stream PEL,不能删除 Outbox 行或清空 Redis 来回退。

7. 验收标准

  • 客户 SubmitResp 零丢失,MessageId 与业务号码唯一;
  • Inbox、Submit Outbox、Gateway/结果 Stream 和 BullMQ 最终排空;
  • 每条消息最多一个有效首次 Submit,补发必须关联原提交;
  • 供应商完整提交速率达到该档目标,不只看入口速率;
  • 签名、模板、余额、应用/企业状态、号码频次、报备和通道停用继续实时拦截;
  • 冻结、释放、扣费、退款和 SmsBillingRecord 净额一致;
  • 无持续锁等待、无 idle in transaction、连接数不超过预算;
  • 服务重启和 Outbox 重放不重复提交、不重复计费。

8. 风险与工作量判断

这是跨 Prisma migration、发送状态机、Gateway 命令协议、回调处理、部署配置和压测工具的架构改造,不适合作为当前阶段的继续小修。主要风险是双发布、Outbox 重放重复提交、补发认领冲突、实时通道状态过期和账务顺序错误。

建议以后单独立项,先完成 Outbox 影子对账和崩溃恢复,再进入批量路由。没有完成影子验证前,不应直接替换现有正式发布路径。

9. 2026-08-25 完整实施结果

  • GatewaySubmitOutbox、独立发布器和正式发布路径已完成;发送Worker进一步实现最大32条、3ms聚合窗的有界批次。批内一次加载消息、号段、路由/连接及签名报备事实,按通道执行限速,再以一个短事务createMany写Submit、集合式更新消息并createMany写Outbox。重试及异常分支保留逐消息状态机和幂等键。
  • Submit结果、分片结果、回执、上行、受限协议日志和死信改由仅绑定127.0.0.1:3001cmpp-gateway-callback进程接收,使用独立12槽PostgreSQL连接池;指标绑定127.0.0.1:9468。客户HTTP Webhook仍由主API的独立BullMQ Worker处理,避免慢客户回调占用Gateway事实回写池。连接状态控制仍写主API,供应商事件才写回调进程。
  • 初次发布smoke暴露并修复了回调URL职责过宽问题:连接状态误发到回调端点导致数据库显示connecting、路由失败。修复为控制面/事件面双URL后,六连接均为connected;该失败样本未发生供应商提交或计费,不纳入性能结果。
  • 修复后正价smoke为9/9受理,账单9×325=292520 TPS为199/199、账单6467530 TPS为299/299、账单9717550 TPS为499/499、账单162175。四个成功窗口共1006条,账单1006笔、单价均325、合计326950。
  • 50 TPS档499条非补发首次供应商Submit覆盖9.930秒,即50.25条/秒;相对同日改造前Outbox阶段的33.07条/秒提高约52%。该档客户端P50/P95/P99为35/76/135ms;首提499条唯一,全部尝试及Outbox各542条唯一,补发均关联原提交。
  • 压测结束Inbox、Submit Outbox和两条Redis Stream全部排空,数据库无重复Submit ID、无等待锁、无idle in transaction;回调池max=12,total=1,idle=1,waiting=0。隔离应用单价已恢复0,三条临时运营商规则已删除,真实账务事实保留审计。100/200/300/500未继续执行,当前验收结论限定为50 TPS档通过。

10. 长短信分段回执并发整改方案(2026-09-16,待实施)

10.1 目的、证据和适用范围

业务解释:一条长短信有多段回执,多个处理流程同时发现整条短信已经有结果,重复安排通知或补发。现有数据库唯一键能阻止重复创建,但不能避免此前重复查询、选路和事务开销。整改目标是“先认领处理资格,再做业务动作;中途退出后可以安全接续”。

依据:预生产只读诊断。2026-09-16 09:2009:4012682次下游回执唯一键冲突关联6335条长短信;1862次补发唯一键冲突关联1859个长短信原始提交。下游冲突关联消息的13373条Inbox均matched且attemptCount=1;补发6组抽样显示不同分段失败回执同时到达并争抢创建补发。核心回执、补发、提交文件在d13ca07至cbc4a03之间无变化。

缺陷有两层:单个receiptKey去重不能覆盖不同分段;补发“先查询、后选路、再创建”的流程存在并发窗口。CPU峰值包含I/O等待,缺少业务进程历史CPU与慢SQL,不能承诺消除此缺陷即可消除全部CPU高峰。

本节是现有发送链路的专项补充,作为本次并发整改的设计依据;细化第4节结果处理及第7节不重不漏验收要求,不替代路由、计费、长短信协议或历史容量结论。不扩展到批次编号冲突、通道容量改造或历史短信重发。用户本轮仅要求方案文档,以下模型、状态、参数均是拟实施设计。

10.2 必须保持的业务规则

  1. 原始回执逐事件持久化,同一事件幂等;不同分段的合法回执不能当成重复而丢弃。
  2. 成功依现有通道回执模式汇总:per_segment须齐段成功,message_level保持既有整条成功语义。不得为降低冲突把第一段成功视为整条成功。
  3. 同一次有效发送的失败只能触发一份补发决策;是否允许补发仍检查当前应用、通道组、报备、风控、时间上限等既有规则。unknown不触发补发,72小时及组内时限规则保持。
  4. 旧尝试的迟到回执保留审计,不覆盖后续有效尝试的最终成功,不再次触发旧尝试补发或退款。矛盾回执按既有规则记录异常,不采用“谁抢到锁谁决定事实”。
  5. 补发不是消息最终失败;仍有有效补发时不得提前生成业务最终失败通知或最终退款。
  6. HTTP回调保持业务消息维度;CMPP回执按客户原始分段及Registered_Delivery要求生成。内部上游分段与客户分段不能混用。
  7. 现有提交、回执、补发、投递和账务唯一约束全部保留;计费单位、价格、扣退费条件及租户权限不变。
  8. 对外投递仍是可重试的至少一次交付,不能承诺网络侧绝对只收到一次;整改保证同一逻辑通知仅创建一份耐久投递事实,重试沿用其身份。

10.3 影响模块与最小实施边界

所有者 拟修改内容
send-receipt.service.ts、send-chain.helpers.ts 保存分段后提交汇总工作;按当前持久化事实重新汇总和校验有效尝试,移出重复终态副作用
send-retry.service.ts、send-gateway-submit.service.ts 统一补发处理资格,认领后选路,事务内验证资格并创建Submit与GatewaySubmitOutbox
send-gateway-result.service.ts、send-timeout.service.ts、send-downstream-delivery.service.ts 清查提交失败、超时、平台失败等旁路,接入同一业务消息终态协调,不留下与回执处理竞争的独立写入口
send-accounting.service.ts、send-completion.service.ts 核实稳定账务键和事务接口;重复恢复不重复扣退,不能用已完成标记替代实际账务事实
downstream-receipt-targets.ts、HTTP Webhook生产端 保持目标分段契约,幂等落库后再独立投递,重复命中不直接再次发送
Prisma、回调进程恢复扫描、定向与集成测试 增量状态模型、认领/恢复索引、有限批次消费与故障注入

预期主要为API发送链路和数据库迁移;不新增页面、不改变客户HTTP参数、签名算法、错误码和CMPP协议。Gateway命令契约预期不变,若实现必须变更,先补契约及Go测试,不能隐式扩大范围。需检查全部调用者而非只在回执方法外套进程内锁。

10.4 拟增数据模型与认领规则

采用PostgreSQL耐久工作记录和短事务条件更新,避免仅靠进程内锁或Redis短期锁。拟新增 SmsAttemptCompletionWork,每个sourceSubmitRecordId唯一;该名称及字段在实施迁移前与现有模型再次核对。无供应商Submit的业务拒绝使用独立的稳定事件键,不伪造Submit ID。

字段 含义及约束
id、workKey 主键及唯一工作键;有Submit时固定为attempt:
tenantId、messageRecordId、sourceSubmitRecordId 与真实记录归属一致;不信任外部回调传入的租户或消息归属
revision、processedRevision 收到新的有效事实递增revision;完成时只确认已处理版本,避免并发新回执被覆盖
state pending、processing、retry_wait、idle、needs_reviewidle表示当前事实已处理,不等于永不再接收新事实
leaseOwner、leaseUntil、fenceVersion 租约及递增认领版本;旧持有者不能提交后续业务动作
decision、retrySubmitRecordId 本次已提交决策及已有补发引用;补发一旦创建,不因恢复或后续分段重新选路创建
attempts、nextAttemptAt、lastError、createdAt、updatedAt 恢复、退避和诊断信息;错误脱敏,不复制短信正文或凭据

索引:唯一workKey、非空sourceSubmitRecordId唯一;按state/nextAttemptAt及state/leaseUntil支持有界扫描;messageRecordId支持对账。不按手机号或高基数业务ID创建监控标签。

认领以数据库时间为准:pending、到期retry_wait或租约过期processing可由条件更新取得;认领成功递增fenceVersion。建议初始租约60秒、每20秒续租、每次扫描不超过32条,作为测试起点而非容量结论。连接池预算、扫描间隔及退避上限须通过隔离环境实测定稿。

每次持久化副作用都在短事务中锁定工作记录并核对认领版本、租约及当前有效尝试,同时锁定对应业务消息,使不同尝试/超时入口也不能交错覆盖最终状态。锁顺序统一为工作记录→业务消息→既有账务锁;禁止反向调用形成死锁。不得持有事务等待限速、网络或Redis。

10.5 事实保存、业务收尾和补发流程

A. 接收与汇总

  1. 维持耐久Inbox接收。处理单个回执时,将回执事实、分段审计和工作记录唤醒放入同一短事务;唯一事件已存在时,也须核实存在对应工作,不直接忽略未完成收尾。
  2. Inbox的matched表示关联事实已可靠保存,业务收尾由工作记录跟踪;两者状态不能混为一个“完成”。在事务提交前退出,由Inbox恢复;提交后退出,由工作扫描恢复。
  3. 工作消费者认领后,重新读取当前Submit、所有有效分段及业务最终状态。未齐段成功则结束本轮为idle,保留processedRevision;后续新事实使其再次pending。unknown也不能被永久锁死,按既有状态机处理后续明确结果或超时。
  4. 确认最终结果时,事务内重新核验revision、当前尝试和认领版本。有新事实或尝试已变更,重新汇总或记历史,不使用认领前的旧message对象直接更新。

B. 允许补发

  1. Submit失败入口与回执失败入口都唤醒同一sourceSubmitRecordId的工作;取得资格后才进行选路等高成本操作。
  2. 优先查询已有retryOfSubmitRecordId对应的Submit/Outbox。存在则复用,不再次选路、扣费或创建;不存在才评估业务规则并选择候选。
  3. 选路和限速等待不持数据库锁,长等待续租;失去租约即停止。最终短事务核验资格、当前尝试及必要的实时启用/报备条件,原子提交补发Submit、消息当前尝试更新、GatewaySubmitOutbox和工作决策。
  4. 事务失败全部回滚;成功后由原有Outbox发布器发送,不由认领流程直接调用供应商。不可把“处理中”当成“补发已成功创建”,也不可因此提前退款。
  5. 临时数据库/连接错误进入retry_wait,不能误判为“无通道、最终失败”。确定不允许补发后,才走既有最终失败规则。

C. 最终业务结果与客户通知

最终消息状态、必要账务变化和全部应生成的耐久通知事实,应在同一短事务中提交。需让相关服务接受事务客户端;HTTP使用既有HttpWebhookEvent/Delivery模型,CMPP使用CmppDownstreamDelivery,不在事务内向客户发网络请求。

如实际账务接口无法共用事务,实施必须先补充稳定业务键的耐久分步恢复设计并验证全部崩溃点,不能先标工作完成再异步裸调用退款/通知。整条业务完成与单次发送尝试结束须分开:已安排补发只结束旧尝试,不给客户发最终失败。

正常幂等命中使用针对已知唯一键的无异常插入(例如参数化INSERT ON CONFLICT ... DO NOTHING RETURNING)并回读核实归属和内容;不能把所有数据库错误当重复成功。补发/回执唯一约束仍是最后防线,非预期冲突保留告警。

10.6 故障恢复与历史兼容

中断点 恢复预期
回执事实事务提交前 事务回滚,Inbox按既有机制再处理
事实已保存、工作尚未被领取 耐久pending扫描继续;不得依赖setImmediate一定执行
认领后、选路中进程退出 租约到期由新消费者接续,旧版本写入被fence拒绝
Submit/Outbox事务提交结果不确定 按原始提交唯一键回读;已有记录继续原Outbox,不创建新Submit
最终状态/账务/通知事务中退出 全部回滚或全部提交;恢复查稳定业务键,账务不重复
通知已入库、网络投递失败 原有投递记录重试;不重新运行短信补发决策
消费者持续失败 有限指数退避,达到配置上限进入needs_review并告警;不得伪造业务成功或静默丢弃
同时有新分段或迟到事实 revision差异驱动再次汇总;旧尝试只留审计,已完成副作用不重复

迁移只增表/索引,不删除历史回执、补发、账单或唯一约束。启用前暂停受影响消费者并完成现有任务排空,不能让旧版直接处理与新版协调器长期并行。

切换时建立准确切换时间及待处理清单:未完成Inbox、正在处理回执以及尚未完成收尾的业务记录进入受控衔接;有最终通知/账务/补发事实的历史记录以事实作为已完成依据。不能仅因新工作表为空扫描全部历史短信安排补发。历史异常修复另行设计、授权和审计。

上线前必须验证回执先于Submit结果、历史缺失Submit关联、无租户通道测试短信和平台业务拒绝等路径。无法可靠关联的记录保留待匹配/人工排查,不推测来源、不误收尾。

10.7 观测、性能与验收证据

增加低基数指标:工作认领成功/竞争未获得、租约接管、旧版本拒绝、恢复次数、needs_review数量、最老未完成时长、最终动作数量。重复命中记录计数和必要采样,不让正常幂等持续生成ERROR堆栈;真实失败仍明确告警。

业务验证必须对账:每个源Submit最多一个有效后继补发;每个客户目标最多一份逻辑通知;每个账务业务键最多一笔;最终状态与分段事实/最新有效尝试一致;Inbox、工作表、Submit Outbox、Redis和客户投递都有可解释的完成或等待原因。

性能对比使用同环境、同版本以外条件、同长短信比例、同成功失败分布和同负载,分别记录入口、首次Submit、总Submit、最终送达、分段回执速率及窗口。记录CPU用户态/内核态/I/O wait、数据库写入与锁等待、队列积压及回执完成延迟。新增进程CPU/SQL采样需低开销评估,不擅自在线开启全量SQL日志。

确定性并发用例要求不再出现上述两类预期竞争导致的23505异常,正常情况下每次有效尝试仅一份已提交决策;故障恢复可重复计算,但不能重复业务副作用。性能无倒退且队列按批准的排空时限清空;执行负载前固定对比基线和延迟阈值,不事后选窗口宣称提升。不得承诺未经测量的CPU降幅或TPS。

10.8 实施顺序、成本和发布恢复

  1. 补定向失败用例复现两段同时成功/失败;盘点所有终态和补发调用路径及账务事务能力,形成事务边界清单。
  2. 实施增量模型、原子认领、revision/fence和扫描恢复;先验证多进程争抢及旧持有者失效。
  3. 接入回执、提交失败、超时及平台失败路径;事务化Submit/Outbox与最终通知/账务。保持外部协议和业务规则。
  4. 执行定向测试、API全量回归、类型检查、构建和现有质量门禁;真实PostgreSQL并发及迁移测试必须通过。Gateway有修改再执行Go测试及vet。
  5. 在另行明确的隔离模拟范围内,以真实API、PostgreSQL、Redis、Gateway和Webhook接收端验收,包含非零计费、重启故障、回执乱序及排空对账;页面检查发送详情和批次最终状态。mock只能证明隔离逻辑。
  6. 真实回归通过后,按明确提交/推送/目标环境授权交付;部署使用标准release流程和独立恢复资产。测试与预生产分别验收,预生产不得以历史授权直接补发或压测。

这是跨回执、补发、账务和耐久任务恢复的修复,成本主要在事务改造及故障验收,不能仅以修改几行或压低日志级别交付。完成第1步盘点后再给出实现工时;若需改变计费、状态优先级或Gateway契约,先记录范围扩展。

恢复策略:迁移保留增量表;发布失败先停止新旧受影响消费者交叉执行,盘点未完成工作、已生成Submit/Outbox/账务/投递事实,再选择经过兼容验证的应用版本恢复。旧版本不理解新工作表,不能直接回退并声称全部恢复;必须证明待处理工作已排空或存在经过验证的接续路径,否则保持暂停并修复。禁止删除工作记录、回滚业务数据或重新入队来掩盖异常。

上线停止条件:发现重复供应商Submit、重复扣退费、终态错误覆盖、持续积压、无法接管或恢复缺口立即停止放量,保留现场;不通过手工补发完成验收。

10.9 完成标准与本轮状态

验收用例见 系统功能测试用例 的TC-RC-20260916-0112;原TC-SEND-018/019/020继续执行。记录见 测试进度

只有代码、真实并发/故障恢复、账务与队列对账均通过,并完成对应环境独立验收,才能标为该环境已修复。2026-09-16本轮只编写整改方案与用例,未实现模型或业务代码,未运行发送、迁移、提交、推送、测试部署或预生产部署。

10.10 2026-09-16 实施细化(开发中,尚未验收)

用户已明确授权实施第10节、提交、推送及测试环境部署;第10.1、10.9的“仅方案”状态是此前记录,当前开始实施,不能据此宣称已上线。

  • 增加 SmsCompletionEvent 耐久事件日志,与工作 revision 在同一接收事务提交;Inbox matched 表示原始事件已存入该日志。规范化 Receipt/Segment 事实、汇总和副作用由收尾事务一并提交。此细化替代10.5 A1中先写规范化表的顺序,避免规范化事实落库后收尾未接续的窗口。日志保留原始事件,不能将未处理日志计作已完成业务回执。
  • 回执、整条提交结果、分段提交结果、超时与平台拒绝通过统一入口入日志;先按可靠关联核验消息及租户。无可靠Submit关联的供应商回执继续留在Inbox待匹配;无供应商Submit的平台拒绝使用message工作键。迁移不扫描历史消息重发。
  • 所有收尾协作者通过仅限发送链路的事务适配器加入当前短事务,账务内层事务加入同一事务。锁顺序为工作→消息→账务。HTTP通知生产端显式接收事务客户端,CMPP通知无异常幂等创建;事务内仅写耐久通知,不执行网络投递。
  • 补发首先认领。首次尝试到达选路点时回滚探测事务,在事务外重新选路和限速;最终事务重新读事件、revision、有效尝试与当前路由条件,原子创建Submit/Outbox。回滚产生的会话ID不得进入跨事务缓存。临时故障必须抛出并退避,不能被吞成最终失败。
  • 恢复扫描初始5秒/32条,租约60秒、20秒续期、事务20秒上限。最多12次失败进入needs_review;这些是待真实环境验证的初值。异步通知仍由原恢复消费者投递。
  • 超时与迟到结果保持已退款状态,不重复扣退;每个历史业务键依据实际账务和通知事实恢复。上线前必须以故障与乱序测试核实,不把代码编译当作证明。

待补证据:真实PostgreSQL并发、fence过期、事务中断、队列排空、长短信CMPP与HTTP通知以及账务对账。当前仍为开发中。

10.11 本次实现边界与发布依赖(2026-09-16)

  • 恢复扫描在worker/callback/all角色运行;正式运行必须启用SEND_SUBMIT_OUTBOX_PUBLISH_ENABLED及对应发布进程。测试环境18:20只读核验两项Outbox开关为true、独立服务active。关闭发布器不属于受支持上线组合,不能把pending当成已发送。
  • 收尾needs_review及等待超过300秒由监控采集器直接查询耐久工作/事件表,写入同一告警事件库,恢复仍保留、仅人工清除;不依赖应用发布工具安装Prometheus规则。过程计数仍提供低基数metrics,采集进程口径需区分。
  • 两项迁移仅新增四张表及索引,无历史回填、无删除、无自动重发;全量106项已在新隔离数据库重放。工作时间使用UTC表达式,避免数据库会话时区与Prisma时间不一致。应用回退前必须盘点和接续未完成工作,旧代码不能消费这些新表。
  • 本地验证包括真实PostgreSQL、两个独立OS进程、事务回滚、旧消费者挂起后接管、三段通知目标、非零账务和重试上限可见告警;网络投递与目标环境的重启/排空验收独立记录,不将路由隔离测试冒称整链路通过。