fix: coordinate SMS completion and improve operations diagnostics
CSS quality / css-quality (push) Has been cancelled

This commit is contained in:
hectorzhao
2026-09-16 18:27:29 +08:00
parent a0209f93bc
commit a350aca883
40 changed files with 2599 additions and 366 deletions
+151
View File
@@ -195,3 +195,154 @@ GatewaySubmitOutbox
- 修复后正价smoke为9/9受理,账单`9×325=2925`20 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 目的、证据和适用范围
业务解释:一条长短信有多段回执,多个处理流程同时发现整条短信已经有结果,重复安排通知或补发。现有数据库唯一键能阻止重复创建,但不能避免此前重复查询、选路和事务开销。整改目标是“先认领处理资格,再做业务动作;中途退出后可以安全接续”。
依据:[预生产只读诊断](cpu-diagnosis-20260916.md)。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:<sourceSubmitRecordId> |
| 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 完成标准与本轮状态
验收用例见 [系统功能测试用例](system-functional-test-cases.md) 的TC-RC-20260916-0112;原TC-SEND-018/019/020继续执行。记录见 [测试进度](testing-progress.md)。
只有代码、真实并发/故障恢复、账务与队列对账均通过,并完成对应环境独立验收,才能标为该环境已修复。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进程、事务回滚、旧消费者挂起后接管、三段通知目标、非零账务和重试上限可见告警;网络投递与目标环境的重启/排空验收独立记录,不将路由隔离测试冒称整链路通过。