docs: 拆分契约/前提/参数文档,合并消息生命周期并收敛设计裁决
- design.md 合并 message-lifecycle.md(后者保留跳转 stub),去重并改用稳定 ID 引用 - 新增 invariants.md(前提/不变量/声明边界/验证映射)、contracts.md(对外条款与 Q 台账)、 reference.md(参数/指标/模块/错误分类) - architecture/flight-state/user-stories/README 同步重划引用;README 废止「引用章节号」 - 裁决收敛:R 按超期而非次数放弃、清除前提含放弃清单、航班表写者互斥、head-deadline 仅告警、 超期判据用本地入队时间、投递批次读取时刻冻结、schd 只进不退与条件标记、FLID 复用为前提 - 删除 runbooks 与 CI 防漂移校验:设计阶段不产出面向执行期的产物 Refs: ACM2-42..51
This commit is contained in:
+57
-10
@@ -1,16 +1,63 @@
|
||||
# 设计文档入口
|
||||
|
||||
当前实现核对日期:2026-09-10(代码基线 `eea1120`)。文档哈希不在此行回填,以 `git log -- docs/` 为准。文档中的目标能力不等于已实现;测试通过不等于现场已发布。
|
||||
实现核对日期:2026-09-11(代码基线以 `git log -- docs/` 为准)。文档描述**目标设计**,不等于已实现;测试通过不等于现场已发布。进度与缺口在 Plane(ACM2)跟踪;文档只记录「对外主张当前是否可声明」,见 [invariants.md](invariants.md) 声明边界。
|
||||
|
||||
## 1. 按读者选入口
|
||||
|
||||
| 你是谁 | 从哪读起 |
|
||||
|---|---|
|
||||
| 新开发者 | [invariants.md](invariants.md)(前提 / 不变量 / 声明边界)→ [design.md](design.md)(机制)→ [reference.md](reference.md)(参数 / 指标 / 模块) |
|
||||
| 库方 / 上游接口人 | [contracts.md](contracts.md)(条款 `C-x` 与待确认 `Q`) |
|
||||
| 值班主任 / 运维 | [reference.md](reference.md)(参数与指标)→ [invariants.md](invariants.md)(哪些主张当前不可声明)。执行规程在上线/切流前另立,设计阶段只保留前置条件与红线 |
|
||||
| 评审 / PR 作者 | [invariants.md](invariants.md) + 本文 §5 评审清单 |
|
||||
|
||||
## 2. 文档职责(一个事实只有一处定义)
|
||||
|
||||
| 文档 | 唯一职责 |
|
||||
|---|---|
|
||||
| [architecture.md](architecture.md) | 系统边界、模块职责、FIFO、单写者、数据库归属。 |
|
||||
| [design.md](design.md) | 管道流程、处理/投递状态、恢复与运行配置。 |
|
||||
| [message-lifecycle.md](message-lifecycle.md) | 信箱消息全生命周期的唯一现行规范:五事实、状态、水位、回填与清除。 |
|
||||
| [flight-state.md](flight-state.md) | 航班设计的唯一现行规范:当前态、表关系、事务流程和开放问题。 |
|
||||
| [user-stories.md](user-stories.md) | US/OPS 验收目标,不能用“当前基础”替代完成证据。 |
|
||||
| [旧系统行为基线](legacy/msgexchange-api-legacy-user-stories.md) | msgexchange-api 现役行为基线与 KEEP/FIX 来源,Q8 对拍参考;不作为 v2 设计依据。 |
|
||||
| [legacy/decision-flight-state-history.md](legacy/decision-flight-state-history.md) | 已撤销方案的存档说明;现行规则仍以 `flight-state.md` 为准。 |
|
||||
| [SIS 规范](legacy/SIS_AODB_RMS-V0.1.md) / [XSD](legacy/unisysaodbsis.xsd) | 外部协议事实;即使在 legacy 目录仍是兼容依据。 |
|
||||
| [architecture.md](architecture.md) | 系统边界、模块职责、存储归属、架构决策(D1–D4)。 |
|
||||
| [invariants.md](invariants.md) | 前提 `PRE-x`、不变量 `INV-x`、声明边界 `CLM-x`、验证映射。 |
|
||||
| [design.md](design.md) | 管道机制:收报·水位、主泵·处理·事务边界、回填、投递、作业与归档。 |
|
||||
| [contracts.md](contracts.md) | 对外承诺与要求(`C-x`)、待确认事项(`Q`)。 |
|
||||
| [reference.md](reference.md) | 参数注册表、指标与健康、模块入口、错误分类。 |
|
||||
| [flight-state.md](flight-state.md) | 航班域模型与合并语义。 |
|
||||
| [user-stories.md](user-stories.md) | US / OPS 验收目标。 |
|
||||
| [legacy/](legacy/) | 现役行为基线与外部协议事实([SIS 规范](legacy/SIS_AODB_RMS-V0.1.md)、[XSD](legacy/unisysaodbsis.xsd));与现行文档冲突时以 SIS 为准。 |
|
||||
|
||||
维护规则:架构写约束,设计写机制,规范(`flight-state.md`、`message-lifecycle.md`)写唯一规则,故事写目标,Plane 跟踪工作。不要在多份文档复制阶段清单;同一规则只允许一处定义,其他文档引用章节号。外部协议事实([SIS 规范](legacy/SIS_AODB_RMS-V0.1.md)、[XSD](legacy/unisysaodbsis.xsd))与现行文档冲突时以 SIS 为准;legacy 行为基线只描述现役行为。
|
||||
`message-lifecycle.md` 已并入 design.md,文件仅保留跳转说明。
|
||||
|
||||
## 3. 事实归属表
|
||||
|
||||
| 事实 | 唯一归属 | 其他文档怎么写 |
|
||||
|---|---|---|
|
||||
| 水位 `W`、空洞判定与推进条件 | design「收报与水位」 | contracts 写对库方的承诺 `C-1`–`C-3`;reference 写参数 |
|
||||
| 保留期下界 `R_keep`、清除前置条件 | contracts「保留与清除」 | design 只引 `C-x`;执行步骤在上线前另立 |
|
||||
| 处理标记值集与写权限 | contracts `C-5` | design 只写行为约束「只写空标记、不回撤、不覆盖」(`INV-6`) |
|
||||
| 回填四结果、放弃语义、`R` 的作用 | design「回填」 | invariants 记结论与可声明性 |
|
||||
| `head-deadline` / 退避 / `claim-batch` 等取值 | reference「参数」 | design 只引 `PARAM:x` |
|
||||
| 消费权排他、ID 不复位、报文不可变、时钟、单实例 | invariants「前提」 | 其他文档只引 `PRE-x` |
|
||||
| 航班身份、合并语义、`STATE_VERSION`、`OPERATION_DAY` | flight-state.md | design 只引域规则 |
|
||||
| 对外术语(落信 / 入站 / 库方 / 处理标记) | contracts「术语」 | — |
|
||||
|
||||
## 4. 引用与写作纪律
|
||||
|
||||
1. **引用只用稳定 ID**:`PRE-x`、`INV-x`、`CLM-x`、`C-x`、`PARAM:x`、`[G-x]`、`[Q-x]`、`US-xx`、`ACM2-nn`。**不再用章节号做跨文档引用。**
|
||||
本条取代旧版 README 的「其他文档引用章节号」规则:章节号随增删章节腐烂,指针失效后必然被改写为复述——重构前实测有 30 处「唯一定义处」与 33 处跨文档章节引用。
|
||||
2. 指针之后**不再复述**被指内容。若两处需要同一段话,说明它放错了位置。
|
||||
3. 文档不记进度,只记「主张是否可对外声明」。进度在 Plane。
|
||||
4. 机制段只写「是什么 / 为什么」。验收 → invariants 验证映射;前置条件、红线与库方方案 → contracts / invariants;参数默认值 → reference;操作步骤在上线/切流前另立规程,设计阶段不写。一句话一个主张;加粗每节 ≤ 3 处。
|
||||
5. 数值只有两个家:参数默认值与指标名在 reference.md;契约数值(保留期、值集、下界)在 contracts.md。
|
||||
|
||||
## 5. 维护清单
|
||||
|
||||
**Q 答复落地(4 步)**:① 在 contracts.md 就地补结论与日期 → ② 改 reference.md 的默认值与「依据」列 → ③ 机制有变才改 design.md → ④ 在 invariants.md 的 CLM 上划掉挂起。
|
||||
|
||||
**PR 评审 4 问**:新事实是否已有归属?是否复述了别处?是否用了稳定 ID?是否把运维 / 验收写进了机制段?
|
||||
|
||||
**尺寸触发器**:design.md 超过约 800 行才按子系统再拆;invariants.md 超过两页先怀疑混进了机制。
|
||||
|
||||
## 6. 当前状态
|
||||
|
||||
- 2026-09-11 文档体系重构:新增 invariants.md / contracts.md / reference.md;design.md 由 design.md + message-lifecycle.md 合并(后者保留跳转 stub);跨文档引用由章节号改为稳定 ID。
|
||||
- 不建 runbooks.md:设计阶段没有可执行的运行环境,操作步骤在上线/切流前另立(见 Plane ACM2-48);设计阶段需要的只有前置条件与红线,它们分别在 contracts(`C-7`–`C-12`)与 invariants(CLM-x)。
|
||||
- 目标设计与交付状态分离:未交付、未确认、不可声明的主张见 invariants.md 的声明边界与 Plane ACM2,不在正文里逐段标注。
|
||||
|
||||
+16
-14
@@ -12,7 +12,7 @@ msgexchange-v2 是机场 OMMS 的上游报文处理中间件,用于替换旧
|
||||
- **输出**:Kafka 的 `msg` / `schd` 消息、共享 MySQL 的 `COUTMSGS` 出站信箱,以及查询 HTTP 接口;不直接推送前端。
|
||||
- **当前范围(阶段 A)**:航班当前态落自有 PostgreSQL(`FLIGHT_SCHD` + 资源明细表 + `FLIGHT_ROUTE_POINT`,权威口径见 [flight-state.md](flight-state.md))。无 Redis 依赖;ES 历史投影属暂缓的阶段 B。
|
||||
|
||||
本文描述架构约束,不代表所有能力已实现;实现缺口见第 9 节。模块交互、状态机和参数详见 [design.md](design.md),消息生命周期与信箱清除见 [message-lifecycle.md](message-lifecycle.md),需求见 [user-stories.md](user-stories.md)。历史报文契约仍以 [SIS 接口规范](legacy/SIS_AODB_RMS-V0.1.md) 和 [XSD](legacy/unisysaodbsis.xsd) 为兼容依据,其他 legacy 资料仅作参考,旧系统行为基线的职责见 [README](README.md) 职责表。
|
||||
本文描述架构约束,不代表所有能力已实现;实现缺口见 [invariants.md](invariants.md) 的声明边界与 Plane(ACM2)。模块交互、状态机与参数详见 [design.md](design.md),前提与不变量见 [invariants.md](invariants.md),对外契约见 [contracts.md](contracts.md),需求见 [user-stories.md](user-stories.md),参数与指标见 [reference.md](reference.md);操作步骤在上线/切流前另立规程(设计阶段只保留前置条件与红线)。历史报文契约仍以 [SIS 接口规范](legacy/SIS_AODB_RMS-V0.1.md) 和 [XSD](legacy/unisysaodbsis.xsd) 为兼容依据,其他 legacy 资料仅作参考,旧系统行为基线的职责见 [README](README.md) 职责表。
|
||||
|
||||
现场供库时目标为 Oracle 11g,否则自建 PostgreSQL;当前只有 PG 实现可运行,Oracle 不是已支持的平台。
|
||||
航班表结构和处理逻辑见 [运营航班状态设计](flight-state.md)。
|
||||
@@ -54,7 +54,7 @@ CIIMS / AODB 等上游
|
||||
| `codec` | XML 解码,区分非法报文与可修复的解码失败。 |
|
||||
| `processing` | FIFO 调度、业务身份绑定与去重、领域决策与落库(SCHD/FLOP/FDEL/ADFT):纯领域逻辑只返回决策;Processor 作为事务协调器,在锁事务内完成状态写入、事件与回填意图登记,不直接触碰 Kafka。 |
|
||||
| `delivery` | 消费待发事件,负责按目标保序、`schd` 聚合、投递和失败重试。 |
|
||||
| `jobs` | 回填补偿扫描、航班历史清理与留痕保留期清理;独立 job 线程执行(调度见 design.md §6.1,红线见 flight-state.md §6),不参与 FIFO。 |
|
||||
| `jobs` | 回填补偿扫描、航班历史清理与留痕保留期清理;独立 job 线程执行(调度见 design.md「维护作业与归档」,红线见 flight-state.md「生命周期与开放项」,与主泵的互斥见 `INV-18`),不参与 FIFO。 |
|
||||
| `domain` / `config` | 领域状态、事件和决策模型,以及运行参数。 |
|
||||
| `infra` | 仓储(JDBC/stub)、外部适配器、重试、健康检查与日志;通过接口隔离基础设施。 |
|
||||
|
||||
@@ -62,7 +62,7 @@ CIIMS / AODB 等上游
|
||||
|
||||
### 收报与处理
|
||||
|
||||
1. `InboxPoller` 默认每秒按 ID 区间扫描水位 `W` 之后的信箱记录(`ID > W`,**不以处理标记为谓词**),在自有 PG 中建立 `PROC_STATE(PENDING)`;水位与入队在同一事务推进。重复扫描不能重复入队,中断后由重扫补建。扫描谓词与水位的唯一口径见 message-lifecycle.md §5.1。
|
||||
1. `InboxPoller` 默认每秒按 ID 区间扫描水位 `W` 之后的信箱记录(`ID > W`,**不以处理标记为谓词**),在自有 PG 中建立 `PROC_STATE(PENDING)`;水位与入队在同一事务推进(`INV-2`)。重复扫描不能重复入队,中断后由重扫补建。扫描谓词与水位见 design.md「收报与水位」。
|
||||
2. 主泵只处理最小未完成 `MSG_ID`。解析报文、绑定业务身份并去重后,分派给 SCHD/FLOP/FDEL/ADFT 处理器。
|
||||
3. 在自有 PG 同一事务内(先取 `PIPELINE_LOCK`)保存航班状态变更(`FLIGHT_SCHD` 与明细表)、`MSG_EVENT` 待发事件、处理终态与回填意图。
|
||||
4. 事务提交后,补写共享信箱的处理标记(外部副作用,由回填退避重试与超期强制补写保障)。
|
||||
@@ -75,11 +75,13 @@ CIIMS / AODB 等上游
|
||||
|
||||
## 5. 必须保持的约束
|
||||
|
||||
- **消息严格 FIFO**:队头失败并退避时,后续消息仍不能越过它。只有队头完成或按失败策略进入终态后,队列才继续推进。收报重扫和水位设计必须防止较小 ID 漏入队而被后续消息越过。
|
||||
- **动态状态单写者**:`FLIGHT_SCHD` 及明细表只由主泵单线程写入。事务内第一步对 `PIPELINE_LOCK` 单行 `SELECT ... FOR UPDATE` 互斥;该方案不依赖数据库扩展或额外 DBA 特权;Oracle 11g 待适配时验证。不能通过增加实例或处理线程直接扩容。
|
||||
- **身份去重**:同一业务身份只能绑定一条有效处理记录,重复报文不应再次产生业务副作用。具体身份组成和重放规则见设计文档。
|
||||
- **快照可恢复**:快照以 `PROC_STATE` 成功终态判定重放;每次成功写入推进 `STATE_VERSION`;`OPERATION_DAY` 一经确定不可变(见 flight-state.md §2.1)。
|
||||
- **物理清除只发生在历史归档**:删除一律先标记(FDEL)或由生命周期清除;历史存储未接通时必须删 0 条(见 flight-state.md §6)。
|
||||
本节只列约束的**归属**;完整定义与验证映射见 [invariants.md](invariants.md),实现与演进不得违反:
|
||||
|
||||
- 消息严格 FIFO:`INV-3`、`INV-4`、`INV-5`(发现完整性依赖 `PRE-2`/`PRE-3`,当前不可对外声明,见 CLM-1/CLM-2)。
|
||||
- 动态状态单写者与写者集合互斥:`INV-17`、`INV-18`。
|
||||
- 身份去重:`INV-9`(身份组成见 design.md「消息、身份与决策」)。
|
||||
- 快照可恢复与运营日不可变:`INV-12`、`INV-13`。
|
||||
- 物理清除只发生在历史归档之后:`INV-15`(红线见 flight-state.md「生命周期与开放项」)。
|
||||
|
||||
这些约束优先于吞吐量优化。单写者降低了并发复杂度,代价是队头阻塞和吞吐上限;如需并行化,必须先重新定义顺序与状态归属,不能只调整线程数。
|
||||
|
||||
@@ -88,9 +90,9 @@ CIIMS / AODB 等上游
|
||||
| 存储 | 承载内容 | 职责说明 |
|
||||
|---|---|---|
|
||||
| 自有 PostgreSQL | 单行锁 `PIPELINE_LOCK`、处理状态与回填事实 `PROC_STATE`、消费水位 `INBOX_CURSOR`、待发事件 `MSG_EVENT`、请求跟踪 `REQ_TRACK`、航班当前态 `FLIGHT_SCHD` + 8 张资源明细表 + `FLIGHT_ROUTE_POINT`、留痕 `SCHD_SNAP_LOG` | 本系统唯一业务数据库。消息处理、状态推进、处理终态、回填意图与待发事件在单事务内原子提交;本地事务只在此库。 |
|
||||
| 共享 MySQL | `CMINMSGS` 入站信箱、`COUTMSGS` 出站信箱 | 外部系统所有。本系统仅执行约定的信箱读写与处理标记回填,不建表、不迁移 schema、不写历史表;由库方按 Q9 执行的清除与历史归档见 message-lifecycle.md §6。兼容 HTTP 入口可按既有契约写入入站信箱。 |
|
||||
| 共享 MySQL | `CMINMSGS` 入站信箱、`COUTMSGS` 出站信箱 | 外部系统所有。本系统仅执行约定的信箱读写与处理标记回填,不建表、不迁移 schema、不写历史表;由库方按 `Q9` 执行的清除与历史归档见 contracts.md「保留与清除」。兼容 HTTP 入口可按既有契约写入入站信箱。 |
|
||||
|
||||
**不使用跨库事务。** PG 事务只能保证“处理结果与待发事件一起提交”,不能覆盖 MySQL 回填或 Kafka 发送等外部副作用。跨存储依靠幂等、重试和持久化补偿恢复;各中断位置的判定与恢复动作统一见 [message-lifecycle.md](message-lifecycle.md) §4,本文不重复。
|
||||
**不使用跨库事务。** PG 事务只能保证“处理结果与待发事件一起提交”(`INV-17`),不能覆盖 MySQL 回填或 Kafka 发送等外部副作用。跨存储依靠幂等、重试和持久化补偿恢复;各中断位置的判定与恢复动作见 design.md「中断恢复」。
|
||||
|
||||
对外投递按**至少一次**设计,不承诺端到端恰好一次。Kafka 生产者幂等不能消除应用重启或 outbox 重发带来的所有重复。
|
||||
|
||||
@@ -127,10 +129,10 @@ CIIMS / AODB 等上游
|
||||
|
||||
上线前必须完成并验证:
|
||||
|
||||
- 真实 PG + 共享 MySQL 信箱的端到端处理、补偿与投递,以及出站信箱适配;未闭合的缺口清单见 design.md §10。
|
||||
- 航班状态不变量与恢复证据:`STATE_VERSION` 推进、`OPERATION_DAY` 不可变、故障中断回滚(见 flight-state.md §3.1、§4、§7)。
|
||||
- FIFO 越序、身份去重、FDEL/ADFT、清场顺序与投递故障的回归测试(见 design.md §8)。
|
||||
- 真实 PG + 共享 MySQL 信箱的端到端处理、补偿与投递,以及出站信箱适配;未闭合的缺口与不可声明项见 [invariants.md](invariants.md) 的声明边界。
|
||||
- 航班状态不变量与恢复证据:`STATE_VERSION` 推进、`OPERATION_DAY` 不可变、故障中断回滚(`INV-12`–`INV-16`,语义见 flight-state.md)。
|
||||
- FIFO 越序、身份去重、FDEL/ADFT、清场顺序与投递故障的回归测试(见 [invariants.md](invariants.md) 验证映射)。
|
||||
- 单实例排他保护与启动校验、影子隔离、Kafka 生产配置约束——当前配置允许环境变量覆盖 `acks`/幂等/in-flight,且默认 in-flight 值与 D3 不同,切流前必须按 D3 收敛。
|
||||
- 死信与一致性异常的告警、可执行的人工重放流程、端到端追踪、积压指标与安全边界。
|
||||
|
||||
验收与进度由 Plane 跟踪,缺口逐项见 design.md §10 与 user-stories.md;航班状态规则统一以 flight-state.md 为准。
|
||||
验收与进度由 Plane 跟踪,缺口逐项见 [invariants.md](invariants.md) 的声明边界与 [user-stories.md](user-stories.md);航班状态规则统一以 flight-state.md 为准。
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
# 对外契约与待确认事项
|
||||
|
||||
本文件是本系统与外部对手方之间**承诺与要求**的唯一出处,也是待确认事项(`Q`)的唯一台账。条款编号 `C-x`、问题编号 `Q-x` 稳定不变;条款被取代时标 `[作废 by C-y]` 并保留原文,不静默改写。
|
||||
|
||||
读者:库方(共享 MySQL 管理方)接口人、上游(CIIMS / AODB / SIS)接口人、本系统开发与运维。
|
||||
|
||||
条款状态词只有三种:`[待确认 Q-x]`(未取得对方书面确认)、`[已确认 YYYY-MM-DD]`(对方书面确认且已回写)、`[我们单方承诺]`(不依赖对方,已生效)。
|
||||
|
||||
## 术语
|
||||
|
||||
| 术语 | 含义 |
|
||||
|---|---|
|
||||
| 上游 | 向信箱写入报文的源头系统(CIIMS、AODB 等)。 |
|
||||
| 信箱 | 共享 MySQL 的入站表 `CMINMSGS`;出站方向为 `COUTMSGS`。 |
|
||||
| 库方 | 共享 MySQL 的管理方;表结构变更与数据清除只能由库方执行或书面授权。 |
|
||||
| 处理标记 | 信箱行上表示「本系统已处理」的约定字段;逻辑名 `DATE_PROCESSED` / `STATUS`,实际列名以库方契约为准。本系统只把空标记写成已处理值,不回撤、不覆盖。 |
|
||||
| 落信 | 报文进入信箱(`CMINMSGS` 存在该行),执行方是上游。 |
|
||||
| 入队 | 本系统在自有 PG 建立 `PROC_STATE` 记录,开始处理。 |
|
||||
| 已回填 | 本系统已把处理标记写回该信箱行。 |
|
||||
| 投递确认 | 投递目标已接受且本地 `MSG_EVENT` 已置 `SENT`;不表示业务消费者已消费。 |
|
||||
| 自有 PG | 本系统唯一的业务数据库 PostgreSQL;与信箱之间不存在跨库事务。 |
|
||||
|
||||
## A. 共享信箱(库方)
|
||||
|
||||
### A.1 ID 与可见性
|
||||
|
||||
- **C-1** ID 单调:信箱 ID 按提交顺序分配,已发布水位之下不再出现更小的新 ID。`[待确认 Q2]`
|
||||
- **C-2** ID 分配 → 事务可见时延上界由库方**直接给出**。该值决定空洞老化阈值与补偿扫描窗口宽度;**不可由 SIS 报文 `Expiry` 推导**(`Expiry` 是报文保留与传输恢复口径,与「ID 分配后多久对读事务可见」不是同一个量)。`[待确认 Q2]`
|
||||
- **C-3** ID 空间不复位、不复用、不回退:含表轮换、备份恢复、`AUTO_INCREMENT` 归零。采用整表轮换方案时,新表种子必须 ≥ `max(ID)+1`,保证 ID 不断链;本系统的水位 `W` 是不可逆单游标,ID 回退会导致其后所有行永久不可见。`[待确认 Q2]`
|
||||
- **C-4** 报文行不可变:同一业务身份(`SNDR|TYPE|STYP|SEQN`)的重发必为同一内容。若上游会以同一身份改发正文,需要另定识别规则(`Q15`)。`[待确认 Q15]`
|
||||
|
||||
### A.2 保留与清除(标记、保留期、清除前提)
|
||||
|
||||
- **C-5** 处理标记值集与写权限:本系统只写入库方认可的 legacy 值集内的「已处理」值(默认 `PROCESSED`),只写空标记、不回撤、不覆盖;内部原因(死信、重复、放弃)记录在自有 PG,**不在信箱新增枚举**。`[待确认 Q7]`
|
||||
- **C-6** 清除语义必须是「标记 + 保留期」:打标本身不触发清除,触发条件是「到达保留期 `R_keep`」且「边界内全部行已打标」。若库方语义是「打标即可清除」,则清除前置条件不成立,且**增大 `R` 无法补救**,必须另行约定保留期或引入独立原文保留通道。`[待确认 Q7][待确认 Q9]`
|
||||
- **C-7** 保留期下界(本文件是唯一定义处):
|
||||
`R_keep ≥ max(人工重放期限 + 人工处置期限, 审计期限, 回填重试上限)`。
|
||||
这是「重放窗口内原文仍在」的**唯一保证来源**。报文在 CIIMS 的 `Expiry`(480 分钟量级,SIS §3.16)可作为原文保留期的参照,但它是报文有效期,不等于本处所需的保留期。`[待确认 Q6][待确认 Q9]`
|
||||
- **C-8** 清除前置条件(本文件是唯一定义处):执行清除时,边界内**每行必须已有终局**——即「已持有处理标记」**或**「已登记在本系统的回填放弃清单中且经人工对账确认」。放弃行不写标记,未达终态的行顺延至处理完成后清除;本系统不执行 DDL,也不写共享历史表。`[待确认 Q7][待确认 Q9]`
|
||||
- **C-9** 清除执行方与方案:清除由库方执行或书面授权执行。方案 A(按 `DATE_RECEIVED` 日分区 + `TRUNCATE/DROP PARTITION`)为首选;方案 B(`CREATE TABLE ... LIKE` + 保留窗复制 + `RENAME TABLE` + 对账 + `CMINMSGS_HST` 归档 + `DROP`)为备选。现场 MySQL 版本与分区 DDL 能力待确认。`[待确认 Q9]`
|
||||
- **C-10** 时间语义:时间比较与换算统一采用机场时区 `Asia/Shanghai` 及明确类型转换;`DATE_RECEIVED` 由上游/库方写入,其时钟基准需可解释(见 `PRE-4`)。`[待确认 Q7]`
|
||||
|
||||
### A.3 原文保留与重放
|
||||
|
||||
- **C-11** 重放窗口内的原文必须可读:legacy 现役按接收超 1 天归档并删除 `CMINMSGS`;若沿用该窗口,则与 `C-7` 冲突,须以 `C-7` 为准。`[待确认 Q9]`
|
||||
- **C-12** 若原文被提前清除(违反保留契约),本系统的死信处置不变,按契约违例走运维追责;该情形不改变 `C-8` 的清除前提。`[我们单方承诺]`
|
||||
|
||||
### A.4 我们向库方的承诺
|
||||
|
||||
- **C-13** 只读约定区间的信箱行(`ID > W`),单活动实例运行,不引入并行消费者。
|
||||
- **C-14** 不建表、不改表结构、不迁移 schema、不写共享历史表;兼容 HTTP 入口按既有契约写入入站信箱。
|
||||
- **C-15** 处理标记只写 `C-5` 认可的值,不回撤、不覆盖已有非空标记。
|
||||
|
||||
## B. 上游(SIS / AODB)
|
||||
|
||||
- **C-20** 业务身份四元组 `SNDR|TYPE|STYP|SEQN` 的语义由上游定义;`SEQN` 的取值范围与回绕见 SIS §2.8.1。**重置周期未知**,它决定业务身份是否加入日期边界(默认不加)。`SNDR` 取值域也需对拍(SIS 为 AODB/RMS,legacy 实发 OSH5 等)。`[待确认 Q11]`
|
||||
- **C-21** `FLID` 在保留期内不复用。若复用,事件版本(`STATE_VERSION`)必须按 incarnation 作用域,否则「保留最新版本」的合并规则会把新航班的事件压掉,旧 tombstone 也可能删掉在用航班。`[待确认 Q16]`
|
||||
- **C-22** 报文不可变(同 `C-4`)。
|
||||
- **C-23** 请求/应答回显契约:目标优先按已确认的回显字段精确匹配;回显未确认时的降级匹配(同类开放请求且报文 `DTTM ≥ sentAt`)存在跨代误配风险,必须明确接受并审计,不得宣称精确关联。比较前统一时区与时间单位。`[待确认 Q5]`
|
||||
- **C-24** 出站信箱 `COUTMSGS`:消费方与消费顺序、`COUTMSGS_ACK_DATE_RECV` / `COUTMSGS_ACK_RESEND_TIMES` / `COUTMSGS_DATE_SENT` / `COUTMSGS_ERROR` 各列语义与写入责任、出站行清除责任与保留期、落信成功但本地未置 `SENT` 时的重复写入风险及下游去重契约,均未确认。本系统对出站的交付承诺只到**落信**为止。`[待确认 Q10]`
|
||||
- **C-25** 主 / 共享删除顺序与 EROR 回报:SIS 要求删主航班前先删子共享航班,顺序不符时 RMS 应向 AODB 回发 EROR(SIS §1.6.1-1.d,事件定义 SIS §4.8);现行设计为幂等原子级联、不回发 EROR。二选一。`[待确认 Q14]`
|
||||
- **C-26** 日计划缺失可选字段的语义:SIS 要求最新日计划中未发送的可选字段表示 AODB 已无该数据、子系统应删除本地值(SIS §3.16 注释 4,RESP 同格式见 §3.17),与现行「未携带字段保留」相反。`[待确认 Q13]`
|
||||
- **C-27** 历史积压批次中「不再处理」的确认主体、审批留痕与跳过值集。`[待确认 Q12]`
|
||||
|
||||
### B.1 我们向上游的承诺
|
||||
|
||||
- **C-28** 兼容 HTTP 入口的响应只表示**接收结果**(成功时为 `text/plain` 的信箱记录 ID),不表示业务处理成功;请求媒体类型、字符集与失败响应仍需与现役逐项对拍。`[待确认 Q3]`
|
||||
- **C-29** 对外投递按**至少一次**设计,不承诺端到端恰好一次;Kafka 消息的 key 为 `FLID`,同一 `FLID` 内保序,跨 `FLID` 不承诺顺序。`[待确认 Q4]`
|
||||
|
||||
## C. 待确认事项台账(Q)
|
||||
|
||||
| 编号 | 事项 | 当前假定 | 阻塞 | 状态 |
|
||||
|---|---|---|---|---|
|
||||
| Q1 | 权威存储(内部方向) | 自有 PG 单库权威 + 无损明细;现场供库目标 Oracle 11g | — | 已定案(内部),Oracle 适配与部署验收另计 |
|
||||
| Q2 | 信箱 ID 单调、ID 分配→事务可见时延上界、ID 空间不复位;空洞与迟到处置 | 时延按 5 分钟 `max-commit-delay`(**缺少依据的占位值**,不可由 SIS `Expiry` 推导) | 发现完整性声明、空洞老化阈值、补偿扫描窗口、水位不可逆性 | 未确认 |
|
||||
| Q3 | HTTP 契约:媒体类型、字符集、错误码、查询接口对拍 | 成功为 `text/plain` 记录 ID | 兼容入口验收 | 未确认 |
|
||||
| Q4 | Kafka wire:发送粒度、key、去重标识、分区与批次确认 | 逐 `FLID` 发送,key=`FLID` | 投递契约 | 未确认 |
|
||||
| Q5 | 请求匹配:回显字段可靠性与降级匹配 | `RQFD` 60 秒 / `RQRD` 30 秒超时 | 请求跟踪闭环 | 未确认 |
|
||||
| Q6 | 重放期限与人工处置期限的取值(唯一作用是决定 `R_keep` 下界) | `R` = 30 天;重放/处置期限未定 | `R_keep` 取值 | 未确认 |
|
||||
| Q7 | 处理标记值集与写权限、原文保留期、处理时间语义 | 写入 `PROCESSED` | 回填值集、保留期下界 | 未确认 |
|
||||
| Q8 | 逐类覆盖清单(积压摸底的类型分布依据) | — | 积压处置与逐类矩阵 | 未确认 |
|
||||
| Q9 | 清除执行方与 DDL 授权、方案 A/B 选型、分区能力 | 首选方案 A | `R_keep` 与清除边界 | 未确认 |
|
||||
| Q10 | 出站消费方、ACK 列语义、出站清理与去重契约 | — | 出站信箱 | 未确认 |
|
||||
| Q11 | 上游 `SEQN` 重置周期与业务身份的日期边界 | 不含日期边界 | 身份算法 | 未确认 |
|
||||
| Q12 | 积压批次「不再处理」的确认主体、审批留痕与跳过值集 | — | 积压跳过处置 | 未确认 |
|
||||
| Q13 | 日计划缺失可选字段的删除语义 | 保留未携带字段 | 快照合并 | 未确认 |
|
||||
| Q14 | 主/共享删除顺序与 EROR 回报义务 | 幂等原子级联 | 出站事件类型 | 未确认 |
|
||||
| Q15 | 上游是否会以同一业务身份改发正文(决定是否需要区分「重复」与「改发」) | 假定期望不可变(`C-4`) | 身份去重语义 | 未确认 |
|
||||
| Q16 | `FLID` 重用语义(决定版本是否按 incarnation 作用域) | 假定不复用(`C-21`) | 事件合并与 tombstone | 未确认 |
|
||||
|
||||
Q 的答复只在本表就地更新(补「结论」与日期),并触发 [README.md](README.md)「维护清单」的落地 4 步;不另开文件。
|
||||
+212
-177
@@ -1,80 +1,60 @@
|
||||
# msgexchange-v2 设计文档
|
||||
# msgexchange-v2 设计文档(管道机制)
|
||||
|
||||
## 1. 阅读说明
|
||||
本文件定义**管道机制**:记录模型、状态机、收报与水位、主泵与事务边界、回填、投递、失败恢复与维护作业。
|
||||
|
||||
本文说明模块如何协作、状态如何流转,以及失败后如何恢复。系统范围、存储归属和部署约束见 [architecture.md](architecture.md),不在这里重复。
|
||||
- 系统边界、模块职责、存储归属、架构决策:architecture.md
|
||||
- 前提 `PRE-x`、不变量 `INV-x`、声明边界 `CLM-x`:invariants.md
|
||||
- 对外承诺与要求 `C-x`、待确认事项 `Q`:contracts.md
|
||||
- 参数、指标、模块入口、错误分类:reference.md
|
||||
- 航班域模型与合并语义:flight-state.md
|
||||
|
||||
运营航班权威状态只落自有 PostgreSQL(`FLIGHT_SCHD` 及资源明细表),Redis 已彻底退出动态权威与全部写路径;ES 历史投影属暂缓范围,不参与当前设计。
|
||||
正文描述**目标设计**。交付状态不在正文标注:主张能否对外声明记在 invariants.md 的声明边界,实现进度在 Plane(ACM2)。
|
||||
|
||||
本文描述处理机制与流程;尚未交付的能力在本文件中明确标注,并以 §10 差异为准。航班状态规则统一由
|
||||
[运营航班状态设计](flight-state.md) 维护。
|
||||
## 1. 术语与持久化记录
|
||||
|
||||
**文档标记约定**(全文沿用,[message-lifecycle.md](message-lifecycle.md) 同):
|
||||
|
||||
- 【目标】= 设计要求,是否已交付以「实现差异」节为准;
|
||||
- 【现状】= 已按设计实现的行为;
|
||||
- 【缺口】= 尚未实现,条目登记在本文 §10 与 [message-lifecycle.md](message-lifecycle.md) §12;
|
||||
- 【待确认】= 依赖开放问题(Q 编号),当前取值只是假定。
|
||||
|
||||
正文只描述目标设计。需要点明交付状态时,用上述标记写一句,不展开解释——缺口的唯一清单是 §10 与 [message-lifecycle.md](message-lifecycle.md) §12。
|
||||
|
||||
### 1.1 符号与术语
|
||||
|
||||
| 符号 / 术语 | 语义 | 存储与字段 | 配置键 | 当前取值 | 约束与唯一定义处 |
|
||||
|---|---|---|---|---|---|
|
||||
| `W`(水位) | 信箱 ID 的连续上界:`(min, W]` 已全部读入自有 PG | `INBOX_CURSOR.COMMITTED_UP_TO` | — | 初值 0 | 只随新 ID 成功入队推进(永久空洞放行是唯一例外);[message-lifecycle.md](message-lifecycle.md) §5.1 |
|
||||
| `holeSince` | `W+1` 处空洞最早被观测到的时刻;无空洞时为 NULL | `INBOX_CURSOR.HOLE_SINCE` | — | NULL | 跨重启保留;旧空洞补齐后新空洞重新计时;[message-lifecycle.md](message-lifecycle.md) §5.1 |
|
||||
| `R`(超期补写期限) | 回填长期失败时的强制补写上限(按接收时间计) | 判据用 `PROC_STATE.RECEIVED_AT` | `msgx.pipeline.overdue-backfill` | 30 天 | 仅 `R ≤ R_keep`;**不保护重放窗口**(完整论证唯一见 [message-lifecycle.md](message-lifecycle.md) §5.2) |
|
||||
| `R_keep`(清除保留期) | 信箱行可被物理清除前的最短保留时间 | 库方侧 | 库方策略 | 待定(Q9) | `R_keep ≥ max(人工重放期限 + 人工处置期限, 审计期限, 回填重试上限)`;**仅在"标记 + 保留期"清除语义下成立**;[message-lifecycle.md](message-lifecycle.md) §6 |
|
||||
| `head-deadline` | 队头滞留上限,超过即毒丸升级 | 计时起点 `PROC_STATE.PROCESSING_STARTED_AT` | `msgx.pipeline.head-deadline` | 10 分钟 | 为空时以 `UPDATED_AT` 兜底;§3.2 |
|
||||
| 队头 | 最小的未完成消息(`PENDING` 与 `FAILED` 都占位) | `PROC_STATE`(`ORDER BY MSG_ID`) | — | — | 单线程串行处理,后续消息不得越过;§3.2 |
|
||||
| 终态 | `SUCCEEDED` / `SKIPPED` / `DEAD` | `PROC_STATE.STATE` | — | — | 到达后队列方可推进;§2.3 |
|
||||
| 回填意图 | 「还欠一次信箱标记」的持久化事实 | `PROC_STATE.BACKFILL_NEXT_AT` 非空 | — | 随终态写入 | 与终态同一条记录、同一条语句;§3.3 |
|
||||
| 处理标记 | 信箱行上表示「本系统已处理」的约定字段 | 信箱 `DATE_PROCESSED` / `STATUS` | `mailbox.processed-value` | `PROCESSED` | 只写空标记,不回撤、不覆盖;[message-lifecycle.md](message-lifecycle.md) §5.2 |
|
||||
|
||||
## 2. 数据与领域模型
|
||||
|
||||
### 2.1 持久化记录
|
||||
|
||||
所有内部表都属于自有 PostgreSQL;共享 MySQL 只保留约定的信箱读写边界。
|
||||
| 术语 | 语义 |
|
||||
|---|---|
|
||||
| `W`(水位) | 信箱 ID 的连续上界:`(min, W]` 已全部读入自有 PG;只随新 ID 成功入队推进(永久空洞放行是唯一例外)。 |
|
||||
| `holeSince` | `W+1` 处空洞最早被观测到的时刻;无空洞时为 NULL,跨重启保留。 |
|
||||
| 队头 | 最小的未完成消息(`PENDING` 与 `FAILED` 都占位)。 |
|
||||
| 终态 | `SUCCEEDED` / `SKIPPED` / `DEAD`;到达后队列方可推进。 |
|
||||
| 回填意图 | 「还欠一次信箱标记」的持久化事实,与终态同一条语句落库。 |
|
||||
| `R`、`R_keep`、处理标记 | 定义见 contracts.md(契约数值只在那里)。 |
|
||||
|
||||
| 记录 | 用途 | 关键约束 |
|
||||
|---|---|---|
|
||||
| `PROC_STATE` | 入站消息的处理状态、身份、重试次数、错误原因与回填事实 | `MSG_ID = CMINMSGS_ID` 主键防止重复入队;`IDENTITY_KEY` 唯一约束防止业务重复;按最小未完成消息 ID 取队头;`PROCESSING_STARTED_AT` 为 HOL 计时起点(口径见 §3.2);`BACKFILL_NEXT_AT` 非空 = 还欠一次回填(回填意图),`BACKFILL_AT` 非空 = 标记已确认,`BACKFILL_ABANDONED_AT/REASON` 非空 = 已停止自动重试(**不等于**标记已确认);`RECEIVED_AT` 复制自信箱接收时间,**可能为 NULL**,为 NULL 时 [message-lifecycle.md](message-lifecycle.md) §5.2 的超期兜底不生效。 |
|
||||
| `MSG_EVENT` | 等待投递的事件(outbox) | `EVENT_ID` 决定投递顺序;`TARGET` 区分 `KAFKA:msg` / `KAFKA:schd`;`PARTITION_KEY` 恒为 `FLID`;`EVENT_TYPE` 区分 UPSERT 与 TOMBSTONE。 |
|
||||
| `REQ_TRACK` | 上游请求及应答关联 | 保存请求类型、覆盖运营日、发送方、出站信箱 ID 与发送/完成时间;同类只允许一个开放请求。登记、超时与应答匹配尚未实现(§10)。 |
|
||||
| `REF_MASTER` | 静态参考数据(目标表) | `(RTYPE, RKEY)` 唯一;尚未建表,客户端与刷新流程见 user-stories.md US-13/US-14(US-14 两类映射的存储落点未定)。 |
|
||||
| `FLIGHT_SCHD` | 航班标量及单值异常字段 | `FLID` 主键;`OPERATION_DAY` 一经确定不可变;版本与最近消息 ID 用于追踪。变长资源集合存于 8 张资源明细表与 `FLIGHT_ROUTE_POINT`,规则见 [flight-state.md](flight-state.md) §2,不在此重复。 |
|
||||
| `INBOX_CURSOR` | 共享信箱消费水位 `W` 与空洞计时 `holeSince` | 单行游标;`W` 只随新 ID 成功入队推进(永久空洞放行是唯一例外),遇空洞即停;`HOLE_SINCE` 持久化空洞观测时刻,**进程重启不丢失计时**;[message-lifecycle.md](message-lifecycle.md) §5.1。 |
|
||||
| `PROC_STATE_HST` | 终态处理记录的归档目标 | 尚未建表;不得改写为共享库历史表。 |
|
||||
| `PROC_STATE` | 入站消息的处理状态、身份、尝试次数、错误原因与回填事实 | `MSG_ID = CMINMSGS_ID` 主键防重复入队;`IDENTITY_KEY` 唯一约束防业务重复;按最小未完成 `MSG_ID` 取队头;`BACKFILL_NEXT_AT` 非空 = 还欠一次回填,`BACKFILL_AT` 非空 = 标记已确认,`BACKFILL_ABANDONED_AT/REASON` 非空 = 已停止自动重试(**不等于**标记已确认);`RECEIVED_AT` 复制自信箱接收时间、**可能为 NULL**、仅用于对账与展示;`ENQUEUED_AT` 是本地入队时间、非空、是超期判据的唯一依据 `[G-ENQUEUED-AT]`。 |
|
||||
| `MSG_EVENT` | 等待投递的事件(outbox) | `EVENT_ID` 决定投递顺序(全局串行分配,见投递);`TARGET` 区分 `KAFKA:msg` / `KAFKA:schd`;`PARTITION_KEY` 恒为 `FLID`;`EVENT_TYPE` 区分 UPSERT 与 TOMBSTONE。`KAFKA:schd` 按 `FLID` 单行 upsert,只保留最新 `STATE_VERSION`。 |
|
||||
| `REQ_TRACK` | 上游请求及应答关联 | 状态 `PENDING / SENT / DONE / EXPIRED`;保存请求类型、覆盖运营日、发送方、出站信箱 ID 与发送/完成时间;**「同类只允许一个开放请求」的唯一键 = `(请求类型, 覆盖运营日, 发送方)`,且仅对开放状态生效**。登记、超时与应答匹配尚未实现 `[G-REQ-TRACK]`。 |
|
||||
| `REF_MASTER` | 静态参考数据(目标表) | `(RTYPE, RKEY)` 唯一;尚未建表,客户端与刷新流程见 user-stories US-13/US-14,US-14 两类映射的存储落点未定。 |
|
||||
| `FLIGHT_SCHD` | 航班标量及单值异常字段 | `FLID` 主键;`OPERATION_DAY` 一经确定不可变;版本与最近消息 ID 用于追踪。变长集合存于 8 张资源明细表与 `FLIGHT_ROUTE_POINT`,规则见 flight-state.md。 |
|
||||
| `INBOX_CURSOR` | 消费水位 `W`、空洞计时 `holeSince`、播种事实 `SEEDED_AT` | 单行游标;`W` 只随新 ID 成功入队推进,遇空洞即停;`HOLE_SINCE` 持久化空洞观测时刻,进程重启不丢计时。`SEEDED_AT IS NULL` **不等于**从未消费(已有库新增列后同样为 NULL)。 |
|
||||
| `SCHD_SNAP_LOG` | 日计划处理留痕 | 只追加、可重建,不参与状态决策;保留期见 reference。 |
|
||||
| `PROC_STATE_HST` | 终态处理记录的归档目标 | 尚未建表 `[G-PROC-HST]`;只归档到自有 PG 的目标表,不落共享库历史表。 |
|
||||
|
||||
字段与索引定义以 `src/main/resources/db/migration/V1__flight_state_baseline.sql` 为准;Oracle 11g 的迁移形态见 `src/main/resources/db/migration/oracle11g/`(占位,未接入任何 Flyway 配置)。报文原文仍从共享信箱读取,因此必须协调原文保留期,不能在消息尚需处理或重放时提前清理;生命周期、回填与清除契约的唯一定义见 [message-lifecycle.md](message-lifecycle.md)。
|
||||
字段与索引以 `src/main/resources/db/migration/` 下的 V1 基线为准(Oracle 11g 目录为占位,未接入 Flyway)。报文原文仍从共享信箱读取,原文保留期必须覆盖处理与重放窗口(`C-7`)。
|
||||
|
||||
### 2.2 消息、身份与决策
|
||||
## 2. 消息、身份与决策
|
||||
|
||||
`XmlCodec`(实装 `JacksonXmlCodec`)将 XML 解码为 `DecodedMessage`,包含 `SNDR / TYPE / STYP / SEQN / DTTM` 元数据、`MsgKind` 与业务载荷。解码失败区分 `MALFORMED`(报文非法,不重试)与可随 codec 修复的编码错误。`MsgKind` 为一等分派键:`Schd(RESP/DNLD/ADFT)`、`Flop`、`Fdel`、`Unsupported`。
|
||||
`XmlCodec`(实装 `JacksonXmlCodec`)把 XML 解码为 `DecodedMessage`,包含 `SNDR / TYPE / STYP / SEQN / DTTM` 元数据、`MsgKind` 与业务载荷。解码失败区分 `MALFORMED`(报文非法,不重试)与可随 codec 修复的 `CODEC_ERROR`。`MsgKind` 是一等分派键:`Schd(RESP/DNLD/ADFT)`、`Flop`、`Fdel`、`Unsupported`。
|
||||
|
||||
业务身份统一由 `Identity.of` 生成:
|
||||
业务身份统一由 `Identity.of` 生成:`SNDR | TYPE | STYP | SEQN`。接收时只按信箱 ID 去重,解码后才首次绑定业务身份;重试保留原有绑定,因此自身重试不会被判为重复。身份被另一条记录占用时,当前消息转 `SKIPPED`,记录 `duplicate-of:<id>`。是否加入日期边界取决于上游 `SEQN` 重置周期(见 `C-20`/`Q11`),默认关闭;上线后不能随意更换身份算法。
|
||||
|
||||
```text
|
||||
SNDR | TYPE | STYP | SEQN
|
||||
```
|
||||
**身份绑定是独立的幂等单语句**(`WHERE IDENTITY_KEY IS NULL`),不参与业务事务。它的前提是「报文不可变」(`PRE-7`):同一身份的重发不会被比对内容,若上游改发正文会被判为重复并跳过(`Q15`)。
|
||||
|
||||
接收时只按信箱 ID 去重;解码后才首次绑定业务身份。重试保留原有绑定,不能把自己判为重复消息。身份被另一条记录占用时,当前消息转为 `SKIPPED`,记录 `duplicate-of:<id>`。是否加入日期边界取决于上游序号重置周期,默认关闭(`SEQN` 的取值范围与回绕已由 `SIS_AODB_RMS-V0.1.md` §2.8.1 定义,重置周期见 Q11);上线后不能随意更换身份算法。身份绑定是**独立的幂等单语句**(`WHERE IDENTITY_KEY IS NULL`),不参与业务事务,见 §3.3 表 1。
|
||||
分派与落库由 `MessageProcessor` 协调:按 `MsgKind` 把已绑定身份的队头消息交给对应事务协调器(DNLD/RESP → `ScheduleProcessor`,ADFT → `AdftProcessor`,FLOP → `FlopProcessor`,FDEL → `FdelProcessor`,其余 → `FAILED(UNSUPPORTED)`)。这些处理器在 `PIPELINE_LOCK` 事务内读取当前完整态,调用纯领域决策逻辑得到下一完整态与待发事件,再统一落库并登记回填意图;它们不直接触碰 Kafka。领域决策逻辑不执行 I/O。
|
||||
|
||||
分派与落库由 `MessageProcessor` 统一协调:按 `MsgKind` 把已绑定身份的队头消息交给对应事务协调器(DNLD/RESP → `ScheduleProcessor`,ADFT → `AdftProcessor`,FLOP → `FlopProcessor`,FDEL → `FdelProcessor`,其余 → `FAILED(UNSUPPORTED)`)。这些 Processor 在 `PIPELINE_LOCK` 事务内读取当前完整态,调用纯领域决策逻辑得到下一完整态与待发事件,再统一落库并登记回填意图;它们不直接触碰 Kafka。处理终态与业务变更在同一事务边界提交,**该结论只对处理器产出的「业务型终态」成立**——非业务型终态不涉及航班表,不取 `PIPELINE_LOCK`。完整边界唯一见 §3.3 表 1。
|
||||
|
||||
### 2.3 状态与错误分类
|
||||
## 3. 状态与错误分类
|
||||
|
||||
```text
|
||||
处理:PENDING / FAILED → SUCCEEDED(成功)
|
||||
→ SKIPPED(业务重复已实现;忽略/无匹配分支见 US-04/US-06,尚未实现)
|
||||
→ SKIPPED(业务重复已实现;忽略 / 无匹配分支见 US-04/US-06,尚未实现)
|
||||
→ FAILED(等待退避重试)
|
||||
→ DEAD(MALFORMED / PROTOCOL / EXHAUSTED,均需人工处置)
|
||||
|
||||
投递:PENDING → SENT
|
||||
→ PENDING(退避后重试)
|
||||
→ DEAD(重试耗尽,保留记录作 DLQ)
|
||||
→ DEAD(重试耗尽,记录保留作 DLQ)
|
||||
```
|
||||
|
||||
`SUCCEEDED / SKIPPED / DEAD` 是处理终态,不再阻塞后续消息;`FAILED` 不是终态,仍占据队头。`DEAD` 表示需要处置,不等于业务成功。
|
||||
@@ -86,35 +66,71 @@ SNDR | TYPE | STYP | SEQN
|
||||
| `CODEC_ERROR` | 解码能力问题,退避重试;修复后允许重放。 |
|
||||
| `UNSUPPORTED` | 处理器或快照能力未实现,按可恢复失败处理,不直接当作非法报文;仍受重试上限约束。 |
|
||||
| `INFRA` | 基础设施或执行异常,退避重试。 |
|
||||
| `EXHAUSTED` | 重试耗尽或滞留超时,转 `DEAD`,人工复核后允许重放。 |
|
||||
| `EXHAUSTED` | 重试耗尽,转 `DEAD`,人工复核后允许重放。 |
|
||||
|
||||
重试次数用尽时统一转 `DEAD(EXHAUSTED)`:`ERROR_CLASS` 被覆写为 `EXHAUSTED`,**原始错误类别不再保留**(`LAST_ERROR` 保留原因文本)。由于重放白名单包含 `EXHAUSTED`,这类记录仍可人工重放(§6.1)。
|
||||
重试次数用尽时统一转 `DEAD(EXHAUSTED)`:`ERROR_CLASS` 被覆写为 `EXHAUSTED`,原始错误类别不再保留(`LAST_ERROR` 保留原因文本)。重放白名单包含 `EXHAUSTED`,这类记录仍可人工重放。
|
||||
|
||||
## 3. 收报与主泵
|
||||
## 4. 收报与水位
|
||||
|
||||
### 3.1 收报
|
||||
### 4.1 收报流程
|
||||
|
||||
`InboxPoller` 默认每秒按 ID 升序、有限批次(`claim-batch`,默认 50)读取水位之后的信箱记录(`ID > W`,**不以处理标记为谓词**),在自有 PG 建立 `PENDING` 并把水位推进到连续上界;入队与水位推进在同一 PG 事务内提交,重复扫描幂等、中断后重扫补建。收报层不解析业务载荷,也不回填已处理标记。
|
||||
`InboxPoller` 按 ID 升序、有限批次读取水位之后的信箱记录(`ID > W`,**不以处理标记为谓词**),在自有 PG 建立 `PENDING` 并把水位推进到连续上界。每轮:
|
||||
|
||||
收报流程、空洞判定(遇空洞即停 / 超期放行)及其代价与前提(Q2 承诺、`max-commit-delay` 老化阈值、单实例排他)**唯一定义于** [message-lifecycle.md](message-lifecycle.md) §5.1,本节不重复。水位不是已处理标记。
|
||||
1. 读取游标 `(W, holeSince)`。信箱不可读时记日志、等下一轮,**不动水位**——这是基础设施失败,不能当成「没有新消息」。
|
||||
2. 取 `ID > W` 的升序前 `PARAM:msgx.pipeline.claim-batch` 行。
|
||||
3. 从 `W+1` 起逐 1 数,求连续上界;遇到第一个缺号即停止计数。
|
||||
4. 空洞判定(仅当本批存在「缺号之后的行」时才可能成立):
|
||||
- 缺号首次被观测到 → `holeSince = now`;
|
||||
- `now − holeSince < PARAM:msgx.pipeline.max-commit-delay` → 水位停在缺号前,**本批缺号之后的行一律不入队**(否则晚提交的较小 ID 会排到它们后面,破坏 FIFO);
|
||||
- `now − holeSince ≥ PARAM:msgx.pipeline.max-commit-delay` → 判定为永久空洞,水位放行到「缺号后第一行 − 1」并清空 `holeSince`。放行**只跳过空洞本身,不越过任何已存在的行**。
|
||||
5. 在同一个 PG 事务内:对水位以内的每一行 `insertIfAbsent(MSG_ID, RECEIVED_AT, ENQUEUED_AT)` 并写回 `(W, holeSince)`;主键冲突表示已入队(重复扫描与兼容入口并发都安全),不计入、不报错。
|
||||
6. 提交。本批因空洞或批次上限未入队的行留待下一轮——**每轮最多解决一个空洞**。
|
||||
|
||||
兼容 HTTP 入口执行“写入共享信箱 → PG 入队”。两步不在同一事务中:信箱成功而 PG 失败时,原文不能丢失,由轮询补建;客户端失败重试可能再次写信箱,业务身份去重仍然必需。
|
||||
`holeSince` 落在 `INBOX_CURSOR.HOLE_SINCE`,进程重启不丢计时。旧空洞补齐后出现的新空洞从新观测时刻重新计时,不继承旧等待时间。
|
||||
|
||||
兼容入口只写 `PROC_STATE`、不参与水位,登记的行因此超出水位,主泵在水位追平前不领取(§3.2 步骤 2);完整语义与运维含义唯一见 [message-lifecycle.md](message-lifecycle.md) §5.1。
|
||||
**代价(必须接受并观测)**:水位遇空洞即停意味着空洞之后的所有消息最多要等一个老化窗口才能入队;自增回滚等会在 ID 序列留下永久空位,每出现一个永久空位就是一次等长的入队停摆,空位频繁时有效吞吐按比例下降。运行期必须观测永久空洞计数与水位滞后(指标见 reference)。
|
||||
|
||||
### 3.2 主泵调度
|
||||
### 4.2 发现完整性依赖与扫描路径
|
||||
|
||||
每次 `Pump.tick` 只围绕最小未完成消息(`PENDING` 与 `FAILED` 都占队头):
|
||||
水位的有效性依赖 `PRE-2`、`PRE-3`(`C-1`/`C-2`/`C-3`)。承诺缺失时 `W` 只是快路径提示,不足以证明该区间收齐。ID 分配 → 事务可见时延上界必须由库方直接给出,**不可由 SIS 报文 `Expiry` 推导**。
|
||||
|
||||
| 路径 | 目的 | 谓词 | 状态 |
|
||||
|---|---|---|---|
|
||||
| 快路径(日常) | 发现水位之后的新消息 | `ID > W ORDER BY ID ASC LIMIT claim-batch` | 已实现 |
|
||||
| 只读迟到检测 | 复查被放行的空洞 ID 是否后来真的出现 | 进程内监视队列 + 批量存在性检查 | **临时观测**:只计数与告警,不补入队;监视队列有界、重启丢失,且只覆盖「曾被放行过的空洞 ID」。其存在理由是「把静默丢失变成可观测事实」,**退出条件**是 G1 补偿扫描交付或 `Q2` 承诺成立,届时删除该机制,不保留为长期能力。 |
|
||||
| 补偿扫描 `[G1]` | 发现「提交晚于水位推进」的迟到行并安全处置 | 按周期重扫 `W` 之前一个窗口(宽度由 `C-2` 决定)内的 ID 区间 | 未实现 |
|
||||
|
||||
在补偿扫描交付前,「较小 ID 迟提交」没有补入队机制:快路径只读 `ID > W`,水位一旦越过某个 ID,该 ID 之后到达的消息永远不会被发现。**补偿扫描解决的是「不丢」,不是「不越序」**——补入队时更大的 ID 可能已经处理完,顺序已经越了;「不越序」只能由 `PRE-2` 承诺支撑(见 CLM-1 / CLM-2)。
|
||||
|
||||
### 4.3 切流播种
|
||||
|
||||
若信箱已有存量(典型情况是最老分区已被清除、`MIN(ID)` 远大于 1),从 `W=0` 启动会先把 `ID=1` 判成空洞、白等一个老化窗口。是否跳过存量属于**切流决策**,因此不做默认选择:只有显式配置 `PARAM:msgx.pipeline.cutover-watermark` 才播种,取值 `min`(读现存全部)/ `zero`(从 0 按空洞规则)/ `max`(跳过当前可见存量)/ 具体 ID。升级实例(已有水位或已有处理记录)拒绝重新播种;播种事实与水位同语句落库(`SEEDED_AT`)。
|
||||
|
||||
### 4.4 兼容 HTTP 入口
|
||||
|
||||
`POST /cminmsgs/send` 执行「写入共享信箱 → PG 入队」,两步不在同一事务:信箱成功而 PG 失败时原文仍在信箱中,由轮询补建;客户端失败重试可能再次写信箱,业务身份去重仍然必需。该入口直接写 `PROC_STATE`、不读不推水位,登记的行因此**超出水位**;主泵只领 `MSG_ID ≤ W`,所以在水位追平前不会被处理——顺序不受影响,代价是延迟到追平,且必须让收报轮询运行。响应语义见 `C-28`。
|
||||
|
||||
### 4.5 单实例与水位
|
||||
|
||||
信箱读取不加锁,水位是单行覆盖写,本设计只在单活动实例下成立(`PRE-5`)。多实例并发收报会让水位互相覆盖(覆盖回退只会造成重复扫描,不会丢消息,但空洞计时会失真),必须先有实例级排他。
|
||||
|
||||
## 5. 主泵调度与单条处理
|
||||
|
||||
### 5.1 调度
|
||||
|
||||
每次 `Pump.tick` 只围绕最小未完成消息:
|
||||
|
||||
1. 无队头:按轮询间隔休眠。
|
||||
2. 队头超出水位(`msgId > W`):**不领取**,休眠到下一轮。这类行只可能来自兼容入口的直接登记;允许领取会让它越过尚未入队的较小 ID。会打印一条**限流 WARN**(仅在水位值变化时打一次)。
|
||||
3. 队头为 `FAILED` 且已毒丸(`attempts ≥ max-attempts`,或 `now − 计时起点 ≥ head-deadline`):转 `DEAD(EXHAUSTED)`,终态与回填意图同一条 UPDATE 落库,**不做跨库写**。**该分支只写 `PROC_STATE`,不取 `PIPELINE_LOCK`、不在处理器事务内,也不在 `MessageLifecycleGate` 内**(见 §3.3 表 1 与 §6.1)。
|
||||
2. 队头超出水位(`msgId > W`):**不领取**,休眠到下一轮。这类行只可能来自兼容入口的直接登记;允许领取会让它越过尚未入队的较小 ID。会打印一条限流 WARN(仅在水位值变化时打一次)。
|
||||
3. 队头为 `FAILED` 且尝试次数达上限:转 `DEAD(EXHAUSTED)`,终态与回填意图同一条 UPDATE 落库,**不做跨库写**。该分支只写 `PROC_STATE`,不取 `PIPELINE_LOCK`、不在处理器事务内,也不在 `MessageLifecycleGate` 内。
|
||||
4. 队头为 `FAILED` 且未到 `next_attempt_at`:休眠到可重试时刻,不处理后续消息。
|
||||
5. 其余(新消息或退避到期的重试):记录 `PROCESSING_STARTED_AT`(仅首次)后调用 `MessageProcessor.processOne`,失败迁移在该边界内完成。
|
||||
5. 其余(新消息或退避到期的重试):调用 `MessageProcessor.processOne`,失败迁移在该边界内完成。
|
||||
|
||||
维护作业由独立 job 线程调度、不占用消息循环(§6.1)。**所有取时统一经注入 `Clock`**(收报空洞老化、主泵调度、处理器落库时间、回填重试、作业切日),不使用系统时钟;HOL deadline 以首次处理时写入的 `PROCESSING_STARTED_AT` 为稳定起点,**该列为空时(V3 迁移之前的存量行)以 `UPDATED_AT` 兜底**;人工重放会清空 `PROCESSING_STARTED_AT`,由新一轮首次处理重新记录。
|
||||
**队头滞留只告警、不迁移状态**:`PARAM:msgx.pipeline.head-deadline` 用于 `msgloop` 健康告警(「队头滞留超阈值」),**不作为终态判据**。理由:按尝试上限与退避表,正常重试包络远小于该阈值,它只在单次处理长时间卡住时触发;而处理卡死应由外部调用的有界超时兜底,用超时把消息直接推入 `DEAD` 会绕过人工复核,并制造与人工重放并发的旁路写入者。
|
||||
|
||||
### 3.3 单条处理
|
||||
**所有取时统一经注入 `Clock`**(收报空洞老化、主泵调度、处理器落库时间、回填重试、作业切日),不使用系统时钟。
|
||||
|
||||
### 5.2 processOne
|
||||
|
||||
```text
|
||||
processOne(head):
|
||||
@@ -137,169 +153,188 @@ processOne(head):
|
||||
6. 结束:主泵不做回填;回填意图已随终态落库,由扫描补写信箱标记
|
||||
```
|
||||
|
||||
**表 1 事务边界**(哪些动作在一个事务里、哪些不是):
|
||||
- 原文缺失归为 `MALFORMED`;读取异常按基础设施失败进入重试,与「原文缺失」区分。
|
||||
- 忽略规则(`LDM / REGN / RSTA / EROR` → `SKIPPED`)尚未实现(`[G-IGNORE]`,US-04);类型未覆盖不等于报文非法:忽略类报文在规则实现前不按 `MALFORMED` 处理。
|
||||
- **主泵不回填**:终态与回填意图由同一条 UPDATE 落库,回填一律由扫描驱动,不占用 FIFO 关键路径。回填只需消息 ID,缺 META 或解码失败的死信同样可补写。影子环境禁写。
|
||||
|
||||
| 动作 | 显式事务 | 持 `PIPELINE_LOCK` | 触及航班表/事件 | 原子性来源 |
|
||||
### 5.3 事务边界
|
||||
|
||||
| 动作 | 显式事务 | 持 `PIPELINE_LOCK` | 触及航班表 / 事件 | 原子性来源 |
|
||||
|---|---|---|---|---|
|
||||
| 收报入队(`insertIfAbsent` + `cursor.save`) | 是 | 否 | 否 | 同库事务 |
|
||||
| 身份首次绑定 | 否 | 否 | 否 | 单语句 + `uk_proc_identity` |
|
||||
| 身份首次绑定 | 否 | 否 | 否 | 单语句 + 唯一约束 |
|
||||
| 业务型终态(处理器产出 `SUCCEEDED`) | 是 | 是 | 是 | 同库事务:航班变更 + 事件 + 终态 + 回填意图 |
|
||||
| 非业务型终态(`MALFORMED` / `PROTOCOL` / `SKIPPED` / `EXHAUSTED`) | 否 | 否 | 否 | 单语句(终态与回填意图同一条 UPDATE) |
|
||||
| 毒丸升级 `DEAD(EXHAUSTED)`(§3.2 步骤 3) | 否 | 否 | 否 | 单语句 |
|
||||
| 航班历史清理的物理删除 | 是 | 是(`INV-18`) | 是 | 同库事务:复查判据 + 归档成功后删除 |
|
||||
| 回填(信箱标记 + `BACKFILL_AT`) | 否 | 否 | 否 | 跨库两次单写;幂等可重跑 |
|
||||
| 人工重放(批量改回 `PENDING`) | 否 | 否 | 否 | 单语句批量;`MessageLifecycleGate` 与回填互斥 |
|
||||
|
||||
结论:**「航班变更与处理终态同事务」只对业务型终态成立**;非业务型终态不涉及跨表一致性,因此不需要 `PIPELINE_LOCK`,但终态与回填意图仍由同一条 UPDATE 保证不分离。
|
||||
结论:「航班变更与处理终态同事务」只对业务型终态成立。`PIPELINE_LOCK` 的竞争写者是**航班历史清理**(`INV-18`),不是别的处理器线程;没有第二写者时该锁不产生额外串行度。
|
||||
|
||||
- 原文缺失归为 `MALFORMED`;读取异常不能伪装成“缺失”,应进入基础设施重试。
|
||||
- 忽略规则(`LDM / REGN / RSTA / EROR` → `SKIPPED`)尚未实现(§10);不能因类型未覆盖就把合法忽略报文当非法报文处理。
|
||||
- **主泵不回填**:终态与回填意图由同一条 UPDATE 落库,回填**一律由扫描驱动**(跨库写不能占用 FIFO 关键路径);四种结果、退避、超期强制补写与放弃恢复、调度周期口径全在 [message-lifecycle.md](message-lifecycle.md) §5.2(US-09/Q7)。回填只需消息 ID,缺 META 或解码失败的死信同样可补写;【缺口】影子环境禁写(§10)。
|
||||
### 5.4 历史积压
|
||||
|
||||
## 4. 日计划快照与请求匹配
|
||||
信箱中的成规模存量(上线前遗留、停机累积)**不是特殊模式**:它逐条走与日常完全相同的 FIFO 路径。
|
||||
|
||||
### 4.1 快照发布
|
||||
- 顺序由 `MSG_ID` 决定,不由执行方式决定。入队与处理由不同线程驱动、可以并发,「先入队后处理」只是可选的运维规程,不是正确性前提;系统不提供「只入队」模式。
|
||||
- 不加速、不分流、不走旁路:不允许并行队头,也不允许实时消息跳过积压。
|
||||
- 尝试上限、退避与队头滞留告警对积压同样生效,不因积压而放宽。
|
||||
- 经确认不再处理的行置 `SKIPPED` 并记录原因,到达终态后走回填通道;不存在「整段 DELETE」的快速通道(授权与留痕见 `C-27`/`Q12`)。
|
||||
- 消化期间的可观测项与完成时限口径见 reference 与 CLM-9:**扫描周期不是完成时限**。
|
||||
|
||||
## 6. 回填
|
||||
|
||||
### 6.1 事实与扫描谓词
|
||||
|
||||
终态落库时登记回填意图;标记回写由扫描驱动,跨库单写、幂等可重跑。扫描谓词(与实现一一对应):
|
||||
|
||||
```text
|
||||
STATE ∈ {SUCCEEDED, SKIPPED, DEAD} -- 终态
|
||||
AND BACKFILL_AT IS NULL -- 标记尚未确认
|
||||
AND BACKFILL_ABANDONED_AT IS NULL -- 未放弃(放弃行可人工恢复)
|
||||
AND ( BACKFILL_NEXT_AT IS NULL -- 异常兜底:终态行没有退避时间
|
||||
OR BACKFILL_NEXT_AT ≤ NOW -- 退避到期
|
||||
OR ENQUEUED_AT < NOW − R ) -- 进入强补写窗口,覆盖退避
|
||||
ORDER BY BACKFILL_ATTEMPTS ASC, MSG_ID ASC -- 公平轮转,永久失败行不占满批次
|
||||
LIMIT PARAM:msgx.pipeline.backfill-batch
|
||||
```
|
||||
|
||||
超期判据使用**本地入队时间**(`ENQUEUED_AT`),不使用信箱的 `RECEIVED_AT`:后者来自外部时钟,前偏会在「打标即清除」语义下造成提前清除(`PRE-4`)。`ENQUEUED_AT` 落地前,该分支沿用 `RECEIVED_AT` 并在声明边界标注时钟偏斜风险 `[G-ENQUEUED-AT]`。
|
||||
|
||||
### 6.2 四种结果与放弃
|
||||
|
||||
| 结果 | 判定 | 处置 |
|
||||
|---|---|---|
|
||||
| 写入成功 | 标记为空、写入 1 行 | 记 `BACKFILL_AT`,不再重试 |
|
||||
| 早已有标记 | 写入 0 行且信箱行存在 | **视为成功**,不覆盖已有值,记 `BACKFILL_AT` |
|
||||
| 信箱行不存在 | 写入 0 行且信箱行不存在 | **立即放弃自动重试**(原因 `MISSING_ROW`)并告警。终态行存在而信箱行不存在,只可能是该行在入队后被删除(永久空洞 ID 从不入队,不会进入本扫描) |
|
||||
| 暂时性故障持续超期 | 超时 / 连接失败持续到 `R` 仍未打标 | **停止自动重试**(原因 `TRANSIENT_DEADLINE`)并告警;`R` 之前只退避重试,**不按尝试次数放弃**;保留人工恢复能力 |
|
||||
|
||||
**放弃 ≠ 标记已确认**:放弃行不写 `BACKFILL_AT`,因此不满足 `C-8` 的清除前提,库方不得据此清除;放弃清单需人工对账确认后才可用于清除判定。
|
||||
|
||||
### 6.3 `R` 的作用
|
||||
|
||||
`R`(契约值见 contracts)有两个作用:
|
||||
|
||||
1. **取消退避**:已终态但超期未打标的行,每轮扫描都被尝试,不再等退避到期;
|
||||
2. **暂时性故障的放弃期限**:超时、连接失败这类暂时性故障**在 `R` 之前只退避重试、不放弃**;到 `R` 仍未打标才停止自动重试、记入放弃清单并告警。
|
||||
|
||||
放弃判据用**时间**而不是**尝试次数**:按退避表(档位 ≤ 8 秒)与扫描周期,固定次数的实际上限只有几十分钟,一次小时级的共享库故障会把全部待回填行一次性判死,随后必须成批人工恢复——这是必须避免的失败模式。`PARAM:msgx.pipeline.backfill-max-attempts` 因此不再是放弃判据,只保留为单行重试的告警阈值 `[G-BACKFILL-ABANDON-BYTIME]`。
|
||||
|
||||
关于「最终一定打标」,准确表述是三段,缺一不可:
|
||||
|
||||
1. 退避重试(`R` 之前不放弃,参数见 reference);
|
||||
2. 到 `R` 仍失败则停止自动重试、告警,进入放弃清单,保留人工恢复(`reopen`);
|
||||
3. `C-8` 允许以「放弃清单 + 人工确认」作为清除判定,避免一行永久卡住整个分区。
|
||||
|
||||
两个边界要说清:`MISSING_ROW`(信箱行不存在)是**确定性结论**,立即放弃,不受 `R` 保护;`R` 仍然**不保护重放窗口**——`R` 与 `R_keep` 只要求 `R ≤ R_keep`,重放窗口的唯一保证来源是 `C-7`。
|
||||
|
||||
重放窗口的保护只有两条路:约定保留期(`C-6` + `C-7`,目标前提),或另设原文保留通道(`[G-REPLAY-CHANNEL]`,尚未设计)。若库方清除语义是「打标即可清除」,则当天打标的原文当天即可被清除,增大 `R` 无效。
|
||||
|
||||
## 7. 日计划快照与请求匹配
|
||||
|
||||
### 7.1 快照发布
|
||||
|
||||
`SCHD-DNLD` 与 `SCHD-RESP` 共用 `ScheduleProcessor.applyScheduleRecords`:
|
||||
|
||||
1. **重放判定**:`PROC_STATE` 已存在成功终态 → 幂等成功,仅追加留痕,不重复写入。
|
||||
2. **整包校验**:声明记录数、航班标识与运营日推导等校验失败 → 整包 `DEAD(PROTOCOL)`,不写半包,既有状态保持不变。
|
||||
3. **事务写入**:锁内按 `FLID` 点查归属日,发现同一航班跨运营日即整包回滚并 `DEAD(PROTOCOL)`;通过后合并写主表与资源明细。报文未携带的航班不因本次日计划报文被删除。
|
||||
4. **提交结果**:同一事务保存 `KAFKA:schd` / `KAFKA:msg` 事件、置消息 `SUCCEEDED` 并预登记回填意图;提交后信箱回填由扫描承接(§3.3),留痕在事务外追加。
|
||||
4. **提交结果**:同一事务保存 `KAFKA:schd` / `KAFKA:msg` 事件、置消息 `SUCCEEDED` 并预登记回填意图;提交后信箱回填由扫描承接,留痕在事务外追加。
|
||||
|
||||
单事务保证未提交变更整体回滚。消息重放由 `PROC_STATE` 的消息 ID 与业务身份控制;版本号不能单独证明消息身份。
|
||||
消息重放由 `PROC_STATE` 的消息 ID 与业务身份控制;版本号不能单独证明消息身份。
|
||||
|
||||
**应答守卫仍是缺口**:`RESP` 应匹配开放 `RQFD` 请求(无匹配、过期或报文早于发送时间则不更新快照);当前 RESP 与 DNLD 无差别进入快照写入(§4.2/§10),不能视为 RESP 匹配闭环。
|
||||
**应答守卫 `[G-RESP-GUARD]`**:`RESP` 应匹配开放请求(无匹配、过期或报文早于发送时间则不更新快照);当前 `RESP` 与 `DNLD` 无差别进入快照写入,因此当前不构成匹配闭环。
|
||||
|
||||
### 4.2 上游请求与静态数据
|
||||
### 7.2 上游请求与静态数据
|
||||
|
||||
`REQ_TRACK` 表与仓储已存在(状态 `PENDING / SENT / DONE / EXPIRED`),但没有运行时协调器:出站 `COUTMSGS` 适配、请求编码、超时与应答匹配均未实现(`XmlCodec.encodeRqrd` 只是占位)。本节是目标机制,不是现状。
|
||||
|
||||
请求生命周期目标:
|
||||
`REQ_TRACK` 表与仓储已存在,但没有运行时协调器:出站 `COUTMSGS` 适配、请求编码、超时与应答匹配均未实现(`[G-REQ-TRACK]`)。目标机制:
|
||||
|
||||
```text
|
||||
REGISTERED → SENT → WAITING → DONE
|
||||
PENDING → SENT → DONE
|
||||
└──→ EXPIRED
|
||||
```
|
||||
|
||||
- 注册同类新请求前使旧开放请求过期;只有 `COUTMSGS` 写入确认后才标记 `SENT` 并关联出站记录;写信箱成功但本地未确认的情况需要补偿与去重,不能无条件重新发送。
|
||||
- 应答优先按已确认的回显字段精确匹配;回显契约未确认时的降级匹配(同类开放请求且 `DTTM ≥ sentAt`)存在跨代误配风险,必须明确接受并审计,不能宣称精确关联。比较前统一时区和时间单位。
|
||||
- 应答优先按已确认的回显字段精确匹配;降级匹配的跨代误配风险必须明确接受并审计(`C-23`)。
|
||||
- 时间比较统一时区与单位,并需定义时钟偏斜容忍;容忍判据未定(`Q5`),在定义前不得把降级匹配描述为精确关联。
|
||||
- 参考应答写入自有 `REF_MASTER`(尚未建表),日计划应答走快照流程;请求完成必须在相应数据处理成功之后,超时和迟到应答不能修改已关闭请求对应的状态。
|
||||
- 出站承诺只到落信(`C-24`、CLM-8);主为 EROR 回报义务见 `C-25`。
|
||||
|
||||
请求与参考数据的交付范围见 user-stories.md US-08/US-13/US-14 与本文 §10。
|
||||
## 8. 事件投递
|
||||
|
||||
## 5. 事件投递
|
||||
### 8.1 普通事件(`KAFKA:msg`)
|
||||
|
||||
### 5.1 普通事件
|
||||
`Dispatcher` 从 `MSG_EVENT` 取待发事件。**保序边界是 `FLID`**(与分区键一致):同一 `FLID` 内按 `EVENT_ID` 保序,队头失败即暂停该 `FLID`;不同 `FLID` 之间不互相阻塞,也不承诺跨 `FLID` 顺序。消费者按 `(FLID, STATE_VERSION, UPDATED_AT)` 防旧覆盖新。
|
||||
|
||||
`Dispatcher` 按 `TARGET` 读取最小未发送 `EVENT_ID`(当前逐条投递只处理 `KAFKA:msg`)。队头退避未到期时,该目标停止推进;发送确认后才标记 `SENT`,失败记录次数并按退避推后,达到上限转 `DEAD`(记录保留作 DLQ)。所有外部调用需要有界超时,避免阻塞整个投递线程。
|
||||
本批领取的 `EVENT_ID` 集合在**读取时刻冻结**:发送与标记只作用于这批事件,期间新提交的事件留待下一轮,不参与本批,也不被本批的「完成」带走。
|
||||
|
||||
投递是至少一次:Kafka Broker 或其他投递目标已接受、但本地未标记成功时可能重发;目标端接受不等于业务消费者已消费。Kafka 生产约束沿用架构决策 D3(architecture.md §7),但生产者幂等不替代应用层事件去重;跨重启的事件身份和消费方去重契约仍需落实。共享出站信箱也必须单独解决重复写入,不能假设 Kafka 的保证适用于 MySQL。真实 Kafka 适配器尚未实现(`DeliveryPort` 仅有 stub),投递闭环须先交付适配与配置强制校验。
|
||||
`EVENT_ID` 由全局串行分配产生:事件生产者在事务内写 outbox,主泵单线程,历史清理与主泵互斥(`INV-18`),因此**分配顺序 = 提交顺序**,不存在「已提交的较大 ID 先于未提交的较小 ID 被投递」。
|
||||
|
||||
### 5.2 `schd` 聚合
|
||||
发送确认后才标记 `SENT`,失败记录次数并按退避推后,达到上限转 `DEAD`(记录保留作 DLQ)。所有外部调用需要有界超时,避免阻塞投递线程。
|
||||
|
||||
`KAFKA:schd` 不进入逐条投递循环,只由 `flushSchd` 发送:
|
||||
投递是至少一次:Broker 或其他目标已接受但本地未标记成功时可能重发;目标端接受不等于业务消费者已消费。Kafka 生产约束沿用 architecture 的 D3,生产者幂等不替代应用层事件去重。
|
||||
|
||||
1. 到期领取批次:`mergePendingSchd` 按 `FLID` 合并未发事件,每个 `FLID` 只保留最新 `STATE_VERSION`,批次大小受 `flush-limit` 约束。
|
||||
2. 逐条发送:UPSERT 发送该 `FLID` 的最新整态(key = `FLID`);TOMBSTONE 发送 null 值删除通知。
|
||||
3. 成功后把本批被代表的事件(含被最新版本合并压掉的旧事件)一起标记完成,推进 `lastFlush`;失败的单条增加次数并退避,达到上限转 `DEAD`。
|
||||
### 8.2 `schd` 聚合
|
||||
|
||||
默认聚合周期 3 秒、批上限 500。它提供最新状态通知,不保留每次中间变化;`KAFKA:msg` 与 `KAFKA:schd` 之间不承诺顺序。
|
||||
`KAFKA:schd` 只提供最新状态通知,不保留每次中间变化,因此 outbox 按 `FLID` 单行 upsert:同一 `FLID` 只保留最新 `STATE_VERSION` 的事件与投递状态。两条写规则:
|
||||
|
||||
## 6. 失败恢复与维护作业
|
||||
- **只进不退**:仅当新事件的 `STATE_VERSION ≥` 行内现有版本才覆盖,防止迟到的旧事件把新状态压回去。该合并规则以 `C-21`(`FLID` 在保留期内不复用)为前提。
|
||||
- **条件标记**:发送成功后按**读取时刻的版本**做条件标记(`WHERE STATE_VERSION = <本批版本>`);该行若期间已被更新的版本覆盖,则不标记,留待下一轮重发。
|
||||
|
||||
### 6.1 失败、重试与重放
|
||||
发送时:
|
||||
|
||||
`ProcFailure` 与 `FailureScheduler` 统一处理侧失败落账,投递侧(`Dispatcher`)按同一套次数与退避规则迁移事件。默认最多 5 次(attempts ≥ 5 转 `DEAD`,不再计算下次重试);退避表默认 1、2、4、8 秒,**启动自检强制档位数 = max-attempts − 1**,"表里有档但永不触发"的配置不可能出现;单档封顶 `backoff-cap-ms`(默认 60 秒)仅对超过封顶的档位生效。时间经可注入 `Clock` 判定。
|
||||
1. 到期领取批次:按 `FLID` 取未发送行,批次大小受 `PARAM:msgx.schd.flush-limit` 约束;
|
||||
2. 逐条发送:UPSERT 发送该 `FLID` 的最新整态(key = `FLID`);TOMBSTONE 发送 null 值删除通知;
|
||||
3. 成功后按上条规则标记完成并推进 `lastFlush`;失败按退避推后,达到上限转 `DEAD`。
|
||||
|
||||
失败必须在持有具体消息、事件或批次的位置记录,外层循环只做兜底日志和等待,不重复增加次数。线程中断应恢复中断标记并向上传递;不把 JVM `Error` 当普通业务失败捕获。
|
||||
默认聚合周期与批上限见 reference。`KAFKA:msg` 与 `KAFKA:schd` 之间不承诺顺序。`schd` 行与 `msg` 行共用 `MSG_EVENT`,靠 `TARGET` 区分。
|
||||
|
||||
`ReplayService` 只允许 `CODEC_ERROR / UNSUPPORTED / INFRA / EXHAUSTED` 从 `FAILED / DEAD` 回到 `PENDING`:重置 `ATTEMPTS=0`、`NEXT_ATTEMPT_AT=NULL`、`PROCESSING_STARTED_AT=NULL`,**不重置 `IDENTITY_KEY`**(保留身份,避免重放时把自己判成重复消息)。它按错误类**全局批量**重放,尚无按记录预检、操作审计与管理入口(US-10)。重放与回填通过 `MessageLifecycleGate` 在同一实例内互斥,避免「旧回填给已重新入队的消息写标记」;【缺口】**毒丸升级路径不在该 gate 内**(§3.2 步骤 3),该竞态窗口登记于 §10。旧消息进入终态后后续消息可能已执行,**重新入队不等于恢复历史顺序**;人工重放前必须评估状态覆盖和版本保护,不能直接批量重放到生产。
|
||||
### 8.3 清理
|
||||
|
||||
**维护作业**:`JobRunner` 用独立 daemon 线程每 30 秒触发 `BackfillService.sweep`(调度周期;扫描谓词、超期、放弃与公平轮转口径唯一见 [message-lifecycle.md](message-lifecycle.md) §5.2),每天机场时区 03:30 后触发一次 `HistorySweepJob`(§6.2)。回填意图在写终态的同一条语句里登记在 `PROC_STATE`(`BACKFILL_NEXT_AT/ATTEMPTS/ERROR`)。作业不参与消息 FIFO,也不使到期消息饥饿。
|
||||
已 `SENT` 的事件行按 `PARAM:msgx.pipeline.event-retention` 由维护作业清理;`DEAD` 行保留作 DLQ,人工处置后再清理。没有这条规则时 outbox 会无限增长。
|
||||
|
||||
### 6.2 历史清理与归档
|
||||
## 9. 失败恢复与维护作业
|
||||
|
||||
**航班历史清理**(`HistorySweepJob`,每天 03:30 触发):按 `HistoryProps` 的保留期与终态/静默判据选出候选(含 `DELETED`),先写历史存储,成功后物理删除主行与明细;历史存储未接通或 `msgx.history.history-store-enabled=false` 时删除 0 条。未经 FDEL、由生命周期直接清除的航班,清除前补发一次删除事件。语义与红线见 flight-state.md §6,不在此重复。
|
||||
### 9.1 失败、重试与重放
|
||||
|
||||
**留痕清理**:`SCHD_SNAP_LOG` 保留 90 天,在历史清理窗口内按 `(SCOPE_END, RECV_AT)` 删除;该清理不依赖历史存储开关,随作业每日执行。
|
||||
`ProcFailure` 与 `FailureScheduler` 统一处理侧失败落账,投递侧按同一套次数与退避规则迁移事件。启动自检强制退避档位数与尝试上限匹配,让「表里有档但永不触发」的配置无法通过。失败必须在持有具体消息、事件或批次的位置记录,外层循环只做兜底日志和等待,不重复增加次数。线程中断应恢复中断标记并向上传递;不把 JVM `Error` 当普通业务失败捕获。
|
||||
|
||||
**处理终态归档**:`PROC_STATE_HST` 仍是目标表(user-stories.md US-11),尚未建表;不得归档 `PENDING / FAILED`,也不能因移走身份记录而失去业务去重能力。ES 历史投影(阶段 B)不启用。自有记录归档与信箱原文保留的关系见 [message-lifecycle.md](message-lifecycle.md) §8/§9。
|
||||
`ReplayService` 只允许 `CODEC_ERROR / UNSUPPORTED / INFRA / EXHAUSTED` 从 `FAILED / DEAD` 回到 `PENDING`,并重置尝试次数、下次重试时间与错误原因,**不重置 `IDENTITY_KEY`**(保留身份,避免重放时把自己判成重复消息)。它按错误类全局批量重放,尚无按记录预检、操作审计与管理入口(US-10)。重放与回填通过 `MessageLifecycleGate` 在同一实例内互斥;**旧消息进入终态后后续消息可能已执行,重新入队不等于恢复历史顺序**,重放前必须评估状态覆盖和版本保护(CLM-3)。
|
||||
|
||||
共享信箱保留策略由库所有方管理;历史写入与删除事件入队之间仍需恢复方案,顺序调用不构成原子提交。
|
||||
### 9.2 中断恢复
|
||||
|
||||
## 7. 接口与运行配置
|
||||
恢复的唯一依据是各存储中已持久化的记录,不依赖进程内存状态。在 PG 从备份恢复的场景下,已提交的终态与已发出的事件可能回退,后果是重复投递与重复回填(按至少一次与幂等接受),但不得据此重放业务;留痕(`SCHD_SNAP_LOG`)在业务事务外追加,崩溃会丢该条留痕,不影响状态。
|
||||
|
||||
兼容入口为 `POST /cminmsgs/send`,请求体为原始 XML,当前成功响应为 HTTP 200、`text/plain` 格式的信箱记录 ID。这只表示接收结果,不表示业务处理成功。请求媒体类型、字符集和失败响应仍需与现役逐项对拍;查询与其他兼容端点不能因列入需求就视为已提供。
|
||||
| 中断位置 | 重启后的判定 | 恢复动作 |
|
||||
|---|---|---|
|
||||
| 已落信、未入队 | 信箱行位于应扫描的 ID 范围且 PG 无记录(不以处理标记为判据) | 重扫补建入队记录 |
|
||||
| 水位卡在空洞 | `HOLE_SINCE` 有值且未超过 `PARAM:msgx.pipeline.max-commit-delay` | 等待;超期后放行空洞本身并继续推进 |
|
||||
| 事务执行中 | PG 无该消息终态 | 事务整体回滚,按 `PENDING` 重新处理 |
|
||||
| 事务已提交、标记未写 | 终态行仍持有回填意图 | 仅补写标记;业务处理结果保持不变 |
|
||||
| 标记写入中途 | 标记仍为空 | 重新写入;重复写入同一值无副作用 |
|
||||
| 兼容入口已入队、水位未追平 | PG 已有该 ID 的记录 | 轮询读到该行时主键幂等,水位照常推进 |
|
||||
| 回填时信箱行已不存在 | 写入 0 行且信箱行不存在 | 立即放弃自动重试(`MISSING_ROW`)并告警;放弃不等于标记已确认,仍需人工对账 |
|
||||
| `RECEIVED_AT` 为 NULL | 超期分支以 `ENQUEUED_AT` 判定,不再失效 | `[G-ENQUEUED-AT]` 落地前靠退避重试保证 |
|
||||
| 投递目标已接受、`SENT` 未置 | 事件仍 `PENDING` | 允许重发,消费方按事件身份去重 |
|
||||
| PG 从备份恢复 | 终态与事件回退到备份点 | 按至少一次接受重复;不重放业务、不据此改写航班 |
|
||||
|
||||
运行配置以 `application.yml`、`application-dev.yml` 和 `.env.example` 为准,设计上重点区分(下列 `msgx.*` 参数以 `msgx.` 为前缀,`mailbox.*` 不带前缀):
|
||||
### 9.3 维护作业与归档
|
||||
|
||||
- `msgx.pipeline.autostart` 与 `msgx.stubs`:分别控制管道启动与内存适配器;生产禁止 stub,默认不自动启动。
|
||||
- `msgx.pipeline.poll-interval / claim-batch / max-attempts / backoff-ms / backoff-cap-ms / head-deadline`:控制轮询节奏、批次、重试上限、退避与队头滞留;这些参数不能改变 FIFO。
|
||||
- `msgx.pipeline.max-commit-delay / overdue-backfill / backfill-batch / backfill-max-attempts`:空洞老化阈值(Q2 的「ID 分配 → 事务可见时延上界」;默认值依据缺口唯一见 [message-lifecycle.md](message-lifecycle.md) §12 G7)、超期补写期限 R(Q6;完整约束唯一见 [message-lifecycle.md](message-lifecycle.md) §5.2)、回填扫描批量与回填自动重试上限(达上限停止自动重试并可人工恢复);R 与老化阈值都不能为提速而下调。
|
||||
- `msgx.pipeline.delivery-batch / delivery-drain-rounds`:普通事件(`KAFKA:msg`)单目标每轮领取条数上限,与每轮最多连取批数(连取后让出一次循环跑 `schd` flush,防长积压饿死状态通知);保留"队头失败即停止本轮"的目标内保序。
|
||||
- `msgx.operation-day.zone / cutoff-hour`:运营日时区与切日边界,决定 `OPERATION_DAY` 推导(flight-state.md §2.1)。
|
||||
- `mailbox.processed-value`:写回共享信箱的处理标记值,仅限库方认可的 legacy 值集(Q7)。
|
||||
- `msgx.schd.flush-period / flush-limit`:控制状态通知的聚合延迟与批量大小。
|
||||
- `msgx.identity.include-day-boundary`:影响去重语义,不能作为普通调优项切换;序号重置周期见 Q11。
|
||||
- `mailbox.shared-mysql.enabled` 与 `msgx.history.history-store-enabled`:分别门控真实信箱与历史存储接线,默认关闭。
|
||||
- `msgx.health.backlog-cache-ttl-ms`:积压快照缓存窗口(默认 30 秒,`/health` 与 `/metrics` 共用);设为 0 仅用于测试/排障,不作为实时性的替代。
|
||||
- `msgx.pipeline.cutover-watermark`:**一次性、显式**的切流播种(默认不配置 = 不播种),取值 `min` / `zero` / `max` 或具体 ID;播种的动机、升级实例拒绝重播与 `SEEDED_AT` 语义唯一见 [message-lifecycle.md](message-lifecycle.md) §5.1。非法取值由启动自检挡下。
|
||||
- `msgx.pipeline.late-detect-period / late-detect-batch`:**只读**迟到检测的周期与单轮复查量;周期设 0 即关闭。检测只计数与告警,不补入队、不改变处理语义。
|
||||
`JobRunner` 用独立 daemon 线程按周期触发回填扫描、航班历史清理与留痕清理;作业不参与消息 FIFO,也不使到期消息饥饿。`INV-18` 要求历史清理的删除与主泵处理互斥。
|
||||
|
||||
日志关联消息 ID、事件 ID 和批次;失败记录错误分类、次数、下次执行时间。健康检查反映依赖实际可用性;队头滞留、积压、死信和补偿失败需要指标及告警。指标经 Micrometer 暴露(`PipelineMetrics`,启动时急切注册):`msgx.pipeline.backlog.unfinished`、`msgx.pipeline.backlog.oldest_unprocessed_seconds`、`msgx.pipeline.backfill.unmarked_terminal`、`msgx.pipeline.backfill.abandoned`、`msgx.pipeline.backfill.oldest_unmarked_seconds`、`msgx.pipeline.watermark.lag`、`msgx.pipeline.hole.aged_out.total`、`msgx.pipeline.late_arrival.detected.total`(迟到检测命中数——**> 0 表示上游提交确实晚于水位推进,需要与库方对契约**)。取数统一走 `BacklogSnapshotProvider`(`msgx.health.backlog-cache-ttl-ms`,默认 30 秒),`/health` 与 `/metrics` 共用同一份快照——`backlog()` 是 `PROC_STATE` 的全表聚合,不能被高频抓取打穿;无法取数时上报 `NaN`,无可比记录的年龄/滞后类仪表上报 `-1`,两者都不伪造 0。日志出口故障不得阻塞业务线程。
|
||||
- **航班历史清理**:按保留期与终态/静默判据选候选(含 `DELETED`),先写历史存储,成功后物理删除主行与明细;历史存储未接通或开关关闭时删除 0 条。未经 FDEL、由生命周期直接清除的航班,清除前补发一次删除事件。语义与红线见 flight-state.md。
|
||||
- **留痕清理**:`SCHD_SNAP_LOG` 按保留期与 `(SCOPE_END, RECV_AT)` 删除,不依赖历史存储开关。
|
||||
- **处理终态归档**:`PROC_STATE` 终态记录归档至 `PROC_STATE_HST`(尚未建表 `[G-PROC-HST]`)。归档范围只含终态;归档后仍须保留业务去重能力。
|
||||
- **出站事件清理**:见投递清理规则。
|
||||
|
||||
## 8. 验证要求
|
||||
共享信箱保留策略由库方管理(contracts「保留与清除」)。历史写入与删除事件入队之间仍需恢复方案;顺序调用不构成原子提交。
|
||||
|
||||
单元测试使用内存仓储和可推进的 `Clock`,不依赖睡眠或在线中间件。以下不变量必须有回归测试,接口级单测不能替代主泵调度测试:
|
||||
## 10. 容量假设与设计取舍
|
||||
|
||||
| 场景 | 必须验证的结果 |
|
||||
|---|---|
|
||||
| 重复扫描、入队中断 | 不重复入队、不丢记录、不让后续消息越序;终态而未回填的行不得阻断后续消息发现。 |
|
||||
| 较小 ID 迟到(Q2 未确认) | 【缺口 · 已固定基线用例】快路径只读 `ID > W`;`InboxPollerTest` 已把"水位越过后到达的较小 ID 不被发现"钉成基线。补偿扫描(G1)交付前禁止任何"迟到不越序"的验收声明。 |
|
||||
| 空洞老化与重置 | 阈值内不推进水位、不越过入队;超期后只放行空洞本身;旧空洞补齐后出现的新空洞获得完整等待窗口。 |
|
||||
| 兼容入口与空洞并发 | 兼容入口登记的行超出水位,主泵不领取;水位追平后按序处理。端到端用例:`PipelineSmokeTest`「compat injected high id is not claimed until the watermark catches up」。 |
|
||||
| 队头失败、退避及作业竞争 | 消息不越队;到期后恢复;作业不使消息无限饥饿。 |
|
||||
| 同身份多条记录、失败后重试、归档后重复 | 只产生一次有效业务处理,不把自身重试判为重复。 |
|
||||
| 非业务型终态 | 不触碰 `FLIGHT_SCHD` / `MSG_EVENT`,只写 `PROC_STATE`,且终态与回填意图同语句生效。 |
|
||||
| PG 事务失败、快照重复或迟到 | 整体回滚重试、不重复推进版本、不回退状态、不误删增量航班。 |
|
||||
| 整包协议拒绝(运营日冲突、声明数不符) | 整包不落地、整体回滚,既有状态与版本不变,消息终态为 `DEAD(PROTOCOL)`。 |
|
||||
| PG 提交失败、信箱回填失败 | 事件、处理终态与回填意图一起回滚;已提交结果只补写标记,不重放业务;中间态永不补写。 |
|
||||
| 回填四种结果 | 写入成功 / 早已标记(不覆盖、记成功)/ 信箱行不存在(立即放弃并告警,**不得**视为已标记)/ 暂时性故障达上限(停止自动重试,可人工恢复)。 |
|
||||
| 回填公平性 | 最旧的一批记录永久失败时,后续待回填记录仍能被扫描到;已放弃行不再进入扫描。 |
|
||||
| 「打标即清除」语义 | 【待确认】若库方清除由打标触发,必须验证存在约定的保留期或独立原文保留通道,且**不依赖 `R` 的取值**。 |
|
||||
| `RECEIVED_AT` 为 NULL | 超期兜底不生效的行为被显式验证,且不会导致标记被提前写入。 |
|
||||
| 毒丸升级与人工重放并发 | 【缺口】毒丸路径不在 gate 内;窗口必须被复现,或用 gate 覆盖后验证互斥。 |
|
||||
| 投递确认丢失、批次失败、次数耗尽 | 允许可识别的重发、保持目标顺序、整批退避并保留死信。 |
|
||||
| 请求超时、无匹配 RESP、时间单位不一致 | 不误用迟到应答,不提前完成请求。 |
|
||||
| stub 误配置、重复实例、停机中断 | 生产拒绝不安全启动,工作线程能正确退出。 |
|
||||
本设计按以下量级选型(`[待确认]`,未实测;参数默认值的依据列见 reference):
|
||||
|
||||
真实适配器还需 PG 事务与补偿集成测试、日计划崩溃恢复测试、Kafka 故障投递验证;常规验证命令为 JDK 25 下执行 `./gradlew test`。
|
||||
- 单机场、单活动实例、单维护者;入站日消息量千级到万级;单条报文量级 ≤ 10⁴ 字节。
|
||||
- 处理延迟秒级可接受;航班可见性延迟不劣于现役(轮询间隔 ≤ 1 秒 + 聚合周期秒级)。
|
||||
- 因此:不引入多实例并行、分布式锁、分区表;用单行锁与单线程换确定性。
|
||||
|
||||
## 9. 实现入口
|
||||
|
||||
生产代码根目录为 `src/main/kotlin/com/gzzn/omms/msgexchange/`:
|
||||
|
||||
| 关注点 | 主要入口 |
|
||||
|---|---|
|
||||
| 收报与兼容接口 | `ingress/InboxPoller.kt`、`InboxService.kt`、`InboxController.kt` |
|
||||
| 解码 | `codec/JacksonXmlCodec.kt`、`SisWireMapper.kt`、`SisMessageBody.kt` |
|
||||
| 调度与处理 | `processing/Pump.kt`(含 `MessageProcessor`)、`DynamicProcessors.kt`(FLOP/FDEL/ADFT)、`Identity.kt` |
|
||||
| 日计划 | `processing/ScheduleProcessor.kt`;请求协调尚无实现(`REQ_TRACK` 见 `infra/persistence/`) |
|
||||
| 回填与投递作业 | `processing/BackfillService.kt`、`jobs/JobRunner.kt`、`HistorySweepJob.kt`、`delivery/Dispatcher.kt` |
|
||||
| 持久化与恢复 | `infra/persistence/`、`infra/retry/`(`ProcFailure` / `ReplayService` / `FailureScheduler`) |
|
||||
| 启停与配置 | `PipelineLifecycle.kt`、`config/PipelineProps.kt`、`config/HistoryProps.kt` |
|
||||
|
||||
## 10. 当前实现差异
|
||||
|
||||
以下缺口直接影响上述设计是否成立,不能以类或接口已存在作为完成依据。收报与处理链路的逐条缺口另见 [message-lifecycle.md](message-lifecycle.md) §12。
|
||||
|
||||
- **事务与外部副作用**:事务边界以 §3.3 表 1 为准;[message-lifecycle.md](message-lifecycle.md) §4 的两个崩溃窗口已随 V2 迁移消除。回填本身仍是跨库单写,失败按退避重试并由超期期限 R 兜底;R 的取值待 Q6 确认。
|
||||
- **收报与调度**:水位与空洞计时已与入队同事务推进([message-lifecycle.md](message-lifecycle.md) §5.1 口径)。**较小 ID 迟提交尚无补入队机制**:只读迟到检测(阶段 0)已实装,仅计数与告警;窗口补偿扫描(阶段 1,G1)未实现,端到端顺序保证仍待与库方联合验证。空洞老化阈值 `max-commit-delay` 的取值缺依据(§12 G7)。HOL 计时已改用 `PROCESSING_STARTED_AT` + 可注入 `Clock`(§3.2);Q6 仍需确认长期积压与人工重放的期限口径。
|
||||
- **重放互斥**:`MessageLifecycleGate` 只覆盖回填与人工重放,毒丸升级路径不在其中,存在「先标 DEAD 并打标、再被重放拨回 PENDING」的窗口(§12 G5)。
|
||||
- **原文保留通道**:若库方清除语义为"打标即清除",回填成功后原文即可被清除,重放窗口失去保护;独立原文保留通道尚未设计(§12 G10)。
|
||||
- **快照与业务能力**:DNLD/RESP/ADFT 与 FLOP/FDEL 处理器、整包校验与跨运营日整包拒绝均已接入;但 RESP 应答守卫与出站请求未实现,忽略规则(US-04)未实现,29 类 FLOP 与参考应答的逐类矩阵未补全,ADFT 缺失字段与 `FLID` 重用语义待上游确认。
|
||||
- **航班读写**:唯一写入口与权威读已落地;ROUT/ERUT 联合主键、空值/未知属性保真、事件在事务内只算一次、逐航班多次查询仍待修正(见 flight-state.md §6)。`/all/flights` 尚未实现。
|
||||
- **请求、参考数据与归档**:`REQ_TRACK` 表与仓储已建,但无运行时协调与 `COUTMSGS` 出站适配;`REF_MASTER` 未建表;`PROC_STATE_HST` 未建表。航班历史清理脚手架已实现,历史存储未接通时删 0 条。
|
||||
- **生产与运维**:已有事务行锁,但没有整个实例的排他/FIFO 协调;真实 Kafka 与信箱出站适配未交付(仅 stub),配置允许环境变量覆盖 `acks`/幂等/in-flight;启动校验、影子隔离、告警与指标未闭环。对拍比较工具存在不等于现场对拍已完成。Oracle 11g 方言与迁移未接入验证。
|
||||
|
||||
业务契约与待确认事项见 [user-stories.md](user-stories.md),进度由 Plane 跟踪;本文件不维护工单流水账、测试数量或历史方案全文。
|
||||
容量假设变化时,需要重新评估的项:批次大小与轮询间隔、聚合周期与批上限、指标取数口径(`backlog()` 是 `PROC_STATE` 聚合,`PROC_STATE_HST` 未交付前成本随历史增长)、以及 `MSG_EVENT` 保留期。
|
||||
|
||||
@@ -102,8 +102,8 @@ SIS 规定删除主航班时必须先删子共享航班、再删主航班,顺
|
||||
|
||||
- Oracle 11g 的完整方言与集成验证;
|
||||
- RESP 请求—应答匹配和出站请求重发;
|
||||
- ADFT 缺失字段和 `FLID` 重用的上游语义;
|
||||
- 日计划缺失可选字段的删除语义(与 SIS 的冲突见 §3.1,Q13);
|
||||
- ADFT 缺失字段和 `FLID` 重用的上游语义(`FLID` 复用见 `Q16`);
|
||||
- 日计划缺失可选字段的删除语义(与 SIS 的冲突见本文件「合并与写入语义」,`Q13`);
|
||||
- 主/共享删除顺序与 EROR 回报义务(Q14);
|
||||
- 未归属 `OPERATION_DAY` 航班的终止与保留策略;
|
||||
- ROUT/ERUT 联合主键迁移;
|
||||
@@ -111,10 +111,4 @@ SIS 规定删除主航班时必须先删子共享航班、再删主航班,顺
|
||||
|
||||
## 7. 不变量
|
||||
|
||||
- 本地 PostgreSQL 当前态是唯一权威;信箱、Kafka、Redis 和展示视图不是权威。
|
||||
- 同一时刻只有一个主泵推进 FIFO 状态;状态、事件、终态和回填意图原子提交。
|
||||
- 每个航班每次成功状态写入单调推进 `STATE_VERSION`;重复消息不重复推进。
|
||||
- `FLID` 唯一,已确定的 `OPERATION_DAY` 不可改变。
|
||||
- 报文未携带的字段不被隐式清空(与 SIS §3.16 的冲突见 §3.1 与 Q13);集合按完整合并结果写入。
|
||||
- 缺席于某个日计划不构成删除理由;删除只由 FDEL 或受控历史清理触发。
|
||||
- 外部副作用失败可重试,不回滚已提交的本地业务结果。
|
||||
航班域不变量的**定义处是 [invariants.md](invariants.md)**(`INV-11`–`INV-16`),本节不再重复:唯一权威、`FLID` 唯一与 `OPERATION_DAY` 不可变、`STATE_VERSION` 单调、未携带字段不清空、缺席不构成删除、外部副作用失败不回滚已提交结果。与管道共享的部分见同文件的 `INV-17`–`INV-19`。
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
# 前提、不变量与声明边界
|
||||
|
||||
本文件是三件东西的唯一出处:
|
||||
|
||||
- **前提 PRE-x**:由外部提供、我们无法单方保证的事实。前提失效时不变量必须整体重估。
|
||||
- **不变量 INV-x**:本系统自己保证的性质。变更用「追加 + 作废」(`INV-7 → [作废 by INV-7b]`),不静默改写。
|
||||
- **声明边界 CLM-x**:每条对外主张依赖哪些 PRE/INV、当前**可否声明**、挂起原因(`[G-x]` 缺口 / `[Q-x]` 待确认)。
|
||||
|
||||
验证映射在本文件 §4,是验收口径的唯一清单;代码与测试只做证据,不在此重复叙述。
|
||||
|
||||
## 1. 前提(外部提供)
|
||||
|
||||
| 编号 | 前提 | 若不成立的影响 | 状态 |
|
||||
|---|---|---|---|
|
||||
| PRE-1 | 信箱消费权排他:同一时刻只有一个系统有权处理、打标、判定可清除(迁移期由切流规程保证单一权威写者) | 水位、身份去重、清除前提全部失效 | `[待确认]`(切流由运维规程保证,上线前另立) |
|
||||
| PRE-2 | ID 单调 + 可见时延上界:见 `C-1`/`C-2` | 水位只能当快路径提示;空洞老化阈值无依据;不能声明发现完整性 | `[待确认 Q2]` |
|
||||
| PRE-3 | ID 空间不复位、不复用、不回退:见 `C-3` | 水位(不可逆单游标)之后的行永久不可见 | `[待确认 Q2]` |
|
||||
| PRE-4 | 报文的 `DATE_RECEIVED` 时钟基准可解释(偏斜在有界范围内) | 跨系统时间比较(`RECEIVED_AT` 与本地 `NOW`)会提前或推迟判定 | `[待确认 Q7]` |
|
||||
| PRE-5 | 单活动实例运行(信箱读取不加锁、水位是单行覆盖写) | 水位互相覆盖、空洞计时失真 | `[我们自证]`(部署约束,见 architecture) |
|
||||
| PRE-6 | 信箱与自有 PG 之间没有跨库事务 | 回填、水位推进、清除都不能声称原子 | `[我们自证]`(架构事实) |
|
||||
| PRE-7 | 报文不可变:同一业务身份的重发必为同一内容:见 `C-4` | 上游改发会被判为重复并静默跳过 | `[待确认 Q15]` |
|
||||
| PRE-8 | `FLID` 在保留期内不复用:见 `C-21` | 「保留最新版本」的合并规则可能压掉新航班事件,旧 tombstone 可能删掉在用航班 | `[待确认 Q16]` |
|
||||
|
||||
## 2. 不变量
|
||||
|
||||
### A. 管道
|
||||
|
||||
- **INV-1** 五个独立事实互不替代:落信 / 入队 / 处理完成 / 已回填 / 投递确认各有独立证据,前一个不蕴含后一个。
|
||||
- **INV-2** 水位与入队同事务:不允许出现「水位已推进、消息未入队」的持久化状态;水位只增不减,遇空洞即停,只有判定为永久空洞才放行,且放行只跳过空洞本身、不越过任何已存在的行。
|
||||
- **INV-3** 队头唯一:任一时刻只有一个可执行队头(最小未完成 `MSG_ID`,`PENDING` 与 `FAILED` 都占位);`FAILED` 未退避到期时后续消息不得越过。
|
||||
- **INV-4** 只领取已发现的行:主泵只领 `MSG_ID ≤ W`;水位之外的行只可能来自兼容入口,必须等水位追平后按序处理。
|
||||
- **INV-5** 发现与处理互不阻塞:收报只看 `ID > W`,不以处理标记为谓词;终态而未回填的行不阻断后续消息的发现。
|
||||
- **INV-6** 处理终态不可逆:已提交的 `SUCCEEDED` 不因回填或投递失败回改。
|
||||
- **INV-7** 处理标记单调:任何路径只把空标记写成已处理值,不回撤、不覆盖。
|
||||
- **INV-8** 回填只针对终态(`PENDING` / `FAILED` 永不写标记);「还欠一次回填」的事实与终态由**同一条语句**落库,不存在第二处落账。
|
||||
- **INV-9** 一信一行、一身份一记录:`PROC_STATE` 按 `MSG_ID` 唯一;同一业务身份至多绑定一条有效处理记录。
|
||||
- **INV-10** 对外投递至少一次;端到端恰好一次不在交付范围。
|
||||
|
||||
### B. 航班域(定义处;flight-state.md 只引编号)
|
||||
|
||||
- **INV-11** 自有 PG 的航班当前态是唯一权威;信箱、Kafka、展示视图都不是权威。
|
||||
- **INV-12** `FLID` 唯一;已写入非空的 `OPERATION_DAY` 不可改变。
|
||||
- **INV-13** 每个航班每次成功状态写入单调推进 `STATE_VERSION`;重复消息不重复推进。
|
||||
- **INV-14** 报文未携带的字段不被隐式清空;集合按完整合并结果写入,保留输入顺序与源序号。
|
||||
- **INV-15** 缺席于某个日计划不构成删除理由;删除只由 FDEL 或受控历史清理触发。
|
||||
- **INV-16** 外部副作用(回填、Kafka 投递、出站信箱)失败可重试,但不回滚已提交的本地业务结果。
|
||||
- **INV-17** 状态变更、待发事件、处理终态与回填意图在同一 PG 事务内原子提交。
|
||||
- **INV-18** 航班表的写者集合是「主泵处理器」与「历史清理」;两者必须互斥(同一 `PIPELINE_LOCK`,或清理在同一事务内复查判据后再删除),不得出现清理删除与处理器更新同一 `FLID` 的竞态。`[实现核对待确认,关联 ACM2-30]`
|
||||
- **INV-19** 整包校验失败或运营日冲突时整包不落地,既有状态与版本保持不变。
|
||||
- **INV-20** 处理器幂等:同一消息重复执行只产生一次业务效果。身份唯一只防「重复记录」,不防「重新执行」;29 类 FLOP 幂等矩阵补全前,本条**不可声明**。`[G-FLOP-IDEMPOTENT]`
|
||||
|
||||
## 3. 声明边界
|
||||
|
||||
| 编号 | 主张 | 依赖 | 当前可否声明 | 挂起原因 |
|
||||
|---|---|---|---|---|
|
||||
| CLM-1 | 严格 FIFO:迟到的小 ID 不会越序 | PRE-2、PRE-3、`Q2` | **不可** | 发现完整性依赖库方承诺;窗口补偿扫描(G1)未交付 |
|
||||
| CLM-2 | 迟到报文不丢(可被发现并处置) | `G1` | **不可** | G1 未交付;现有阶段 0 只读检测仅计数告警,不补入队 |
|
||||
| CLM-3 | 重放不产生重复业务副作用 | INV-20、`G-FLOP-IDEMPOTENT` | **不可** | 29 类 FLOP 幂等矩阵未补全;重放不恢复历史顺序 |
|
||||
| CLM-4 | 回填不会被短暂故障放弃:最终打标,或进入可对账的放弃清单 | INV-8、`C-5`、`C-8` | **可声明(有条件)** | 条件:`R` 之前不放弃;`MISSING_ROW` 立即放弃并告警;放弃行须经人工对账才可用于清除判定(`C-8`)。原文保留另见 CLM-5 |
|
||||
| CLM-5 | 重放窗口内原文仍可读 | `C-6`、`C-7`、`Q7`、`Q9` | **不可** | 清除语义与保留期未确认;「打标即清除」下无补救 |
|
||||
| CLM-6 | 单实例内严格 FIFO | PRE-5、INV-3 | **可**(限于单活动实例) | — |
|
||||
| CLM-7 | 事件投递在同一 `FLID` 内保序 | INV-10、投递设计 | **可**(跨 `FLID` 不承诺) | 实现当前按目标级全序投递,收敛到按 `FLID` 属投递改造,关联 ACM2-34 |
|
||||
| CLM-8 | 出站交付承诺只到「落信」 | `C-24`、`Q10` | **可**(仅落信语义) | 消费方与 ACK 列语义未确认 |
|
||||
| CLM-9 | 处理标记延迟由调度周期决定(≤30 秒) | — | **不可** | 30 秒只是扫描调度周期;批次积压、单行超时与历史作业都会延长实际延迟 |
|
||||
|
||||
## 4. 验证映射
|
||||
|
||||
每条不变量至少一条证据;「缺口」表示尚无回归。测试名以仓库现状为准,新增测试按此表补位。
|
||||
|
||||
| 不变量 | 场景 | 证据 / 缺口 |
|
||||
|---|---|---|
|
||||
| INV-1 | 五事实互不替代:入队不引用标记、回填不引用投递、投递不引用回填 | 缺口(需接口级断言) |
|
||||
| INV-2 | 重复扫描、入队中断 | 不重复入队、不丢记录;`InboxPollerTest` |
|
||||
| INV-2 | 空洞老化与重置 | 阈值内不推进、不越过入队;超期只放行空洞本身;旧空洞补齐后新空洞获得完整窗口 |
|
||||
| INV-2 | 水位写入与入队同事务 | 缺口(需真实 PG 事务用例,关联 ACM2-39) |
|
||||
| INV-3 / CLM-1 | 较小 ID 迟到 | **缺口基线已固定**:`InboxPollerTest` 钉住「水位越过后到达的较小 ID 不被发现」;补偿扫描交付前禁止任何「迟到不越序」的验收声明 |
|
||||
| INV-4 | 兼容入口与空洞并发 | 兼容入口登记的行超出水位、主泵不领取;`PipelineSmokeTest`「compat injected high id is not claimed until the watermark catches up」 |
|
||||
| INV-3 | 队头失败、退避及作业竞争 | 消息不越队;到期后恢复;作业不使消息无限饥饿 |
|
||||
| INV-5 | 终态未回填不阻断发现 | 缺口(补齐后应断言发现谓词不引用处理状态) |
|
||||
| INV-6 | 投递失败后终态不变 | 缺口 |
|
||||
| INV-7 | 回填四种结果 | 写入成功 / 早已标记(不覆盖、记成功)/ 信箱行不存在(立即放弃并告警,不得视为已标记)/ 暂时故障持续到 `R` 仍未打标(停止自动重试,可人工恢复) |
|
||||
| INV-7 | `RECEIVED_AT` 为 NULL | 超期兜底不生效的行为被显式验证,且不导致标记提前写入(`G-ENQUEUED-AT` 落地后改为覆盖该分支) |
|
||||
| INV-8 | PG 提交失败、信箱回填失败 | 事件、终态与回填意图一起回滚;已提交结果只补写标记,不重放业务;中间态永不补写 |
|
||||
| INV-8 | 非业务型终态 | 不触碰航班表 / `MSG_EVENT`,只写 `PROC_STATE`,且终态与回填意图同语句生效 |
|
||||
| INV-9 | 同身份多条记录、失败后重试、归档后重复 | 只产生一次有效业务处理,不把自身重试判为重复 |
|
||||
| INV-10 | 投递确认丢失、批次失败、次数耗尽 | 允许可识别的重发、保持目标顺序、整批退避并保留死信 |
|
||||
| INV-11 | 权威唯一 | 缺口(展示视图与缓存不得成为写入或对账来源) |
|
||||
| INV-12 / INV-13 | PG 事务失败、快照重复或迟到 | 整体回滚重试、不重复推进版本、不回退状态、不误删增量航班 |
|
||||
| INV-12 | 运营日冲突 | 整包 `DEAD(PROTOCOL)`,既有状态与版本不变 |
|
||||
| INV-14 / INV-19 | 整包协议拒绝(声明数不符、缺载荷) | 整包不落地、整体回滚、既有状态不变 |
|
||||
| INV-15 | 缺席不删除 | 缺口(F-del 与清理路径分别断言) |
|
||||
| INV-18 | 清理与处理并发 | **缺口**:需断言删除与处理同一 `FLID` 时互斥(关联 ACM2-30) |
|
||||
| INV-20 / CLM-3 | 重放同一条消息 | 缺口:29 类 FLOP 幂等矩阵未补全 |
|
||||
| CLM-4 | 放弃行与清除前提 | 断言放弃行不写标记、不被当作已打标(关联 ACM2-36) |
|
||||
| CLM-9 | 回填/积压完成时限 | 缺口:需要「最老待回填年龄」「扫描积压」「作业心跳」指标(关联 ACM2-38) |
|
||||
| — | 请求超时、无匹配 RESP、时间单位不一致 | 不误用迟到应答、不提前完成请求 |
|
||||
| — | stub 误配置、重复实例、停机中断 | 生产拒绝不安全启动,工作线程能正确退出 |
|
||||
|
||||
## 5. 缺口索引与 Plane 的关系
|
||||
|
||||
本表是**缺口标记的唯一清单**:其他文档只在相应位置写 `[G-x]`,不解释、不记进度;工作进度在 Plane(ACM2)。
|
||||
|
||||
| 缺口 | 含义 | 影响 |
|
||||
|---|---|---|
|
||||
| `G1` | 窗口补偿扫描未实现(Plane ACM2-41):水位越过后的迟到小 ID 没有补入队机制 | CLM-1、CLM-2 |
|
||||
| `G-IGNORE` | 忽略规则(`LDM`/`REGN`/`RSTA`/`EROR`)未实现(US-04) | `INV-19` 的忽略分支;合法忽略报文当前按 `UNSUPPORTED` 处理 |
|
||||
| `G-RESP-GUARD` | `RESP` 应答守卫未实现,当前与 `DNLD` 无差别进入快照写入 | 请求匹配闭环;`C-23` |
|
||||
| `G-REQ-TRACK` | `REQ_TRACK` 无运行时协调器:出站适配、请求编码、超时与应答匹配未实现 | US-08;`C-24` |
|
||||
| `G-PROC-HST` | `PROC_STATE_HST` 未建表,终态归档未落地 | US-11;归档能力 |
|
||||
| `G-ENQUEUED-AT` | `PROC_STATE` 尚无 `ENQUEUED_AT` 列,超期判据暂用 `RECEIVED_AT` | `INV-7` / CLM-4;跨系统时钟偏斜与 `RECEIVED_AT` 为 NULL |
|
||||
| `G-FLOP-IDEMPOTENT` | 29 类 FLOP 幂等矩阵未补全 | `INV-20`、CLM-3 |
|
||||
| `G-BACKFILL-ABANDON-BYTIME` | 回填放弃判据由「尝试次数」改为「`R` 超期」尚未落地 | `INV-7`、CLM-4 |
|
||||
| `G-EVENT-RETENTION` | `MSG_EVENT` 已发送行的保留期与清理作业未实现 | outbox 有界性 |
|
||||
| `G-BACKFILL-BACKOFF` | 回填独立退避键(`backfill-backoff-ms` / `-cap-ms`)未实现,暂沿用处理退避表 | 回填重试节奏 |
|
||||
| `G-HEAD-DEADLINE` | `head-deadline` 已定为「仅告警」,代码注释与判据仍写「毒丸升级」 | design「主泵调度」的表述一致性 |
|
||||
| `G-KAFKA-D3` | `kafka.producers.default.max-in-flight` 默认 5,与架构决策 D3 要求的 1 不一致 | 投递幂等前提 |
|
||||
| `G-JOB-HEARTBEAT` | 作业心跳、扫描积压、实际回填延迟指标未实现 | CLM-9;回填可观测性 |
|
||||
| `G-REPLAY-CHANNEL` | 「打标即清除」语义下的独立原文保留通道未设计 | CLM-5 |
|
||||
|
||||
`G1` 沿用 Plane 既有编号(ACM2-41);其余为文档内稳定标记,与 Plane 工作项的对应关系在 Plane 侧维护。
|
||||
+8
-345
@@ -1,348 +1,11 @@
|
||||
# 上游消息生命周期设计
|
||||
# 上游消息生命周期设计(已合并)
|
||||
|
||||
## 本文范围与读者
|
||||
本文件已并入 [design.md](design.md):
|
||||
|
||||
本文定义 msgexchange-v2(下称"本系统")对共享 MySQL 信箱中报文的完整生命周期:从上游写入、本系统处理、处理标记回信箱,到信箱数据最终清除。各阶段的输入输出、幂等方式、故障恢复方法,以及本系统与信箱库管理方之间的分工,都在本文界定。
|
||||
- 收报、水位与空洞 → design.md「收报与水位」
|
||||
- 回填(四种结果、放弃、`R` 的作用)→ design.md「回填」
|
||||
- 中断恢复矩阵 → design.md「失败恢复与维护作业」
|
||||
- 共享信箱保留与清除的前提 → [contracts.md](contracts.md)「保留与清除」
|
||||
- 清除执行、切流播种、人工重放、故障恢复的**前置条件与红线** → contracts.md(`C-7`–`C-12`)与 invariants.md(CLM-x);执行步骤在上线/切流前另立规程
|
||||
|
||||
- 模块协作、状态机与配置参数见 [design.md](design.md);系统边界与"不建表、不改结构"的红线见 [architecture.md](architecture.md);验收口径见 [user-stories.md](user-stories.md)。符号(`W` / `holeSince` / `R` / `R_keep` / `head-deadline` 等)与配置键的对应关系统一见 [design.md](design.md) §1.1。
|
||||
- 读者包括本系统开发与运维人员、以及负责共享 MySQL 的库方接口人;**§5–§7 中标注 Q 编号的条款**即需要与库方书面确认的开放项。
|
||||
- 状态标记沿用 [design.md](design.md) §1:【目标】/【现状】/【缺口】/【待确认】。正文描述目标设计,本文范围内的缺口清单集中在 §12;跨文档引用一律带文档名。
|
||||
|
||||
**角色与术语约定**(全文沿用):
|
||||
|
||||
- **上游**:向信箱写入报文的源头系统(CIIMS、AODB 等)。
|
||||
- **信箱**:共享 MySQL 中的入站信箱表 `CMINMSGS`;出站方向为 `COUTMSGS`(§7)。
|
||||
- **库方**:共享 MySQL 的管理方;表结构变更与数据清除只能由库方执行或书面授权。
|
||||
- **处理标记**:信箱行上表示"本系统已处理"的约定字段;逻辑名 `DATE_PROCESSED` / `STATUS`,实际列名以库方契约为准(legacy 为 `CMINMSGS_DATE_PROCESSED` 等真实列)。正文统一用"处理标记",不再混用列名。
|
||||
- **自有 PG**:本系统唯一的业务数据库 PostgreSQL;信箱与自有 PG 之间不存在跨库事务。
|
||||
|
||||
## 1. 五个独立事实
|
||||
|
||||
消息的认定分为五个环节,每个环节都有独立的证据,互不替代:上游写入了不等于本系统接手了,本系统处理完了也不等于信箱标记已写、下游已收到。
|
||||
|
||||
| 环节 | 认定依据 | 执行方 |
|
||||
|---|---|---|
|
||||
| 落信:报文进入信箱 | `CMINMSGS` 中存在该行 | 上游 |
|
||||
| 入队:本系统开始处理 | 自有 PG 建立 `PROC_STATE` 记录 | `InboxPoller` |
|
||||
| 处理完成:业务处理完成 | `PROC_STATE` 到达终态(SUCCEEDED / SKIPPED / DEAD) | 主泵 |
|
||||
| 已回填:信箱写入处理标记 | 信箱行持有处理标记 | 回填与补偿通道 |
|
||||
| 投递确认:投递目标已接受 | 目标端返回成功且 `MSG_EVENT` 置 `SENT` | `delivery`(Dispatcher / flushSchd) |
|
||||
|
||||
分环节的原因是存储边界:信箱与自有 PG 之间的写入不共享事务。“落信”以共享 MySQL 为准,“入队”和“处理完成”以自有 PG 为准;“已回填”与“投递确认”各以外部操作成功和本地确认事实共同判定。后两个环节各自独立重试,都可能单独失败。“投递确认”只表示 Kafka Broker 或其他投递目标已接受,不表示业务消费者已消费。
|
||||
|
||||
"入队"这一环还有一个不由本系统单独保证的前提:**发现是完整的**——依赖 Q2 的两条上游承诺(§5.1)。
|
||||
|
||||
## 2. 状态总纲
|
||||
|
||||
```text
|
||||
信箱行(共享 MySQL)
|
||||
未标记 ──[本系统写处理标记]──→ 已处理标记 ──[库方按保留期执行]──→ 已归档 ──→ 已清除
|
||||
↑ §6(本系统不执行 DDL)
|
||||
└── 回填与超期补写(§5.2)
|
||||
|
||||
PROC_STATE(自有 PG)
|
||||
PENDING ──处理成功────────→ SUCCEEDED
|
||||
│ ↑ ─→ SKIPPED(业务重复,非错误)
|
||||
│ └── 人工重放(白名单错误类)
|
||||
└─ 处理失败 → FAILED ──退避到期──→ PENDING
|
||||
└── 次数耗尽 / 队头滞留超限 ──→ DEAD(人工处置)
|
||||
|
||||
MSG_EVENT(自有 PG,投递侧,见 design.md §5)
|
||||
PENDING ──投递确认──→ SENT
|
||||
└── 失败退避 ──→ DEAD(DLQ)
|
||||
```
|
||||
|
||||
- `PENDING` 与 `FAILED` 是处理中的状态,`FAILED` 继续退避重试并占用 FIFO 队头;`SUCCEEDED / SKIPPED / DEAD` 是终态,到达后队列方可推进。
|
||||
- `DEAD` 与 `FAILED` 不是不可逆:经人工批准,指定错误类别的记录可重新置回 `PENDING` 处理(放行范围见 §9 / design.md §6.1)。重放处理的是当前状态,不恢复历史处理顺序。
|
||||
- **写事务边界**:业务型终态与航班变更、待发事件、回填意图同处一个 PG 事务;其余终态与毒丸升级只写 `PROC_STATE`(终态与回填意图同一条 UPDATE)。完整边界见 [design.md](design.md) §3.3 表 1。
|
||||
- 信箱处理标记写在该事务**之后**,由与终态同行的回填意图驱动重试与超期补写。
|
||||
|
||||
## 3. 处理阶段
|
||||
|
||||
**表 1 取报与处理**(收报、主泵、解码与身份):
|
||||
|
||||
| 阶段 | 输入 | 输出 | 幂等依据 | 失败处理 |
|
||||
|---|---|---|---|---|
|
||||
| 收报(发现 + 入队) | `ID > W` 的升序信箱行(谓词见 §5.1) | `PROC_STATE(PENDING)` 与推进后的 `(W, holeSince)` | `MSG_ID` 主键(冲突即已入队) | 下轮扫描补入队;整轮失败不动水位 |
|
||||
| 取队头 | 最小未完成 `MSG_ID`(`PENDING` 与 `FAILED` 都占位) | 本轮唯一处理对象 | 表状态即队列 | 无队头则等待;`FAILED` 未到期则等待 |
|
||||
| 解码与身份 | 信箱原文 | `DecodedMessage`、`IDENTITY_KEY` | 身份唯一约束(仅首次绑定) | 报文非法→`DEAD`;解码能力不足→`FAILED` 退避 |
|
||||
| 事务处理 | 当前完整状态 + 报文载荷 | 航班变更、终态、事件、回填意图 | 消息 ID + 业务身份 | 业务型事务整体回滚重试 |
|
||||
|
||||
**表 2 回填**(终态之后的信箱标记):
|
||||
|
||||
| 阶段 | 输入 | 输出 | 幂等依据 | 失败处理 |
|
||||
|---|---|---|---|---|
|
||||
| 回填扫描(每 30 秒,**调度周期 ≠ 完成时限**) | 扫描谓词见 §5.2 | 追平标记,或判定放弃自动重试 | 消息 ID(仅空标生效) | 退避、超期强制补写与放弃口径见 §5.2 |
|
||||
|
||||
主泵**不做**回填:终态与回填意图由同一条 UPDATE 落库,回填一律由扫描驱动(跨库写不能占用 FIFO 关键路径)。
|
||||
|
||||
**处理链路流程**(`├─` 括起的是同一个 PG 事务):
|
||||
|
||||
```text
|
||||
① 取报(InboxPoller,默认每秒一轮)
|
||||
读游标 (W, holeSince) → 取 ID > W 的升序前 N 行 → 求连续上界 → 空洞判定
|
||||
├─ PG 事务:insertIfAbsent(MSG_ID, RECEIVED_AT) × k + 写回 (W, holeSince) → 提交
|
||||
└─ 未入队的行留到下一轮(每轮最多解决一个空洞)
|
||||
|
||||
② 调度(Pump,单线程)
|
||||
headUnfinished()(最小未完成 MSG_ID)
|
||||
├─ 队头为 FAILED 且已毒丸 → DEAD(EXHAUSTED)(单语句,不取锁)
|
||||
├─ FAILED 未到期 → 等待
|
||||
└─ 可执行 → 记 PROCESSING_STARTED_AT(仅首次)→ processOne()
|
||||
|
||||
③ 处理(MessageProcessor)
|
||||
读原文(共享 MySQL)→ 解码 → 身份绑定(独立单语句)
|
||||
└─ PG 事务(持 PIPELINE_LOCK):航班变更 + 待发事件 + 终态 + 回填意图 → 提交
|
||||
|
||||
④ 回填(终态之后,跨库两次单写,无事务)
|
||||
写信箱处理标记 → 记 BACKFILL_AT;失败则回填意图留在该行上
|
||||
└─ 回填扫描(每 30 秒)按表 2 的谓词重试,越过 R 后强制补写
|
||||
```
|
||||
|
||||
领域决策逻辑不执行 I/O;业务型/非业务型终态的写入边界与全部外部副作用(读信箱原文、写信箱标记)都在 PG 事务之外。跨库不共享事务。
|
||||
|
||||
## 4. 故障恢复
|
||||
|
||||
恢复的唯一依据是各存储中已持久化的记录,不依赖任何进程内存中的状态:
|
||||
|
||||
| 中断位置 | 重启后的判定 | 恢复动作 |
|
||||
|---|---|---|
|
||||
| 已落信、未入队 | 信箱行位于应扫描的 ID 范围且 PG 无记录(不以处理标记为判据) | 重扫补建入队记录 |
|
||||
| 水位卡在空洞 | `INBOX_CURSOR.HOLE_SINCE` 有值且未超过 `max-commit-delay` | 等待;超期后放行空洞本身并继续推进(§5.1) |
|
||||
| 事务执行中 | PG 无该消息终态 | 事务整体回滚,按 `PENDING` 重新处理 |
|
||||
| 事务已提交、标记未写 | 终态行仍持有回填意图(`BACKFILL_NEXT_AT` 非空) | 仅补写标记;业务处理结果保持不变 |
|
||||
| 标记写入中途 | 标记仍为空 | 重新写入;重复写入同一值无副作用 |
|
||||
| 兼容入口已入队、水位未追平 | PG 已有该 ID 的记录 | 轮询读到该行时主键幂等,水位照常推进;顺序风险见 §5.1/§12 |
|
||||
| 回填时信箱行已不存在 | 写入 0 行且信箱行不存在 | **立即放弃自动重试**(原因 `MISSING_ROW`)并告警;**放弃 ≠ 标记已确认**,仍需人工对账(§5.2) |
|
||||
| `RECEIVED_AT` 为 NULL | 终态行无标记且无超期判据 | 仅由退避重试保证;【目标】以入队时间兜底(§5.2/§12) |
|
||||
| 投递目标已接受、`SENT` 未置 | 事件仍 `PENDING` | 允许重发,消费方按事件身份去重 |
|
||||
|
||||
两个历史窗口(提交后回填前崩溃、回填意图二次落账失败)已随"终态与回填意图同体同行"消除——意图不存在第二处落账(口径与 design.md §10「事务与外部副作用」一致)。仍属未闭环的只有回填本身在跨库单写期间的持续失败:由退避重试与 §5.2 的超期期限 `R` 兜底,登记于 design.md §10 与 user-stories.md US-09;"信箱行不存在"已有终态(§5.2 放弃语义)。
|
||||
|
||||
## 5. 消费水位、超期补写与历史积压
|
||||
|
||||
### 5.1 消费水位 W 与空洞
|
||||
|
||||
**定义**:水位 `W` 是信箱 ID 的连续上界——从最小 ID 到 `W` 的区间已全部读入自有 PG,无空洞;遇到空洞即停止推进。`W` 只随新 ID 的成功入队推进(永久空洞放行是唯一例外),不依赖处理完成或标记回写。
|
||||
|
||||
**收报流程**(每轮,谓词不以处理标记为条件):
|
||||
|
||||
1. 读取游标 `(W, holeSince)`;信箱不可读时记日志、等下一轮,**不动水位**——这属于基础设施失败,不能当成"没有新消息"。
|
||||
2. 取 `CMINMSGS_ID > W` 的升序前 `claim-batch` 行(`ORDER BY ID ASC LIMIT N`)。
|
||||
3. 从 `W+1` 起逐 1 数,求连续上界;遇到第一个缺号即停止计数。
|
||||
4. 空洞判定(仅当本批存在"缺号之后的行"时才可能成立):
|
||||
- 缺号首次被观测到 → `holeSince = now`;
|
||||
- `now − holeSince < max-commit-delay` → 水位停在缺号前,**本批缺号之后的行一律不入队**。没有这条规则,晚提交的较小 ID 会排到它们后面,破坏 FIFO;
|
||||
- `now − holeSince ≥ max-commit-delay` → 判定为永久空洞,水位放行到"缺号后第一行 − 1",`holeSince` 清空。放行**只跳过空洞本身,不越过任何已存在的行**。
|
||||
5. 在同一个 PG 事务内:对水位以内的每一行 `insertIfAbsent(MSG_ID, RECEIVED_AT)`,并写回 `(W, holeSince)`;主键冲突表示已入队(重复扫描与兼容入口并发都安全),不计入也不报错。
|
||||
6. 提交。本批中因空洞或批次上限未入队的行留待下一轮——**每轮最多解决一个空洞**。
|
||||
|
||||
**空洞计时跨重启保留**:`holeSince` 落在 `INBOX_CURSOR.HOLE_SINCE`,进程重启不丢失。旧空洞补齐后出现的新空洞,从新观测时刻重新计时,不继承旧等待时间。
|
||||
|
||||
**代价(必须接受并观测)**:水位遇空洞即停意味着**该空洞之后的所有消息最多要等 `max-commit-delay` 才能入队**。自增回滚等会在 ID 序列中留下永久空位,因此每出现一个永久空位就是一次等长的入队停摆;空位频繁时有效吞吐按比例下降。运行期必须观测永久空洞计数与水位滞后(§5.3 验收口径、OPS-2)。
|
||||
|
||||
**水位的有效性依赖 Q2 的两条上游承诺**:
|
||||
|
||||
1. **ID 单调**:信箱 ID 按提交顺序分配,晚提交的较小 ID 不参与。承诺缺失时,`W` 只能作为快路径的提示,不能证明该区间收齐。
|
||||
2. **ID 分配 → 事务可见时延上界**:从"库方为该报文分配 ID"到"该 ID 对其他事务可见"的最长时间。它同时决定空洞老化阈值与补偿扫描窗口宽度。
|
||||
|
||||
**这条时延必须由库方直接给出,不能由 SIS 的报文有效期推导。** SIS 的 `Expiry` = 480 分钟、断连 120–480 分钟按 Level 2 处理(`SIS_AODB_RMS-V0.1.md` §2.4.2.3.3、§3.16、§3.17、§5.2)描述的是**报文保留与传输恢复**,与"共享 MySQL 里 ID 分配后多久对读事务可见"不是同一个量,二者之间没有推导关系。因此当前默认的 `max-commit-delay = 5 分钟`**缺少依据**,只是占位假定值,属于上线门槛([design.md](design.md) §7/§10)。报文有效期只能作为**原文保留期 / 重放窗口**(§6、§9)的参照,不能反过来给本阈值背书。
|
||||
|
||||
**两条扫描路径(发现机制)**:
|
||||
|
||||
| 路径 | 目的 | 谓词 | 状态 |
|
||||
|---|---|---|---|
|
||||
| 快路径(日常) | 发现水位之后的新消息 | `ID > W ORDER BY ID ASC LIMIT claim-batch` | 【现状】已实现 |
|
||||
| 迟到检测(只读,阶段 0) | 复查**被放行的空洞 ID** 是否后来真的出现在信箱 | 进程内监视队列 + `existingIds` 批量存在性检查(`late-detect-period`) | 【现状】已实现:只计数与告警,**不补入队** |
|
||||
| 补偿扫描(阶段 1) | 发现"提交晚于水位推进"的迟到行并**安全补入队** | 按周期重扫 `W` 之前一个窗口(宽度由最大提交时延决定)内的 ID 区间 | 【缺口】未实现 |
|
||||
|
||||
【缺口】在补偿扫描交付之前,"较小 ID 迟提交"**没有补入队机制**:快路径只读 `ID > W`,一旦水位越过某个 ID,该 ID 之后到达的消息永远不会被发现。阶段 0 的**只读迟到检测**已实现——监视"被放行的空洞 ID"是否后来真的出现,命中即计数(`msgx.pipeline.late_arrival.detected.total`)并告警,把原先的**静默丢失**变成可观测事实;但它**不补入队**,因此当前设计仍**不能对外声明"迟到不越序"**([design.md](design.md) §8/§10)。检测是进程内尽力而为(重启丢失监视;监视队列有上界,被放行 ID 超量后只保留后段),且只覆盖"曾被放行过的空洞 ID"——这已足够:水位只会越过连续存在的行与被判定为空洞的 ID。
|
||||
|
||||
**水位与其余两条写路径的关系**:
|
||||
|
||||
- **兼容 HTTP 入口**(`POST /cminmsgs/send`):它直接写 `PROC_STATE`、不读不推水位,登记的行因此**超出水位**;主泵只领 `msgId ≤ W`,所以在水位追平前不会被处理——顺序不受影响,代价是延迟到追平,且**必须让收报轮询运行**(收报侧事实由 `InboxPollerTest` 固定,端到端由 `PipelineSmokeTest` 守住)。
|
||||
- **回填**:不回写水位。水位不是处理标记,两者互不替代。
|
||||
|
||||
**单实例前提**:信箱读取不加锁,水位是单行覆盖写。本设计只在单活动实例下成立([architecture.md](architecture.md) §5、D2);多实例并发收报会让水位互相覆盖(覆盖回退只会造成重复扫描,不会丢消息,但空洞计时会失真),必须先有实例级排他。
|
||||
|
||||
**首次启动**:游标初值为 `(W=0, holeSince=NULL)`,第一轮会重读信箱中全部现存行;重复登记由主键幂等挡住,因此历史上"已入队但未回填"造成的漏读会一并补齐。
|
||||
|
||||
**切流播种(显式、一次性)**:若信箱已有存量(典型情况是最老分区已被清除、`MIN(ID)` 远大于 1),从 `W=0` 启动会先把 `ID=1` 判成空洞、白等一个老化窗口,同时也意味着要重新处理保留期内的全部存量。是否跳过存量属于**切流决策**,因此代码不做默认选择:只有显式配置 `msgx.pipeline.cutover-watermark` 才播种,取值 `min`(读现存全部)/ `zero`(从 0 按空洞规则)/ `max`(跳过可见存量)/ 具体 ID。升级实例(已有水位或已有处理记录)**拒绝重新播种**,重新切流必须是显式操作;播种事实与水位同语句落库(`INBOX_CURSOR.SEEDED_AT`),且该列为 NULL **不等于**"从未消费"(已有库新增列后同样为 NULL)。
|
||||
|
||||
### 5.2 超期标记补写
|
||||
|
||||
所有处理终态都只依赖消息 ID 执行回填;报文残缺或缺少 META 不妨碍正常回填。本节规定回填的四种结果、"长期失败"时的超期兜底,以及 `R` 对重放窗口的实际作用。
|
||||
|
||||
**处理规则**:已达 `PROC_STATE` 终态、且接收时间超期(信箱 `DATE_RECEIVED`,落库为 `PROC_STATE.RECEIVED_AT`;判据 `RECEIVED_AT < NOW − R`)仍无标记的信箱行,由回填通道补写一个库方认可的"已处理"类标记。
|
||||
|
||||
**扫描谓词**(与实现一一对应):
|
||||
|
||||
```text
|
||||
STATE ∈ {SUCCEEDED, SKIPPED, DEAD} -- 终态
|
||||
AND BACKFILL_AT IS NULL -- 标记尚未确认
|
||||
AND BACKFILL_ABANDONED_AT IS NULL -- 未放弃(放弃行可人工恢复)
|
||||
AND ( BACKFILL_NEXT_AT IS NULL -- 异常兜底:终态行没有退避时间
|
||||
OR BACKFILL_NEXT_AT ≤ NOW -- 退避到期
|
||||
OR ( RECEIVED_AT IS NOT NULL -- 接收时间可用
|
||||
AND RECEIVED_AT < NOW − R ) ) -- 已达超期期限,覆盖退避(一旦成立恒成立)
|
||||
ORDER BY BACKFILL_ATTEMPTS ASC, MSG_ID ASC -- 公平轮转,永久失败行不占满批次
|
||||
LIMIT backfill-batch
|
||||
```
|
||||
|
||||
**回填的可能结果**:
|
||||
|
||||
| 结果 | 判定 | 处置 |
|
||||
|---|---|---|
|
||||
| 写入成功 | 标记为空、写入 1 行 | 记 `BACKFILL_AT`,不再重试 |
|
||||
| 早已有标记 | 写入 0 行且信箱行存在 | **视为成功**,不覆盖已有值,记 `BACKFILL_AT` |
|
||||
| 信箱行不存在 | 写入 0 行且信箱行不存在 | **立即放弃自动重试**(原因 `MISSING_ROW`)并告警。这是确定性结论,重试不会改变结果 |
|
||||
| 暂时性故障达上限 | 超时/连接失败累计达到 `backfill-max-attempts` | **停止自动重试**(原因 `MAX_ATTEMPTS`)并告警;保留人工恢复能力 |
|
||||
|
||||
**放弃 ≠ 标记已确认**:放弃行不写 `BACKFILL_AT`,因此**不满足** §6"边界内全部行已打标"的清除前提,库方不得据此清除。放弃行可经人工恢复(清标记后重排一次回填)。
|
||||
|
||||
**约束**:
|
||||
|
||||
- 期限 `R` 的语义是"回填长期失败时的强制补写上限"。**`R` 在任何清除语义下都不保护重放窗口**——因为回填不等 `R`:终态落库后由扫描尽快补写(§3 表 2),`R` 只在"回填持续失败"时把补写**提前**,从不推迟补写。
|
||||
- 重放窗口的保护只能来自下面两条路之一,且 Q7/Q9 的确认是**阻塞性前提**,不是"调大 `R`"就能覆盖的参数问题:
|
||||
- **约定保留期(目标前提)**:库方按"标记 + 保留期 `R_keep`"清除(打标本身不触发清除,§6),且 `R_keep ≥ 人工重放期限 + 人工处置期限`。此时 `R` 只需满足 `R ≤ R_keep`,与重放窗口无关。
|
||||
- **另设原文保留机制**:若库方的清除语义是"一旦打标即可清除"(未确认),则**增大 `R` 无效**——当天进入终态、当天打标的消息会被当天清除,即使 `R = 30 天`。此时必须另行约定保留期,或引入独立的原文保留通道(例如由库方把原文归档到 `CMINMSGS_HST` 后供重放读取);该通道**尚未设计**(§12 G10)。
|
||||
- 补写值仅限于库方认可的 legacy 值集(Q7);死信、业务重复等内部原因记录在 `PROC_STATE` 与审计日志,不在信箱新增枚举。
|
||||
- `RECEIVED_AT` 为 NULL(上游未写 `DATE_RECEIVED`)时,超期分支不成立,`R` 兜底**不生效**;该行只能靠退避(封顶 15 分钟)反复重试。【目标】接收时间缺失时以入队时间兜底。
|
||||
- **扫描周期 ≠ 完成时限**:30 秒只是**调度周期**。`JobRunner` 串行执行 `backfill.sweep` 与历史作业后才等待 30 秒,批次积压、单行调用超时与历史作业耗时长都会延长实际回填延迟;只有在"正常无积压"时实际延迟才近似等于扫描周期。因此任何"标记延迟 ≤ 30 秒"的表述都不成立,**回填完成目标是独立指标**(§5.3 验收口径、OPS-2),需要硬时限时必须另行补齐容量、作业隔离与故障条件。
|
||||
|
||||
### 5.3 历史积压消息的处理方案
|
||||
|
||||
"历史积压"指信箱中成规模的未处理存量:上线前遗留、停机期间累积、或消量未完成的批次。处理方案分三步,并约束四条行为红线。
|
||||
|
||||
**第一步:摸底。** 处理动作开始前,先确定积压的范围与构成:ID 区间、条数、时间跨度、报文类型分布(`SCHD-DNLD/RESP/ADFT`、`FLOP-*`、`FDEL` 等,逐类清单依据见 Q8),并与库方确认其中哪些仍需业务处理、哪些按约定跳过(跳过基调只在该批确认时成立,授权与留痕见 Q12,标记值集见 Q7)。
|
||||
|
||||
**第二步:入队与处理的顺序关系。** 顺序由 `MSG_ID` 决定,**不由"先入队后处理"这种执行方式决定**:
|
||||
|
||||
1. 入队:`InboxPoller` 按升序有限批次将历史行全部建为 `PROC_STATE(PENDING)`,水位随之推到积压末端(连续推进以 §5.1 的 Q2 承诺为前提)。此阶段只写自有 PG,不触碰信箱标记。
|
||||
2. 处理:主泵按最小未完成 ID 顺序消化。顺序、阈限与日常完全相同:不加速、不分流、不走旁路。积压期间到达的实时消息 ID 更大,自然排在积压之后。
|
||||
|
||||
**入队与处理可以并发**:两者由不同线程驱动,FIFO 由"队头取最小未完成 `MSG_ID`"保证。因此"先入队后处理"是**可选的运维规程**、**不是正确性前提**,系统也不提供"只入队"模式;入队阶段的中断恢复只涉及重扫(§4 第一行)。
|
||||
|
||||
**第三步:等待过程中的四个边界。**
|
||||
|
||||
- 不插队:主泵按 FIFO 消化,积压期间到达的实时消息排在积压之后。本设计不允许并行队头,也不允许实时通道跳过积压。
|
||||
- 不失控:队头滞留上限(`msgx.pipeline.head-deadline`,当前 10 分钟)对积压同样生效;队头长期失败按既有规则转 `DEAD(EXHAUSTED)` 并告警,不会因积压而延长容忍(判据口径见 design.md §3.2);长期积压与人工重放的期限口径仍由 Q6 定案。
|
||||
- 报文类型的处理方式不变:旧的全量日计划(`SCHD-DNLD`)按最新一份**合并**即可收敛(缺席不删、缺失字段保留;字段级冲突见 flight-state.md §3.1 与 Q13),但仍逐条执行;动态增量(`FLOP-*` / `FDEL` / `ADFT`)持有时序语义,必须逐条。
|
||||
- 可放弃但必须留痕:摸底批中经库方确认"不再处理"的行,处置方式为——`PROC_STATE` 置 `SKIPPED` 并记录跳过原因,到达终态后走 §5.2 通道补写标记;本方案不支持任何"整段 DELETE"的快速通道。
|
||||
|
||||
**验收口径**:积压消化期间持续输出三项指标——剩余积压条数(`unfinished`)、最老未处理信龄(`oldestUnprocessedSeconds`)、**预计消化时长**(定义:`剩余积压条数 ÷ 近 N 分钟实际处理速率`,速率样本窗口 N 需在实现时固定;若实现不提供该估算,本条降级为"由运维按前两项指标自行推算")。另需观测水位滞后与永久空洞计数(§5.1)。期间不允许出现 FIFO 越序、身份去重失效或头行滞留超时未告警。红线依据:architecture.md §5(消息严格 FIFO)、单写者约束。以上指标并入 user-stories.md OPS-2 的可观测性验收。
|
||||
|
||||
## 6. 信箱数据清除
|
||||
|
||||
表结构变更与物理清除由库方执行或书面授权执行;本系统对共享 MySQL 不建表、不改结构(红线见 architecture.md §6)。本系统不执行 DDL、不写共享历史表:`CMINMSGS_HST` 的历史归档写入由库方执行,属 Q9 授权范围。本系统在清除事务中的义务只有一项:为处理完成的行及时写入处理标记,使可清除范围存在明确边界。
|
||||
|
||||
具体方案由库方选择(Q9)。以下两种方案均基于 MySQL 自身能力:支持 `RANGE` 分区与 `TRUNCATE / DROP PARTITION`,不支持 `EXCHANGE PARTITION`(能力与现场版本待库方书面确认,Q9)。
|
||||
|
||||
**方案 A:按日分区(首选)**,适用于库方可以为表增加分区的场合。
|
||||
|
||||
1. `CMINMSGS` 按 `DATE_RECEIVED` 建立日粒度 RANGE 分区;
|
||||
2. 某分区到达保留期时,确认该分区全部行已持有处理标记(§5.2 只在回填可持续成功时保证该条件;放弃行与"打标即清除"语义都会破坏该前提,见 §12 G10);
|
||||
3. 将该分区复制入历史表:`INSERT INTO CMINMSGS_HST SELECT`(`NOT EXISTS` 判重);
|
||||
4. `TRUNCATE / DROP PARTITION` 执行清除:DDL 级操作,无行锁竞争,页外大字段(`CMINMSGS_CLOB_MSG` 列)整块释放;碎片与 binlog 影响待现场验证。
|
||||
|
||||
前提:库方确认现场 MySQL 版本支持分区 DDL,并已授权执行。
|
||||
|
||||
**方案 B:整表轮换**,适用于库方拒绝增加分区的场合。
|
||||
|
||||
1. `CREATE TABLE CMINMSGS_NEW LIKE CMINMSGS`,并将 `AUTO_INCREMENT` 种子设为 `max(ID) + 1`;
|
||||
2. 将保留窗内行(`DATE_RECEIVED ≥ NOW − R_keep`)复制至新表;
|
||||
3. 以单语句原子 `RENAME TABLE` 完成换名;
|
||||
4. 对账:换名与复制之间新写入及新标记的行,从旧表补回;只补新表缺失的行,已存在行按标记单调取并集,不回退、不覆盖(§11);
|
||||
5. 旧表中早于保留窗的部分追加写入 `CMINMSGS_HST`(`NOT EXISTS` 判重);
|
||||
6. `DROP TABLE` 旧表:秒级完成,碎片与大字段一并释放。
|
||||
|
||||
两种中断均安全:换名前失败则废弃新表重新执行;换名后失败则旧表仍完整,对账与归档语句均可重跑。
|
||||
|
||||
方案共同前提:
|
||||
|
||||
- **方案要求库方按"标记 + 保留期"清除**(打标本身不触发清除):处理标记只是可清除的**必要条件**,触发条件是"到达保留期 `R_keep`" ∧ "边界内全部行已持有处理标记"。该语义属 Q7/Q9 确认范围;若确认结果是"打标即可清除",则**本节前提不成立,且增大 `R` 无法补救**(原因唯一见 §5.2),必须另行约定保留期或引入独立原文保留通道(§12 G10)。
|
||||
- 执行清除时,边界内不存在未打标记的行;未达终态的行顺延至处理完成后清除(§5.2 仅对终态行补标)。
|
||||
- 时间比较与换算统一采用机场时区 Asia/Shanghai 及明确的类型转换(口径同 user-stories.md §6)。
|
||||
- 保留期 `R_keep` 的下界(**本节是唯一表述处**):`R_keep ≥ max(人工重放期限 + 人工处置期限, 审计期限, 回填重试上限)`——**仅在"标记 + 保留期"清除语义下**,这是"重放窗口内原文仍在"的**唯一保证来源**。报文在 CIIMS 的 `Expiry`(480 分钟量级,`SIS_AODB_RMS-V0.1.md` §3.16)可作为原文保留期的参照,但它是报文有效期,不等于本处所需的保留期,也不能用于推导 §5.1 的可见性时延。
|
||||
- 超期补写期限 `R` 的约束(仅 `R ≤ R_keep`)**唯一见 §5.2**;`R` 不参与重放窗口保护,本节不重复。
|
||||
- legacy 现役按接收时间超过 1 天即归档并删除 `CMINMSGS`(legacy 行为基线 §3.6);若沿用该窗口,则与上一行的保留期下限冲突,须在 Q9 中与库方一并确认;
|
||||
- 信箱 ID 全程不断链:方案 A 天然满足;方案 B 依赖 `AUTO_INCREMENT` 种子,种子缺失时新 ID 与旧记录主键冲突,水位随之失效。
|
||||
|
||||
## 7. 出站信箱(COUTMSGS)
|
||||
|
||||
本系统一侧的规则如下(出站适配与请求协调**尚未实现**;请求生命周期的机制目标见 design.md §4.2,本节不重复):
|
||||
|
||||
出站前在 `REQ_TRACK` 登记 `REGISTERED`;写入 `COUTMSGS` 并确认落信后置 `SENT` 并关联出站记录 ID。"落信"指写入信箱成功,不等于下游已读取或已发送,交付承诺仅到落信为止(US-08)。
|
||||
|
||||
主/共享删除顺序违例时 SIS 要求向 AODB 回发 EROR(`SIS_AODB_RMS-V0.1.md` §1.6.1-1.d,事件定义见 SIS §4.8);当前没有该出站路径,取舍见 Q14。
|
||||
|
||||
本系统写入的行由谁消费、按什么顺序消费,**当前未知**。以下事项在 Q10 关闭前按未知处理,不得作为已具备能力描述:
|
||||
|
||||
- 消费方与消费顺序;
|
||||
- `COUTMSGS_ACK_DATE_RECV`、`COUTMSGS_ACK_RESEND_TIMES`、`COUTMSGS_DATE_SENT`、`COUTMSGS_ERROR` 各列的语义与写入责任;
|
||||
- 出站行的清除责任与保留期;
|
||||
- 落信成功但本地未置 `SENT` 时的重复写入风险及下游去重契约。
|
||||
|
||||
参考事实:legacy 的 SIS 接口规范记载各系统持有各自的 `COUTMSGS` 副本,由 JDBC Adapter 读取后发送至 CIIMS;同时记载从 RMS 发出的报文不执行 CIIMS 确认(`SIS_AODB_RMS-V0.1.md` §2.4.2.2.2、§2.4.2.2.4)。这两点是否在现场沿用、与 `COUTMSGS_ACK_*` 列的关系如何,均属 Q10 的确认范围,不作为现状依据。
|
||||
|
||||
## 8. 自有记录归档
|
||||
|
||||
- `PROC_STATE`:终态记录归档至 `PROC_STATE_HST`(尚未建表)。未达终态的记录不归档;归档不得使"同一业务身份只能绑定一条有效处理记录"的去重能力失效(US-11)。
|
||||
- `SCHD_SNAP_LOG`:保留 90 天,清理窗口与判据见 design.md §6.2。
|
||||
- 航班当前态的清理规则唯一归属 [flight-state.md](flight-state.md) §6;其判据不依赖信箱原文。信箱原文的可用性仅影响重放能力(§9)。
|
||||
|
||||
## 9. 重放与原文可用性
|
||||
|
||||
- 可重放的错误类别为 `CODEC_ERROR / UNSUPPORTED / INFRA / EXHAUSTED` 四类(以 design.md §6.1 为准);重放处理当前状态,不恢复历史顺序。
|
||||
- 重放的前提是信箱原文仍可读取。该前提仅在"标记 + 保留期"清除语义下可由配置满足(`R_keep` 覆盖重放与处置期限,§6);"打标即清除"语义下的处置唯一见 §5.2 与 §12 G10。
|
||||
- 确认信箱行或原文已缺失时,按 `MALFORMED → DEAD` 处理;共享库超时、连接失败等暂时性读取异常按 `INFRA` 退避重试,不得伪装成原文缺失。若原文缺失源于库方违反保留契约提前清除,按契约违例走运维追责通道(契约条款见 Q7/Q9);该消息的死信处置本身不变。
|
||||
- **回填阶段的"信箱行缺失"是另一回事**(§5.2):它表示"本系统已经处理完、却找不到要标记的那一行"。此时不改变处理终态,也不重新处理业务;需要区分"该 ID 本就属于被判定的永久空洞"与"行被提前清除"。处置按 §5.2:确认 `MISSING_ROW` 即放弃自动重试并告警,人工对账后才能重排回填。
|
||||
|
||||
## 10. 开放问题索引
|
||||
|
||||
| 编号 | 待确认事项 | 当前假定值 | 阻塞谁 | 风险 | 本文相关章节 |
|
||||
|---|---|---|---|---|---|
|
||||
| Q2 | 信箱 ID 单调承诺;**ID 分配 → 事务可见时延上界**;空洞与迟到处理 | 时延按 5 分钟(`max-commit-delay`,**缺少依据的占位值**;不可由 SIS 报文 `Expiry` 推导) | 快路径的发现完整性声明、空洞老化阈值、补偿扫描窗口 | 迟到小 ID 永久不被发现;空洞被误判为永久 | §5.1、§6 |
|
||||
| Q6 | 重放 deadline 与人工处置期限的取值(唯一作用是决定 `R_keep` 下界) | R = 30 天;重放/处置期限未定 | `R_keep` 的最终取值 | `R_keep` 不足导致重放窗口内原文被清除;**若清除语义为"打标即清除",则任何取值都无效**(§5.2) | §5.2、§6、§9 |
|
||||
| Q7 | 处理标记值集与写权限;原文保留期;处理时间语义 | 写入 `PROCESSED` | 回填值集、原文保留期下限 | 值集不被库方接受;保留期短于重放窗口 | §5.2、§6 |
|
||||
| Q8 | 逐类覆盖清单(积压摸底的类型分布依据) | — | 积压摸底与逐类矩阵 | 类型漏项导致积压处置误判 | §5.3 |
|
||||
| Q9 | 清除执行方与 DDL 授权;方案 A / B 选型 | 首选方案 A | `R_keep` 与清除边界 | 方案不可执行;ID 断链 | §6 |
|
||||
| Q10 | 出站消费方、ACK 列语义、出站清理与去重契约 | — | 出站信箱 | 重复写入、无法清理 | §7 |
|
||||
| Q11 | 上游 `SEQN` 重置周期与业务身份的日期边界 | 不含日期边界 | 身份算法 | 跨周期误判重复 | design.md §2.2 |
|
||||
| Q12 | 历史积压批次“不再处理”的确认主体、审批留痕与跳过值集 | — | 积压跳过处置 | 无授权跳过或删除 | §5.3 |
|
||||
| Q13 | 日计划缺失可选字段的删除语义(与 SIS §3.16 注释 4 冲突) | 保留未携带字段 | 快照合并 | 与 SIS 语义冲突 | §5.3、flight-state.md §3.1 |
|
||||
| Q14 | 主/共享删除顺序与 EROR 回报义务 | 自动幂等级联 | 出站事件类型 | 上游状态不一致 | §7、flight-state.md §3.3 |
|
||||
|
||||
Q2、Q6–Q14 的完整登记见 [user-stories.md](user-stories.md) §6 的 Q 表。
|
||||
|
||||
## 11. 不变量
|
||||
|
||||
- 五个事实互不替代:入队不引用信箱标记,回填不引用投递,投递不引用回填。
|
||||
- 发现与处理互不阻塞:收报只看 `ID > W`,终态而未回填的行不阻断后续消息的发现;处理状态不改变扫描谓词。
|
||||
- 队头唯一:任一时刻只有一个可执行队头,`FAILED` 未退避到期时后续消息不得越过。
|
||||
- 只领取已发现的行:主泵只领 `MSG_ID ≤ W`;水位之外的行只可能来自兼容入口,必须等水位追平后按序处理(否则会越过尚未入队的较小 ID)。
|
||||
- 处理终态不可逆:已提交的 `SUCCEEDED` 不因回填或投递失败回改。
|
||||
- 处理标记单调:任何路径只将空标写为已处理,不回撤、不覆盖。
|
||||
- 回填只针对终态:`PENDING / FAILED` 永不写信箱标记。
|
||||
- 一信一行:每条信箱行在 `PROC_STATE` 至多一条记录(`MSG_ID` 主键);同一业务身份至多绑定一条有效处理记录。
|
||||
- 水位不越过任何已存在的行;遇空洞即停,只有 §5.1 判定为永久空洞时才放行。
|
||||
- 写入水位与入队同事务:不允许出现"水位已推进、消息未入队"的持久化状态。
|
||||
- 执行清除前,边界内全部行已持有处理标记;物理删除仅发生在归档成功之后(追加写入与分区留档均构成归档成功)。
|
||||
- 信箱 ID 全局单调、不断链:以 Q2 的 ID 单调承诺与 §6 各方案前提成立为条件(方案 B 依赖 `AUTO_INCREMENT` 种子)。
|
||||
- 对外投递按至少一次设计;端到端恰好一次不在交付范围内。
|
||||
|
||||
## 12. 与当前实现的差异
|
||||
|
||||
本节是本文范围内的缺口清单,与 [design.md](design.md) §10 对应。正文描述目标设计;下列条目尚未交付,不得作为已具备能力引用。
|
||||
|
||||
| 编号 | 缺口 | 影响 | 相关章节 |
|
||||
|---|---|---|---|
|
||||
| G1 | **窗口补偿扫描未实现**(阶段 0 只读检测已交付) | 迟提交的小 ID 无补入队机制,不能声明"迟到不越序";检测覆盖与口径唯一见 §5.1 | §5.1、§4 |
|
||||
| G4 | **`RECEIVED_AT` 为 NULL 时 R 兜底失效** | 该行只能靠退避重试;V2 只对存量行用 `UPDATED_AT` 兜底,新行没有 | §5.2、§4 |
|
||||
| G5 | **毒丸升级不在 `MessageLifecycleGate` 内** | 与人工重放并发时,存在"先标 DEAD 并写标记、再被重放拨回 `PENDING`"的窗口。**注意**:回填与重放**共用同一个 gate 单例**这一点已由装配断言守住(`PipelineSmokeTest`),但毒丸路径仍在 gate 之外 | §2、§5.3;design.md §3.2/§6.1 |
|
||||
| G6 | **"预计消化时长"指标无实现定义** | §5.3 验收口径的第三项指标尚无算法与实现(已降级为可推算项) | §5.3 |
|
||||
| G7 | **`max-commit-delay` 默认值缺少依据** | 库方尚未给出"ID 分配 → 事务可见"的时延上界;5 分钟只是占位假定值,**不能由 SIS 报文 `Expiry` 推导**(§5.1)。取值无依据时可能大量误判永久空洞 | §5.1、§6 |
|
||||
| G8 | **Q 编号条款尚未与库方书面确认** | 当前取值均为假定,见 §10 | §10 |
|
||||
| G10 | **"打标即清除"语义下没有原文保留通道** | 若库方清除由打标触发,则**回填成功后当天打标**会让原文当天即可被清除,增大 `R` 无效 → 重放窗口失去保护。需另行约定保留期,或引入独立原文保留通道(如库方归档 `CMINMSGS_HST` 供重放读取);该通道尚未设计 | §5.2、§6、§9 |
|
||||
|
||||
已关闭(现行行为并入正文,不再列为缺口):G2 兼容入口与水位领取(§5.1)、G3 回填缺失行放弃语义(§5.2)、G9 回填扫描公平轮转(§5.2 谓词)。
|
||||
保留本文件仅为承接历史链接;**不要在此新增内容**,规则定义处见 [README.md](README.md) 的事实归属表。
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
# 参考注册表:参数、指标、模块、错误分类
|
||||
|
||||
本文件是**可派生事实**的唯一出处:参数默认值、指标名、模块入口、错误分类。正文(design.md)只引 `PARAM:<完整键>`,不写数值。
|
||||
|
||||
维护规则:新增或改名一个 `msgx.*` 键、增删一个指标名,必须同步本文件(人工核对,本项目不做 CI 校验)。`mailbox.*`、`datasources.*`、`kafka.*` 属基础设施键,按 §1.3 成组登记。
|
||||
|
||||
## 1. 参数注册表
|
||||
|
||||
「依据」列含义:**契约** = 由 contracts.md 的 `C-x` 决定;**现役** = 沿用 legacy 行为基线;**假定** = 无依据的占位值,必须在对应 `Q` 关闭后重评;**安全默认** = 关闭态,需显式开启。
|
||||
|
||||
### 1.1 管道节奏与重试(`msgx.pipeline.*`)
|
||||
|
||||
| 参数 | 默认 | 单位 | 依据 | 说明 |
|
||||
|---|---|---|---|---|
|
||||
| `msgx.pipeline.poll-interval` | `1s` | Duration | 现役 | 收报轮询节奏;决定队头与空洞检查频率 |
|
||||
| `msgx.pipeline.claim-batch` | `50` | 条 | 假定 | 单轮领取上限;过大延长单轮事务 |
|
||||
| `msgx.pipeline.max-attempts` | `5` | 次 | 假定 | 处理与投递共用;达到即转 `DEAD(EXHAUSTED)` |
|
||||
| `msgx.pipeline.backoff-ms` | `[1000,2000,4000,8000]` | ms / 档 | 假定 | **档位数必须 = `max-attempts − 1`**,启动自检拦截错位 |
|
||||
| `msgx.pipeline.backoff-cap-ms` | `60000` | ms | 假定 | 单档封顶;默认表内无档触及 |
|
||||
| `msgx.pipeline.head-deadline` | `10m` | Duration | 假定 | **仅用于队头滞留告警**,不作终态判据(判据只由 `max-attempts`)。代码注释仍写「毒丸升级」,属待收敛的表述差 `[G-HEAD-DEADLINE]` |
|
||||
| `msgx.pipeline.max-commit-delay` | `5m` | Duration | **假定(无依据)** | 空洞老化阈值;由 `C-2` 决定,**不可由 SIS `Expiry` 推导**(`Q2`) |
|
||||
| `msgx.pipeline.overdue-backfill`(`R`) | `30d` | Duration | 契约(`R ≤ R_keep`) | 进入强补写窗口、**取消退避**的阈值;**不是兜底保证**,不保护重放窗口(`Q6`) |
|
||||
| `msgx.pipeline.backfill-batch` | `100` | 条 | 假定 | 回填扫描单批条数 |
|
||||
| `msgx.pipeline.backfill-max-attempts` | `100` | 次 | **改作告警阈值** | **不再是放弃判据**:暂时性故障按 `R` 超期放弃(见 design「回填」)。该键保留为单行重试的告警阈值 `[G-BACKFILL-ABANDON-BYTIME]` |
|
||||
| `msgx.pipeline.backfill-backoff-ms` | 目标参数(未实现) | ms / 档 | 假定 | 回填独立退避表;**当前不存在,回填沿用 `backoff-ms`** `[G-BACKFILL-BACKOFF]` |
|
||||
| `msgx.pipeline.backfill-backoff-cap-ms` | 目标参数(未实现) | ms | 假定 | 回填退避封顶;旧文档「封顶 15 分钟」无对应配置键,已作废 `[G-BACKFILL-BACKOFF]` |
|
||||
| `msgx.pipeline.cutover-watermark` | 不设置 | `min\|zero\|max\|<id>` | 一次性运维决策 | 显式播种水位;非法值由启动自检挡下;升级实例拒绝重新播种 |
|
||||
| `msgx.pipeline.late-detect-period` | `60s` | Duration | 假定 | 只读迟到检测周期;`≤0` 关闭;机制为临时观测(见 design 扫描路径) |
|
||||
| `msgx.pipeline.late-detect-batch` | `200` | 条 | 假定 | 每轮复查的空洞 ID 上限 |
|
||||
| `msgx.pipeline.delivery-batch` | `200` | 条 | 假定 | `KAFKA:msg` 每轮每目标领取上限 |
|
||||
| `msgx.pipeline.delivery-drain-rounds` | `10` | 轮 | 假定 | 连取批数上限,让出循环跑 `schd` flush,防状态通知被积压饿死 |
|
||||
| `msgx.pipeline.autostart` | `false` | 布尔 | 安全默认 | 启动即拉起收报 / 主泵 / 投递循环;需真实仓储或 `msgx.stubs=true` |
|
||||
| `msgx.pipeline.event-retention` | 目标参数(未实现) | Duration | 假定 | 已 `SENT` 事件行保留期,由维护作业清理;缺失则 outbox 无限增长 `[G-EVENT-RETENTION]` |
|
||||
|
||||
### 1.2 其它 `msgx.*`
|
||||
|
||||
| 参数 | 默认 | 依据 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `msgx.service-name` | `msgexchangeapi` | 契约冻结 | 服务注册名;影子实例用独立名 |
|
||||
| `msgx.register-eureka` | `true` | 现役 | 服务注册开关;影子对拍期置 `false` |
|
||||
| `msgx.stubs` | `true`(dev profile) | 安全默认 | 内存适配器;**生产禁止**,生产 profile 不设置该键 |
|
||||
| `msgx.schd.flush-period` | `3s` | 现役 | `schd` 聚合周期 |
|
||||
| `msgx.schd.flush-limit` | `500` | 假定 | 聚合批上限 |
|
||||
| `msgx.operation-day.zone` | `Asia/Shanghai` | 现役 | 运营日推导时区 |
|
||||
| `msgx.operation-day.cutoff-hour` | `0` | **假定(占位)** | 切日边界;待业务确认 |
|
||||
| `msgx.identity.include-day-boundary` | `false` | 安全默认 | 身份是否加日期边界;影响去重语义,不能当调优项切换(`Q11`) |
|
||||
| `msgx.health.backlog-cache-ttl-ms` | `30000` | 假定 | 积压快照缓存窗口;`0` = 不缓存;`/health` 与 `/metrics` 共用同一快照 |
|
||||
| `msgx.history.history-store-enabled` | `false` | 安全默认 | 历史存储门控;关闭时历史清理删 0 条 |
|
||||
| `msgx.history.cancelled-hours` | `48` | 假定 | 历史清理的取消态判据窗口 |
|
||||
| `msgx.history.terminal-hours` | `48` | 假定 | 历史清理的终态判据窗口 |
|
||||
| `msgx.history.deleted-hours` | `48` | 假定 | 历史清理的已删除判据窗口 |
|
||||
| `msgx.history.idle-hours` | `168` | 假定 | 历史清理的静默期判据 |
|
||||
| `msgx.history.snap-log-retention-days` | `90` | 假定 | `SCHD_SNAP_LOG` 保留天数 |
|
||||
|
||||
### 1.3 信箱与外部依赖(成组登记)
|
||||
|
||||
| 参数组 | 默认 | 依据 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `mailbox.processed-value` | `PROCESSED` | 契约(`C-5`/`Q7`) | 处理标记写入值;仅限库方认可值集 |
|
||||
| `mailbox.shared-mysql.enabled` | `false` | 安全默认 | 真实信箱接线门控 |
|
||||
| `mailbox.shared-mysql.connect-timeout-ms` / `socket-timeout-ms` | `3000` / `30000` | 假定 | 有界外部调用;Connector/J 默认无限等待,必须显式 |
|
||||
| `mailbox.shared-mysql.pool-connection-timeout-ms` / `pool-validation-timeout-ms` | `5000` / `3000` | 假定 | 池级超时 |
|
||||
| `mailbox.shared-mysql.url` / `username` / `password` | 环境变量 | 安全 | 零入库,见 `.env.example` |
|
||||
| `datasources.default.connection-timeout` / `validation-timeout` / `idle-timeout` / `max-lifetime` | `5000` / `3000` / `300000` / `1800000` | 假定 | 自有 PG 池(毫秒数,非 Duration 字面量) |
|
||||
| `datasources.default.data-source-properties.connectTimeout` / `socketTimeout` | `3` / `30` | 假定 | 驱动级超时(秒),防网络黑洞 |
|
||||
| `kafka.producers.default.acks` | `all` | 契约(architecture D3) | 允许环境变量覆盖 |
|
||||
| `kafka.producers.default.enable-idempotence` | `true` | 契约(D3) | 允许环境变量覆盖 |
|
||||
| `kafka.producers.default.max-in-flight-requests-per-connection` | `5` | **与 D3 不一致** | D3 要求 `1`;切流前必须收敛 `[G-KAFKA-D3]` |
|
||||
|
||||
环境变量清单以 `.env.example` 为准。
|
||||
|
||||
## 2. 指标与健康
|
||||
|
||||
| 指标 | 含义 | 期望方向 |
|
||||
|---|---|---|
|
||||
| `msgx.pipeline.backlog.unfinished` | 未处理完的消息条数 | 长期 > 0 且不降 → 积压 |
|
||||
| `msgx.pipeline.backlog.oldest_unprocessed_seconds` | 最老未处理消息的信龄 | 持续增长 → 队头卡住 |
|
||||
| `msgx.pipeline.backfill.unmarked_terminal` | 已终态但未打标的条数 | 不降 → 回填失败 |
|
||||
| `msgx.pipeline.backfill.abandoned` | 已放弃自动回填的条数 | **非 0 需人工对账** |
|
||||
| `msgx.pipeline.backfill.oldest_unmarked_seconds` | 最老待回填年龄 | 决定实际回填延迟 |
|
||||
| `msgx.pipeline.watermark.lag` | 水位落后信箱最新 ID 的距离 | 增长 → 收报停滞 |
|
||||
| `msgx.pipeline.hole.aged_out.total` | 永久空洞放行次数 | 突增 → ID 序列大量空位 |
|
||||
| `msgx.pipeline.late_arrival.detected.total` | 迟到到达命中数 | **> 0 表示上游提交确实晚于水位推进,需要与库方对契约** |
|
||||
|
||||
取数规则:统一走 `BacklogSnapshotProvider`(`PARAM:msgx.health.backlog-cache-ttl-ms`),`/health` 与 `/metrics` 共用同一快照——`backlog()` 是 `PROC_STATE` 的全表聚合,不能被高频抓取打穿;**无法取数上报 `NaN`,无可比记录的年龄/滞后类仪表上报 `-1`,都不伪造 0**。日志出口故障不得阻塞业务线程。
|
||||
|
||||
作业健康:回填扫描的存活性与延迟需要独立可观测(作业心跳、扫描积压、实际回填延迟),因为回填的唯一驱动是扫描作业 `[G-JOB-HEARTBEAT]`。
|
||||
|
||||
## 3. 模块入口
|
||||
|
||||
| 关注点 | 主要入口 |
|
||||
|---|---|
|
||||
| 收报与兼容接口 | `ingress/InboxPoller.kt`、`InboxService.kt`、`InboxController.kt` |
|
||||
| 解码 | `codec/JacksonXmlCodec.kt`、`SisWireMapper.kt`、`SisMessageBody.kt` |
|
||||
| 调度与处理 | `processing/Pump.kt`(含 `MessageProcessor`)、`DynamicProcessors.kt`(FLOP/FDEL/ADFT)、`Identity.kt` |
|
||||
| 日计划 | `processing/ScheduleProcessor.kt`;请求协调尚无实现(`REQ_TRACK` 仓储见 `infra/persistence/`) |
|
||||
| 回填与投递作业 | `processing/BackfillService.kt`、`jobs/JobRunner.kt`、`HistorySweepJob.kt`、`delivery/Dispatcher.kt` |
|
||||
| 持久化与恢复 | `infra/persistence/`、`infra/retry/`(`ProcFailure` / `ReplayService` / `FailureScheduler`) |
|
||||
| 启停与配置 | `PipelineLifecycle.kt`、`config/PipelineProps.kt`、`config/HistoryProps.kt` |
|
||||
| 指标与健康 | `infra/metrics/PipelineMetrics.kt`、`infra/health/BacklogSnapshotProvider.kt` |
|
||||
| 迁移 | `src/main/resources/db/migration/`(V1 基线、V2 生命周期、V3 HOL 起点、V4 回填闭环、V5 切流播种;`oracle11g/` 为占位) |
|
||||
|
||||
## 4. 错误分类与重放白名单
|
||||
|
||||
| 错误类别 | 触发 | 处置 | 可重放 |
|
||||
|---|---|---|---|
|
||||
| `MALFORMED` | 报文非法或原文缺失 | 立即 `DEAD` | 否 |
|
||||
| `PROTOCOL` | 整包协议拒绝(运营日冲突、声明数不符、缺载荷) | 立即 `DEAD`,整包不落地 | 否 |
|
||||
| `CODEC_ERROR` | 解码能力问题 | `FAILED` 退避 | 是 |
|
||||
| `UNSUPPORTED` | 处理器或快照能力未实现 | `FAILED` 退避,受尝试上限约束 | 是 |
|
||||
| `INFRA` | 基础设施或执行异常 | `FAILED` 退避 | 是 |
|
||||
| `EXHAUSTED` | 尝试次数耗尽 | `DEAD`;`ERROR_CLASS` 被覆写为 `EXHAUSTED`(原始类别不保留,`LAST_ERROR` 留文本) | 是 |
|
||||
|
||||
回填放弃原因:`MISSING_ROW`(信箱行不存在,确定性结论)、`TRANSIENT_DEADLINE`(暂时性故障持续到 `R` 仍未打标)。两者都不写 `BACKFILL_AT`,都不等于标记已确认;`R` 之前不放弃。
|
||||
+19
-26
@@ -4,9 +4,9 @@
|
||||
|
||||
本文定义阶段 A 的实施范围与验收口径:**故事定义要交付什么,验收标准定义怎样证明完成,代码落点说明从哪里改起**。保留 US-01~US-15、OPS-1~OPS-4 编号,便于关联已有任务和测试。
|
||||
|
||||
- 系统边界见 [architecture.md](architecture.md),模块流程见 [design.md](design.md)。本文不重复设计全文,也不以工单状态代替代码验收。消息生命周期与信箱清除见 [message-lifecycle.md](message-lifecycle.md),航班规则见 [flight-state.md](flight-state.md)。
|
||||
- 系统边界见 [architecture.md](architecture.md),模块流程见 [design.md](design.md),前提与不变量见 [invariants.md](invariants.md),对外契约见 [contracts.md](contracts.md),航班规则见 [flight-state.md](flight-state.md)。不以工单状态代替代码验收。
|
||||
- “当前基础”来自本轮代码核对,只表示有接口或部分实现,不表示故事完成。`KEEP` 是保留业务兼容,`FIX` 是明确修正旧缺陷,`DEFERRED` 不进入阶段 A。
|
||||
- 五个独立事实(落信、入队、处理完成、回填、投递确认)的定义与判定见 [message-lifecycle.md](message-lifecycle.md) §1;接口、日志和测试必须分开表达,投递确认不等于业务消费者已消费。
|
||||
- 五个独立事实(落信、入队、处理完成、回填、投递确认)的定义与判定见 [design.md](design.md)「术语与持久化记录」;接口、日志和测试必须分开表达,投递确认不等于业务消费者已消费。
|
||||
- 所有内部迁移只落自有 PG;共享 MySQL 不建表、不增列、不写历史表。本文用 `DATE_PROCESSED / STATUS` 表示逻辑字段,实际列名以库方契约为准。
|
||||
- 验收条目可按 `US-xx/条目号` 引用。故事较大时按下文子范围拆成小 PR,不把一个故事等同于一个提交。
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
|
||||
**验收标准**
|
||||
|
||||
1. 按配置周期、ID 升序、有限批次采集信箱行;扫描谓词以 message-lifecycle.md §5.1 为准(按 ID 区间,不以处理标记为谓词)。接收层只入队,不解析业务、不回填已处理标记。
|
||||
1. 按配置周期、ID 升序、有限批次采集信箱行;扫描谓词以 [design.md](design.md)「收报与水位」为准(按 ID 区间,不以处理标记为谓词)。接收层只入队,不解析业务、不回填已处理标记。
|
||||
2. 按信箱 ID 幂等建立 PG `PENDING`;重复扫描、并发兼容入队和进程重启都不能重置已有终态。
|
||||
3. 快路径用持久水位,补偿路径受控重扫遗漏;本批 PG 入队全部确认后才推进水位。补偿可分页推进,不能被已入队但尚未回填的前一批永久挡住。
|
||||
4. PG 不可用或批次中途失败时不改信箱标记;恢复后补建遗漏,记录失败次数与扫描进度。
|
||||
@@ -44,7 +44,7 @@
|
||||
|
||||
**当前基础与落点**:`ingress/InboxPoller.kt` 按 ID 区间扫描(`ID > W`,不以处理标记为谓词),水位落 `INBOX_CURSOR` 并与入队同事务推进;`JdbcCminmsgInboxRepository.readRange/maxId` 与 `ProcStateRepository.insertIfAbsent` 承担发现与幂等入队。空洞老化阈值取 `msgx.pipeline.max-commit-delay`。剩余:Q2 未书面确认前,老化阈值与严格顺序仍是假定口径;真实 MySQL 的中断恢复与迟提交联合测试待现场环境。
|
||||
|
||||
**前置**:共享库读契约;Q2 决定严格顺序的端到端验收。水位与扫描谓词口径以 [message-lifecycle.md](message-lifecycle.md) §5.1 为准。
|
||||
**前置**:共享库读契约;Q2 决定严格顺序的端到端验收。水位与扫描谓词口径以 [design.md](design.md)「收报与水位」为准。
|
||||
|
||||
### US-02 兼容 HTTP 注入报文(KEEP)
|
||||
|
||||
@@ -112,8 +112,8 @@
|
||||
1. `SCHD-ADFT` 与 29 个 FLOP 子类型逐项列入覆盖矩阵,每项有对应的处理器规则与回归测试;未知类型可恢复失败。RESP/DNLD 不计入这批处理器,走 US-06。
|
||||
2. 每类固定“输入与前态 → 后态 → msg → schd → 终态”五面样例;区分字段缺失、显式清空、重复报文和主/共享航班。清单和 golden 样例按 Q8 补齐,不以“已写 29 个类”替代验收。
|
||||
3. 对按 KEEP 规则需忽略的不存在航班,以 `SUCCEEDED` 无副作用结束,并由 US-09 回填;ADFT 建航班等行为按各类型矩阵执行。航班当前态以自有 PG 为唯一权威,重启即恢复,不存在 Redis 全损后白名单无法找回的损坏路径。
|
||||
4. 共享航班更新与删除级联语义以 flight-state.md §3.3 为唯一规范(共享航班通知、主航班 `MAFL` 更新、级联删除、原子变更;不出现主已删、子残留);本条目验收实现不偏离该规范,目标不存在时幂等成功。
|
||||
5. ADFT/FDEL 的值相等比较与半状态禁止规则见 flight-state.md §3.3。
|
||||
4. 共享航班更新与删除级联语义以 [flight-state.md](flight-state.md)「删除与重建」为规范(共享航班通知、主航班 `MAFL` 更新、级联删除、原子变更;不出现主已删、子残留);本条目验收实现不偏离该规范,目标不存在时幂等成功。
|
||||
5. ADFT/FDEL 的值相等比较与半状态禁止规则见 [flight-state.md](flight-state.md)「删除与重建」。
|
||||
6. PSDT 通过 US-14 的只读映射计算 `abdg`,处理器不直接调用 admin-api。
|
||||
|
||||
**当前基础与落点**:落点已变为 `processing/DynamicProcessors.kt`(`FlopProcessor`/`FdelProcessor`/`AdftProcessor`)与 `domain/flight/FlightStateEngine`、`FlightStateRepository`。FDEL/ADFT 与通用 FLOP 处理器已接入(含 DELETED 幂等与重激活、tombstone 同事务登记);29 类逐类语义矩阵与 golden 样例仍未补全,不能因处理器存在就视为覆盖完成。
|
||||
@@ -192,7 +192,7 @@
|
||||
|
||||
**当前基础与落点**:回填意图与处理终态同体同行(`PROC_STATE.BACKFILL_*`),随业务事务提交,`BACKFILL_TODO` 已随 V2 迁移下线;终态落库后回填一律由扫描驱动(`BackfillService.sweep` 每 30 秒、指数退避 30 秒起步封顶 15 分钟,处理关键路径不做跨库写),接收时间超过超期期限 `R` 时强制补写(§5.2)。死信同样可补写——回填只需消息 ID,不依赖 META。剩余:Q7 的标记值集与写权限书面确认;影子环境禁写尚未实装。
|
||||
|
||||
**前置**:US-03 终态接口;Q7、共享库更新权限。覆盖四类终态、事务回滚、重复补偿和重放竞争;生命周期、超期补写与清除口径以 [message-lifecycle.md](message-lifecycle.md) §4/§5.2/§6 为准。
|
||||
**前置**:US-03 终态接口;Q7、共享库更新权限。覆盖四类终态、事务回滚、重复补偿和重放竞争;生命周期与超期补写以 [design.md](design.md)「中断恢复」「回填」为准,清除口径以 [contracts.md](contracts.md)「保留与清除」为准。
|
||||
|
||||
### US-10 安全重放与故障处置
|
||||
|
||||
@@ -219,11 +219,11 @@
|
||||
1. 默认归档接收时间早于 1 天的 SUCCEEDED/SKIPPED/DEAD,保留期可配置 1~7 天;PENDING/FAILED 禁止归档。明确接收时间字段来源,不混用 UPDATED_AT 或本地入队时间。
|
||||
2. 归档到自有 PG `PROC_STATE_HST`;关联 `MSG_EVENT` 的历史目标和保留规则一并设计。仍有未完成投递、回填或恢复依赖时,不移除所需记录。
|
||||
3. 迁移与删除在自有库事务内完成,重复执行幂等;失败保留源记录并报告计数。归档后同信箱 ID/业务身份再次到达,仍能按约定去重。
|
||||
4. 本系统不写共享 MySQL `CMINMSGS_HST`、不清理外部信箱;由库方按 Q9 执行的清除与历史归档见 message-lifecycle.md §6。原文可用性与重放保留期由 Q7/Q8 关联确认。
|
||||
4. 本系统不写共享 MySQL `CMINMSGS_HST`、不清理外部信箱;由库方按 `Q9` 执行的清除与历史归档见 [contracts.md](contracts.md)「保留与清除」。原文可用性与重放保留期由 Q7/Q8 关联确认。
|
||||
|
||||
**当前基础与落点**:`PROC_STATE_HST` 未建表,也没有归档处理记录的作业;先确定去重记录保留与关联策略,再补迁移与归档中断测试。航班历史清理(`HistorySweepJob`,属 US-15 红线范围)与本文档处理记录归档不是同一件事,不能混为一谈。
|
||||
|
||||
**前置**:US-03、US-09;US-07 提供事件终态规则,US-10 提供恢复保留要求。不依赖 US-15。自有记录归档与信箱保留期的关系以 [message-lifecycle.md](message-lifecycle.md) §8/§9 为准。
|
||||
**前置**:US-03、US-09;US-07 提供事件终态规则,US-10 提供恢复保留要求。不依赖 US-15。自有记录归档见 [design.md](design.md)「维护作业与归档」,重放与原文可用性见 [design.md](design.md)「失败、重试与重放」。
|
||||
|
||||
### US-12 查询实时航班(KEEP)
|
||||
|
||||
@@ -275,7 +275,7 @@
|
||||
|
||||
历史存储确认成功后,才允许删除对应实时航班;逐条隔离坏数据,不能删除写历史失败的集合。判史规则与保留期(`HistoryProps`)、业务时区 `Asia/Shanghai`、历史写入与删除事件之间的恢复协议需在启用前完成 golden 对拍。
|
||||
|
||||
阶段 A 不依赖 ES,不启用 `PROJECTION_REBUILD`。`HistorySweepJob` 已作为每日清理脚手架接入,但历史存储未接通(或 `msgx.history.history-store-enabled=false`)时删除 0 条;脚手架存在不等于清场已交付,红线见 flight-state.md §6。
|
||||
阶段 A 不依赖 ES,不启用 `PROJECTION_REBUILD`。`HistorySweepJob` 已作为每日清理脚手架接入,但历史存储未接通(或 `msgx.history.history-store-enabled=false`)时删除 0 条;脚手架存在不等于清场已交付,红线见 [flight-state.md](flight-state.md)「生命周期与开放项」。
|
||||
|
||||
## 5. 运行与切流验收
|
||||
|
||||
@@ -302,22 +302,15 @@
|
||||
|
||||
这些是**阻塞相应实现的具体问题**,不是已完成的验收项。保留原有目标值,但不把矛盾或外部未确认内容写成事实。
|
||||
|
||||
| 编号 | 问题与当前口径 | 解除阻塞的产物 |
|
||||
|---|---|---|
|
||||
| Q1 权威存储(方向已定) | 当前 PG 单库权威,主表 + 无损明细;现场供库目标为 Oracle 11g。 | 单库决策已采纳,Oracle 完整适配与部署验收仍待交付;不得退回 Redis 双写。 |
|
||||
| Q2 入队顺序 | 空洞老化阈值取自"最大提交时延",但库方尚未书面承诺 ID 单调与提交时延,因此迟提交不越序仍无端到端保证。取值可参照 SIS:报文在 CIIMS 的 `Expiry` 为 480 分钟,断连 120–480 分钟按 Level 2 处理,CIIMS 在成功接收或过期前保序保存(SIS §2.4.2.3.3、§3.16、§5.2)。 | 库方 ID/提交顺序约束,或明确的发现完整性与暂停/恢复协议;晚提交、空洞、兼容入口与重扫联合测试。不能凭空假定 ID 连续。 |
|
||||
| Q3 HTTP 契约 | 目标 ResponseDto 与现有 text/plain ID 不同;请求媒体类型目标已列出,错误码、状态码、查询格式等仍需对拍。 | 每个保留接口的真实请求/响应样例、错误表和契约测试;10MB 的字节口径、字符集及兼容变更说明一起固定。 |
|
||||
| Q4 Kafka wire | 当前 msg/schd 均按 `FLID` 逐航班发送(schd 由 `flushSchd` 合并为每个 FLID 的最新状态),与 legacy“多 FLID 数组”形态不同;msg 是否需按 `SNDR` 分区尚未接线。 | 下游确认发送粒度、key、去重标识放置、分区内顺序及批次确认策略;未定案前不改动现役消费契约。 |
|
||||
| Q5 请求匹配 | 目标优先 SEQN 回显,但回显是否可靠需确认;DTTM 降级存在跨代误匹配,尤其旧应答到达新请求期间。 | 15 类请求/响应样例、回显字段与时间格式;降级风险是否接受及拒绝条件。默认超时仍为 RQFD 60 秒、RQRD 30 秒。 |
|
||||
| Q6 deadline 与重放 | 入队时间作为稳定锚点会把长期排队消息计入滞留;历史重放保留 CREATED_AT 后可能立即过期。外部参照:SIS 报文 `Expiry` 480 分钟(SIS §3.16),legacy 现役按接收超 1 天归档并删除(legacy 基线 §3.6)。 | 明确首次处理/排队过期策略与“本次恢复尝试”计时方式,保留原始时间审计;测试积压恢复和旧 DEAD 重放,不用刷新 UPDATED_AT 绕过超时。 |
|
||||
| Q7 信箱外部契约 | 原目标为成功→SUCCESS、忽略→SKIPPED、重复→DUPLICATE、死信→DEAD;当前 JDBC 写 PROCESSED。新 STATUS 值尚不能假定库方支持。 | 库方认可的状态值、META/缺失字段、权限、原文保留期、出站去重与处理时间语义;若只允许 legacy 集合,显式映射内部原因,不新增外部枚举。 |
|
||||
| Q8 业务覆盖清单 | “29 FLOP、14 RQRD、21 参考类、2 机位类”只是数量,不能直接当字段规范。 | 将 SIS/XSD、现役 KEEP/FIX 基线整理为逐类矩阵与脱敏样例,列明路由、字段、缺失/清空、通知、数据来源优先级、多桥规则及测试文件。资料不全的类型不标完成。 |
|
||||
| Q9 信箱清除契约 | 清除由谁执行与 DDL 授权、方案 A/B 选型、现场 MySQL 版本与分区 DDL 能力(message-lifecycle.md §6)。 | 库方书面确认清除授权与方案选型。 |
|
||||
| Q10 出站信箱契约 | `COUTMSGS` 消费方与顺序、`COUTMSGS_ACK_DATE_RECV / COUTMSGS_ACK_RESEND_TIMES / COUTMSGS_DATE_SENT / COUTMSGS_ERROR` 等列语义与写入方、出站清理责任与去重契约(message-lifecycle.md §7;SIS §2.4.2.2.2、§2.4.2.2.4、§2.4.2.3)。 | 库方书面确认出站消费、清理与去重契约。 |
|
||||
| Q11 上游序号重置 | `SEQN` 的取值范围与回绕已由 SIS §2.8.1 定义(`Number(6)`、1–999999、由发送方设置),未知的是重置周期;它决定业务身份是否加入日期边界(design.md §2.2,默认关闭)。`SNDR` 取值域也需对拍:SIS 为 AODB/RMS,legacy 实发 OSH5 等。 | 上游确认重置周期与 `SNDR` 值域;身份算法变更须另行评审。 |
|
||||
| Q12 积压跳过授权 | 历史积压批次中“不再处理”的确认主体、审批留痕与跳过值集(message-lifecycle.md §5.3)。 | 业务与库方书面确认该批跳过范围;`PROC_STATE=SKIPPED` 记录原因并保留审计,不允许整段 DELETE。 |
|
||||
| Q13 日计划字段缺失语义 | SIS 要求最新日计划中未发送的可选字段视为 AODB 已无该数据、子系统应删除本地已有值(SIS §3.16 注释 4,RESP 同格式见 SIS §3.17),与现行"未携带字段保留"相反(flight-state.md §3.1、message-lifecycle.md §5.3)。 | 与 AODB/库方书面确认字段级删除语义;确认前不得按任一方向验收对拍差异。 |
|
||||
| Q14 主/共享删除顺序与 EROR | SIS 要求删主航班前先删子共享航班,顺序不符时 RMS 向 AODB 回发 EROR(SIS §1.6.1-1.d,事件定义 SIS §4.8)。现行设计为自动原子级联,未定义回报路径(flight-state.md §3.3)。 | 确认上游删除顺序约定,并选定"回发 EROR"或"幂等自动级联";出站事件类型随之纳入 US-08 范围。 |
|
||||
**Q 台账已迁至 [contracts.md](contracts.md)「待确认事项台账」**(唯一出处,覆盖 Q1–Q16)。本文件只保留与验收直接相关的依赖关系,不重复问题正文:
|
||||
|
||||
- US-01 / US-09 的验收依赖 `Q2`(发现完整性)与 `C-8`(清除前提);
|
||||
- US-06 / US-13 / US-14 依赖 `Q8`(逐类清单)与 `Q13`(字段缺失语义);
|
||||
- US-08 依赖 `Q3`(HTTP 契约)、`Q4`(Kafka wire)、`Q5`(请求匹配)、`Q10`(出站契约);
|
||||
- US-10 依赖重放白名单与 CLM-3(重放安全,见 [invariants.md](invariants.md));
|
||||
- US-15 依赖 `Q9`(清除授权与 DDL)。
|
||||
|
||||
编号、当前口径与解除阻塞产物的完整表述见 contracts.md。
|
||||
|
||||
业务日期/日计划采用 `Asia/Shanghai`;持久化与比较使用明确的时间类型和转换规则,不靠服务器默认时区,也不直接比较不同单位的数字。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user