docs(spec): 收敛领域边界闭合方案为三个缺口
- 本轮只做 G-KAFKA-D3 / G-IGNORE / G-EVENT-RETENTION,其余缺口不再写入 spec - 删除约束与表结构副本、User Stories 重复、phase B 清单与进度记录 - US-04 AC1 明确先绑定身份再转 SKIPPED,引用改用稳定 ID US-03
This commit is contained in:
+68
-338
@@ -1,367 +1,97 @@
|
||||
# Spec: 领域边界闭合(当前迭代)
|
||||
# 精简实施方案
|
||||
|
||||
## Problem Statement
|
||||
## 1. 本轮目标
|
||||
|
||||
msgexchange-v2 的领域模型在文档中定义了完整的边界,但部分边界未在代码中落地。经过审查,REQ_TRACK 全套机制、MAFL 级联投影、归档保留期三连、REPLAY-CHANNEL 属于当前迭代的过度工程,推至 phase B。剩余必要缺口若不闭合,系统面临:
|
||||
关闭以下缺口:
|
||||
|
||||
- Kafka 有序性违反(`G-KAFKA-D3`)
|
||||
- 运维无法区分已知忽略与真实异常(`G-IGNORE`)
|
||||
- outbox 无限增长(`G-EVENT-RETENTION`)
|
||||
- 回填退避不可调(`G-BACKFILL-BACKOFF`)
|
||||
- `PROC_STATE` 无生命周期终点(`G-PROC-HST`)
|
||||
1. `[G-KAFKA-D3]`:生产者配置满足 `D3`。
|
||||
2. `[G-IGNORE]`:`US-04` 定义的忽略报文正常终结。
|
||||
3. `[G-EVENT-RETENTION]`:已确认投递的事件不会无限增长。
|
||||
|
||||
## Solution
|
||||
依赖约束仅引用:`D3`、`INV-7`、`INV-8`、`INV-9`、`INV-10`、`US-03`、`US-04`。
|
||||
|
||||
闭合 5 个必要的领域边界缺口,同步推进 3 个阻断性 Q 项确认,将过度工程化部分正式标记为 phase B deferred。
|
||||
## 2. 明确不做
|
||||
|
||||
**当前迭代交付:**
|
||||
本轮不实现:
|
||||
|
||||
1. `G-KAFKA-D3`:把 `PARAM:kafka.producers.default.max-in-flight-requests-per-connection` 收敛到 `D3` 要求值,满足 architecture D3 有序性约束
|
||||
2. `G-IGNORE`:codec 层识别 LDM/REGN/RSTA/EROR,分派为 `SKIPPED` 终态,走非业务型终态路径
|
||||
3. `G-EVENT-RETENTION`:jobs 模块新增 MSG_EVENT 已发送行清理作业,按 `PARAM:msgx.pipeline.event-retention` 定期清除
|
||||
4. `G-BACKFILL-BACKOFF`:将 BackfillService 硬编码退避提取为 `PARAM:msgx.pipeline.backfill-backoff-ms` / `-cap-ms`
|
||||
5. `G-PROC-HST`:建 `PROC_STATE_HST` 归档表,jobs 模块新增终态行归档作业
|
||||
- `[G-BACKFILL-BACKOFF]`:没有真实调参需求,保留现有退避算法。
|
||||
- `[G-PROC-HST]`、`[G-HST-RETENTION]`:等待 `Q6`、`Q7`、`Q9` 定案后单独设计。
|
||||
- `[G-FLOP-IDEMPOTENT]`:继续保持 `INV-20`、`CLM-3` 不可声明。
|
||||
- `[G-REQ-TRACK]`、`[G-MAFL]`、`[G-REPLAY-CHANNEL]` 等其他缺口。
|
||||
|
||||
**同步推进确认:**
|
||||
具体排期只记录在 Plane,不写入本 spec。
|
||||
|
||||
- Q2(ID 单调递增):G1 移除后水位线正确性的唯一保障
|
||||
- Q7(processing-mark 语义):回填契约基础
|
||||
- Q9(purge 权限与方案):系统对共享 MySQL 的清除权利
|
||||
## 3. [G-KAFKA-D3]
|
||||
|
||||
**收窄:**
|
||||
实现:
|
||||
|
||||
- `G-FLOP-IDEMPOTENT`:从「29 类全矩阵」收窄为「按实际到达类型增量补齐」
|
||||
- 将 Kafka producer 的有效 `PARAM:kafka.producers.default.max-in-flight-requests-per-connection` 固定为 `D3` 要求值。
|
||||
- 启动时检查最终生效配置;不满足 `D3` 时拒绝启动。
|
||||
- 不改变现有投递模型、批次、重试或 topic。
|
||||
|
||||
---
|
||||
验收:
|
||||
|
||||
## 约束锚点(内联展开)
|
||||
- 默认配置可以启动。
|
||||
- 非法环境变量覆盖导致启动失败。
|
||||
- `acks`、幂等和 `max-in-flight` 三项联合满足 `D3`。
|
||||
|
||||
以下约束全部引自 `docs/` 各锚点文件的稳定 ID,在此内联展开以便 spec 自包含。
|
||||
## 4. [G-IGNORE]
|
||||
|
||||
### 前提(PRE-x,引自 invariants.md §1)
|
||||
实现:
|
||||
|
||||
| 编号 | 前提 | 与本 spec 的关系 |
|
||||
|---|---|---|
|
||||
| PRE-2 | ID 单调 + 可见时延上界(见 `C-1`/`C-2`) | G1 移除后水位线正确性依赖此前提 |
|
||||
| PRE-3 | ID 空间不复位、不复用、不回退(见 `C-3`) | 同上 |
|
||||
| PRE-5 | 单活动实例运行(信箱读取不加锁、水位是单行覆盖写) | 作业与主泵共存的部署前提 |
|
||||
| PRE-6 | 信箱与自有 PG 之间没有跨库事务 | 回填、归档都不能声称原子 |
|
||||
| PRE-7 | 报文不可变(见 `C-4`) | 身份绑定的前提;忽略规则在解码后判定 |
|
||||
- 继续使用现有 XML 解码结果和 `MsgKind.Unsupported`,不增加新的领域类型或规则引擎。
|
||||
- 在 `MessageProcessor` 中按以下顺序处理:
|
||||
|
||||
### 不变量(INV-x,引自 invariants.md §2)
|
||||
解码 META
|
||||
→ 绑定 `IDENTITY_KEY`
|
||||
→ 匹配 `US-04` 忽略清单
|
||||
→ 命中则写 `SKIPPED` + 回填意图
|
||||
→ 未命中则进入现有 `MsgKind` 分派
|
||||
|
||||
**管道不变量:**
|
||||
- 忽略终态不取得 `PIPELINE_LOCK`,不写航班表,不创建 `MSG_EVENT`。
|
||||
- 使用 `LAST_ERROR=ignored:<rule>` 保存原因。
|
||||
- 只增加 `US-04` 要求的忽略计数;指标名登记到 `reference.md`。
|
||||
|
||||
- **INV-1** 五个独立事实互不替代:落信 / 入队 / 处理完成 / 已回填 / 投递确认各有独立证据,前一个不蕴含后一个。
|
||||
- **INV-3** 队头唯一:任一时刻只有一个可执行队头(最小未完成 `MSG_ID`,`PENDING` 与 `FAILED` 都占位);`FAILED` 未退避到期时后续消息不得越过。
|
||||
- **INV-7** 处理标记单调:任何路径只把空标记写成已处理值,不回撤、不覆盖。
|
||||
- **INV-8** 回填只针对终态(`PENDING` / `FAILED` 永不写标记);「还欠一次回填」的事实与终态由**同一条语句**落库,不存在第二处落账。
|
||||
- **INV-9** 一信一行、一身份一记录:`PROC_STATE` 按 `MSG_ID` 唯一;同一业务身份至多绑定一条有效处理记录。
|
||||
- **INV-10** 对外投递至少一次;端到端恰好一次不在交付范围。
|
||||
- **INV-18** 航班表的写者集合是「主泵处理器」与「历史清理」;两者必须互斥(同一 `PIPELINE_LOCK`,或清理在同一事务内复查判据后再删除),不得出现清理删除与处理器更新同一 `FLID` 的竞态。
|
||||
验收:
|
||||
|
||||
### 声明边界(CLM-x,引自 invariants.md §3)
|
||||
- `US-04` 中全部规则及大小写场景均命中。
|
||||
- 忽略消息先完成身份绑定(`US-03`)。
|
||||
- 重复身份继续走既有 `duplicate-of` 路径。
|
||||
- `SKIPPED` 与回填意图同语句落库。
|
||||
- 不产生航班或 outbox 副作用。
|
||||
- 未命中的合法类型继续按 `UNSUPPORTED` 处理。
|
||||
|
||||
- **CLM-3** 重放不产生重复业务副作用——依赖 `INV-20`、`G-FLOP-IDEMPOTENT`;当前**不可声明**(29 类 FLOP 幂等矩阵未补全)
|
||||
- **CLM-9** 处理标记延迟由调度周期决定(≤30 秒)——当前**不可声明**(批次积压、单行超时与历史作业都会延长实际延迟)
|
||||
- **CLM-16**(即 contracts.md `C-16`)回填放弃清单在对应信箱边界被清除前必须保持可查
|
||||
## 5. [G-EVENT-RETENTION]
|
||||
|
||||
### 架构决策(D-x,引自 architecture.md §7)
|
||||
实现:
|
||||
|
||||
- **D3** Kafka 生产要求 `acks=all`、`enable.idempotence=true`、`max.in.flight=1`;不允许通过关闭幂等来满足生产接入。当前证据/缺口:参数默认值与 D3 不一致 `[G-KAFKA-D3]`
|
||||
- **D4** 自有库终态记录只归档到 `PROC_STATE_HST`,不侵入共享库的表结构或保留策略。当前证据/缺口:目标表未建 `[G-PROC-HST]`
|
||||
- 下一条 PG Flyway 迁移仅给 `MSG_EVENT` 增加可空 `SENT_AT`。
|
||||
- 所有投递确认操作在设置 `STATE='SENT'` 的同一条 `UPDATE` 中写入 `SENT_AT`。
|
||||
- `KAFKA:schd` 新代次重置为 `PENDING` 时清空 `SENT_AT`。
|
||||
- 迁移时将存量 `SENT` 行的 `SENT_AT` 设置为迁移时刻 `CURRENT_TIMESTAMP`,避免提前清理。
|
||||
- 在现有 `JobRunner` 维护周期中增加一次有界清理,不新增线程或调度参数。
|
||||
- 只删除同时满足以下条件的行:
|
||||
|
||||
### 对外契约(C-x,引自 contracts.md)
|
||||
```
|
||||
STATE = 'SENT'
|
||||
AND SENT_AT IS NOT NULL
|
||||
AND SENT_AT < 当前时间 - PARAM:msgx.pipeline.event-retention
|
||||
```
|
||||
|
||||
**保留与清除:**
|
||||
- 删除仍携带 `STATE='SENT'` 条件,避免与 `KAFKA:schd` 新代次 upsert 竞态。
|
||||
- `PENDING`、`DEAD`、`SENT_AT IS NULL` 一律保留。
|
||||
|
||||
- **C-5** 处理标记值集与写权限:本系统只写入库方认可的 legacy 值集内的「已处理」值(默认 `PROCESSED`),只写空标记、不回撤、不覆盖;内部原因记录在自有 PG,**不在信箱新增枚举**。`[待确认 Q7]`
|
||||
- **C-6** 清除语义必须是「标记 + 保留期」:打标本身不触发清除,触发条件是「到达保留期 `R_keep`」且「边界内全部行已打标」。`[待确认 Q7][待确认 Q9]`
|
||||
- **C-7** 保留期下界:`R_keep ≥ max(人工重放期限 + 人工处置期限, 审计期限, 回填重试上限)`。这是「重放窗口内原文仍在」的**唯一保证来源**。`[待确认 Q6][待确认 Q9]`
|
||||
- **C-8** 清除前置条件:执行清除时,边界内**每行必须已有终局**——即「已持有处理标记」**或**「已登记在本系统的回填放弃清单中且经人工对账确认」。放弃行不写标记,未达终态的行顺延至处理完成后清除。`[待确认 Q7][待确认 Q9]`
|
||||
- **C-16** 回填放弃清单在对应信箱边界被清除前必须保持可查:`C-8` 以本清单作为清除授权证据之一,该证据不得随处理记录的归档或清除而消失。`[我们单方承诺]`
|
||||
验收:
|
||||
|
||||
**我们向库方的承诺:**
|
||||
- 投递确认与 `SENT_AT` 原子写入。
|
||||
- 存量 `SENT` 行迁移后不会立即过期。
|
||||
- 到期 `SENT` 行被分批清理,未到期及其他状态不受影响。
|
||||
- `KAFKA:schd` 重置与清理两种并发顺序均不误删。
|
||||
- 清理可重复执行,不影响投递顺序。
|
||||
|
||||
- **C-14** 不建表、不改表结构、不迁移 schema、不写共享历史表。`[我们单方承诺]`
|
||||
## 6. 测试约束
|
||||
|
||||
### 用户故事验收标准(US-x,引自 user-stories.md)
|
||||
|
||||
**US-04 明确忽略非业务报文:**
|
||||
|
||||
1. 解码 META 后、身份绑定及处理器分派前,大小写不敏感匹配 `TYPE-STYP` 或 `TYPE-*`;基线为 `LDM-* / REGN-* / RSTA-* / EROR-*`,不混用 `ERROR`。
|
||||
2. 命中后转 `SKIPPED`,记录 `ignored:<rule>` 和计数;不更新航班、不创建业务通知。
|
||||
3. 通过 US-09 保存回填意图;命中、未命中、大小写和重扫均有测试。合法忽略报文不应因 `MsgKind` 尚不能表达它而先解码失败。
|
||||
|
||||
**US-11 归档自有库终态记录(已按裁定修正):**
|
||||
|
||||
1. 终态记录在**了结后**经过的时间(`UPDATED_AT`)达到 `PARAM:msgx.proc-state.archive-after` 时列为归档候选;`PENDING`/`FAILED` 禁止归档,回填未了结的终态行不进入候选。
|
||||
2. 归档到自有 PG `PROC_STATE_HST`,主表保留 `STATE='ARCHIVED'` 的去重影子行(仅 `IDENTITY_KEY` 与 `MSG_ID`),使归档后同业务身份再次到达仍可去重;`MSG_EVENT` 的历史目标与保留规则由 `G-EVENT-RETENTION` 独立处理,不构成归档判据。
|
||||
3. 归档写入与主行置 `ARCHIVED` 在同一自有库事务内完成,按候选时的状态条件复查,影响 0 行即整体回滚;重复执行幂等,失败保留源记录并报告计数。
|
||||
4. 本系统不写共享 MySQL `CMINMSGS_HST`、不清理外部信箱;由库方按 `Q9` 执行的清除与历史归档见 contracts.md「保留与清除」。
|
||||
|
||||
### 参数(PARAM:x,引自 reference.md)
|
||||
|
||||
| PARAM 键 | 当前默认 | 依据 | 与本 spec 的关系 |
|
||||
|---|---|---|---|
|
||||
| `kafka.producers.default.acks` | `all` | 契约(D3) | G-KAFKA-D3 不动此键 |
|
||||
| `kafka.producers.default.enable-idempotence` | `true` | 契约(D3) | G-KAFKA-D3 不动此键 |
|
||||
| `kafka.producers.default.max-in-flight-requests-per-connection` | `5` | **与 D3 不一致** | G-KAFKA-D3 收敛到 `1` |
|
||||
| `msgx.pipeline.event-retention` | 目标参数(**未实现**) | 假定 | G-EVENT-RETENTION 登记默认值(建议 7 天) |
|
||||
| `msgx.proc-state.archive-after` | `1d` | 假定,**未实现** | G-PROC-HST 实现后登记实际默认值(建议范围 1~7 天) |
|
||||
| `msgx.pipeline.backfill-backoff-ms` | 目标参数(**未实现**) | 假定 | G-BACKFILL-BACKOFF 提取硬编码 30 秒起步 |
|
||||
| `msgx.pipeline.backfill-backoff-cap-ms` | 目标参数(**未实现**) | 假定 | G-BACKFILL-BACKOFF 提取硬编码 15 分钟封顶 |
|
||||
| `msgx.pipeline.max-attempts` | `5` | 假定 | 处理与投递共用;达到即转 `DEAD(EXHAUSTED)` |
|
||||
| `msgx.pipeline.overdue-backfill`(`R`) | `30d` | 契约(`R ≤ R_keep`) | 进入强补写窗口的阈值 |
|
||||
|
||||
### 错误分类(引自 reference.md §4)
|
||||
|
||||
| 错误类别 | 触发 | 处置 | 可重放 |
|
||||
|---|---|---|---|
|
||||
| `MALFORMED` | 报文非法、原文缺失或缺少该类型必需的业务载荷 | 立即 `DEAD` | 否 |
|
||||
| `PROTOCOL` | 载荷存在但整包违反业务协议(运营日冲突、声明数不符) | 立即 `DEAD`,整包不落地 | 否 |
|
||||
| `CODEC_ERROR` | 解码能力问题 | `FAILED` 退避 | 是 |
|
||||
| `UNSUPPORTED` | 处理器或快照能力未实现 | `FAILED` 退避,受尝试上限约束 | 是 |
|
||||
| `INFRA` | 基础设施或执行异常 | `FAILED` 退避 | 是 |
|
||||
| `EXHAUSTED` | 尝试次数耗尽 | `DEAD`;`ERROR_CLASS` 被覆写为 `EXHAUSTED` | 是 |
|
||||
|
||||
### 表结构基线(引自 Flyway V1–V9 迁移链)
|
||||
|
||||
**PROC_STATE**(自有 PG,处理状态主表):
|
||||
|
||||
| 列 | 类型 | 约束 | 加入迁移 |
|
||||
|---|---|---|---|
|
||||
| `MSG_ID` | BIGINT | PK | V1 |
|
||||
| `STATE` | VARCHAR(16) | NOT NULL | V1 |
|
||||
| `IDENTITY_KEY` | VARCHAR(200) | UNIQUE (`uk_proc_identity`) | V1 |
|
||||
| `ATTEMPTS` | INT | NOT NULL DEFAULT 0 | V1 |
|
||||
| `NEXT_ATTEMPT_AT` | TIMESTAMPTZ(6) | | V1 |
|
||||
| `ERROR_CLASS` | VARCHAR(20) | | V1 |
|
||||
| `LAST_ERROR` | VARCHAR(1000) | | V1 |
|
||||
| `UPDATED_AT` | TIMESTAMPTZ(6) | NOT NULL | V1 |
|
||||
| `RECEIVED_AT` | TIMESTAMPTZ(6) | | V2 |
|
||||
| `BACKFILL_AT` | TIMESTAMPTZ(6) | | V2 |
|
||||
| `BACKFILL_NEXT_AT` | TIMESTAMPTZ(6) | | V2 |
|
||||
| `BACKFILL_ATTEMPTS` | INT | NOT NULL DEFAULT 0 | V2 |
|
||||
| `BACKFILL_ERROR` | VARCHAR(512) | | V2 |
|
||||
| `BACKFILL_ABANDONED_AT` | TIMESTAMPTZ(6) | | V4 |
|
||||
| `BACKFILL_ABANDONED_REASON` | VARCHAR(64) | | V4 |
|
||||
| `ENQUEUED_AT` | TIMESTAMPTZ(6) | NOT NULL DEFAULT now() | V6 |
|
||||
|
||||
> V7 删除了 `PROCESSING_STARTED_AT`。当前终态值集:`PENDING / FAILED / SUCCEEDED / SKIPPED / DEAD`;G-PROC-HST 新增 `ARCHIVED`。
|
||||
|
||||
**MSG_EVENT**(自有 PG,outbox):
|
||||
|
||||
| 列 | 类型 | 约束 | 加入迁移 |
|
||||
|---|---|---|---|
|
||||
| `EVENT_ID` | BIGSERIAL | PK | V1 |
|
||||
| `TARGET` | VARCHAR(30) | NOT NULL | V1 |
|
||||
| `PARTITION_KEY` | VARCHAR(32) | NOT NULL | V1 |
|
||||
| `EVENT_TYPE` | VARCHAR(16) | NOT NULL DEFAULT 'UPSERT' | V1 |
|
||||
| `STATE_VERSION` | BIGINT | NOT NULL | V1 |
|
||||
| `PAYLOAD_JSON` | TEXT | NOT NULL | V1 |
|
||||
| `STATE` | VARCHAR(16) | NOT NULL | V1 |
|
||||
| `ATTEMPTS` | INT | NOT NULL DEFAULT 0 | V1 |
|
||||
| `NEXT_ATTEMPT_AT` | TIMESTAMPTZ(6) | | V1 |
|
||||
| `ERROR_CLASS` | VARCHAR(20) | | V1 |
|
||||
| `LAST_ERROR` | VARCHAR(1000) | | V1 |
|
||||
| `CREATED_AT` | TIMESTAMPTZ(6) | NOT NULL | V1 |
|
||||
|
||||
> **无 `UPDATED_AT`、无 `SENT_AT`**。G-EVENT-RETENTION 需 V10 迁移新增 `SENT_AT`。
|
||||
|
||||
**PROC_STATE_HST**:尚未建表 `[G-PROC-HST]`。
|
||||
|
||||
### 时间常数排序(引自 design.md「生命周期与清除」)
|
||||
|
||||
1. `R ≤ R_keep`(`C-7`)
|
||||
2. 去重记忆期 ≥ `R_keep`(`INV-9`)——否则「归档后重复」不成立
|
||||
3. 回填放弃清单可见期 ≥ `R_keep`(`C-16`)——否则库方清除缺 `C-8` 依据
|
||||
4. 归档阈值计的是**终局之后**的时间,不是入队之后:终态行未了结回填时不进入候选
|
||||
|
||||
### 待确认事项(Q-x,引自 contracts.md §C,仅列本 spec 涉及项)
|
||||
|
||||
| 编号 | 事项 | 当前假定 | 阻塞 |
|
||||
|---|---|---|---|
|
||||
| Q2 | 信箱 ID 单调、ID 分配→事务可见时延上界、ID 空间不复位 | 时延按 5 分钟 `max-commit-delay`(**缺少依据的占位值**) | 发现完整性声明、空洞老化阈值 |
|
||||
| Q6 | 重放期限与人工处置期限的取值 | `R` = 30 天;重放/处置期限未定 | `R_keep` 取值 |
|
||||
| Q7 | 处理标记值集与写权限、原文保留期 | 写入 `PROCESSED` | 回填值集、保留期下界 |
|
||||
| Q8 | 逐类覆盖清单(积压摸底的类型分布依据) | — | 积压处置与逐类矩阵(**暂缓**) |
|
||||
| Q9 | 清除执行方与 DDL 授权、方案 A/B 选型 | 首选方案 A | `R_keep` 与清除边界 |
|
||||
| Q11 | 上游 `SEQN` 重置周期与业务身份的日期边界 | 不含日期边界 | 身份算法 |
|
||||
| Q13 | 日计划缺失可选字段的删除语义 | 保留未携带字段(**暂缓**) | 快照合并 |
|
||||
|
||||
---
|
||||
|
||||
## User Stories
|
||||
|
||||
1. As a **系统运维**, I want Kafka producer `max-in-flight=1`, so that 同一分区内事件投递顺序与写入顺序一致,满足 architecture `D3`
|
||||
2. As a **系统运维**, I want LDM/REGN/RSTA/EROR 消息被显式标记为 SKIPPED, so that 监控告警能区分「已知忽略类型」和「真正的异常消息」
|
||||
3. As a **系统运维**, I want MSG_EVENT 已投递行按保留期自动清理, so that outbox 表不会无限增长导致查询退化
|
||||
4. As a **系统运维**, I want 回填退避参数可通过配置调整, so that 不同负载下无需改代码即可优化回填节奏
|
||||
5. As a **系统运维**, I want PROC_STATE 终态行归档到 PROC_STATE_HST 并在主表留下去重影子行, so that PROC_STATE 表有生命周期终点且不破坏业务去重(`INV-9`)
|
||||
6. As a **领域模型维护者**, I want FLOP 幂等规则按实际到达类型增量补齐, so that 不为未到达的类型做无用设计
|
||||
|
||||
---
|
||||
|
||||
## Implementation Decisions
|
||||
|
||||
### G-KAFKA-D3
|
||||
|
||||
- 把 `PARAM:kafka.producers.default.max-in-flight-requests-per-connection` 从当前值 `5` 收敛到 `D3` 要求的 `1`
|
||||
- 影响 `KAFKA:msg` 和 `KAFKA:schd` 两个投递目标
|
||||
- 吞吐影响可接受:单分区有序是 `INV-10` 的隐含前提,`D3` 明确要求该键取有序值
|
||||
- 配置落点:`application.yml` 的 `kafka.producers.default.max-in-flight-requests-per-connection`(现由 `MSGX_KAFKA_MAX_IN_FLIGHT` 覆盖);`.env.example` 注释中的简写改为全键名
|
||||
- `README.md` 中 `max.in.flight=1` 简写统一为全键;保留「严禁非幂等降级」的契约口径
|
||||
- 验收:以配置断言(或启动自检)钉住生效值满足 `D3`,并在 `G-KAFKA-D3` 缺口索引中更新验证证据映射
|
||||
|
||||
### G-IGNORE
|
||||
|
||||
- codec 层按 `TYPE`+`STYP` 组合识别 `LDM`/`REGN`/`RSTA`/`EROR`(大小写不敏感,基线见 `US-04`)
|
||||
- **不新增 `MsgKind` 变体**:`MsgKind` 的契约是 `Schd(RESP/DNLD/ADFT)`/`Flop`/`Fdel`/`Unsupported`;忽略是解码后路由边界的策略,与 `MsgKind` 正交
|
||||
- 判定位置:解码成功之后、身份绑定之前、`MsgKind` 分派之前(`US-04`);忽略报文不写 `IDENTITY_KEY`
|
||||
- 分派路径:不走航班决策逻辑,直接走非业务型终态(`INV-8`)
|
||||
- 单语句写 `PROC_STATE(SKIPPED)` + 回填意图同语句落库
|
||||
- 不取 `PIPELINE_LOCK`、不触航班表/事件
|
||||
- 与 `UNSUPPORTED` 区分:忽略类不进入重试、不占队头退避
|
||||
- 命中优先于 `MsgKind.Unsupported` 分支:不得再落到 `FAILED(UNSUPPORTED)` 的退避路径
|
||||
- 规则未实现前的回退行为:忽略类报文按 `UNSUPPORTED` 可重试处理(现状),**不得按 `MALFORMED` 判死**
|
||||
- `design.md` 明确:「类型未覆盖不等于报文非法:忽略类报文在规则实现前不按 `MALFORMED` 处理」
|
||||
- 计数:`US-04` 要求计数;若新增指标(建议 `msgx.pipeline.codec.ignored.total`),须同 PR 登记 `reference.md` 指标表,不新增未登记指标
|
||||
|
||||
### G-EVENT-RETENTION
|
||||
|
||||
- `MSG_EVENT` 新增 `SENT_AT` 列(可空,`NULL` = 未投递):V10 Flyway 迁移,只增不改;**写入必须在条件确认 `SENT` 的同一 UPDATE 内完成**(原子性保证,不存在先写状态再补时间的两步路径);`KAFKA:schd` 代次重置为 `PENDING` 时清空;已有行 `SENT_AT` 为 `NULL`,视为未投递,不会被清理谓词命中
|
||||
- jobs 模块新增清理作业,扫描 `MSG_EVENT` 中 `STATE='SENT'`、`SENT_AT IS NOT NULL` 且 `SENT_AT < now() − PARAM:msgx.pipeline.event-retention` 的行,批量删除(`ORDER BY EVENT_ID ASC` + 批次上限,幂等可重跑)
|
||||
- 删除必须带 `STATE='SENT'` 条件:`KAFKA:schd` 同 `FLID` 出现新代次时该行被重置为 `PENDING`,两种执行顺序下都不会被误删
|
||||
- 清理先删 → upsert 新建行(新代次 `PENDING`,正确)
|
||||
- upsert 先重置为 `PENDING` → 同一 DELETE 因谓词不匹配而跳过(正确)
|
||||
- `KAFKA:msg` 的 `DEAD` 行保留作 DLQ,人工处置后再清理
|
||||
- `KAFKA:schd` 的 `DEAD` 行在同 FLID 出现新的可接受状态代次时被重置
|
||||
- 作业不触航班表,**不需要 `PIPELINE_LOCK`**:`INV-18` 约束的是航班表写者互斥(「主泵处理器」与「历史清理」),`MSG_EVENT` 不是航班表
|
||||
- 默认保留期在 reference.md 登记(建议起点 7 天,需生产数据校准)
|
||||
|
||||
### G-BACKFILL-BACKOFF
|
||||
|
||||
- 从 BackfillService 提取两个配置参数(reference.md 参数注册表已登记为目标参数):
|
||||
- `PARAM:msgx.pipeline.backfill-backoff-ms`(当前硬编码 30 秒起步)
|
||||
- `PARAM:msgx.pipeline.backfill-backoff-cap-ms`(当前硬编码 15 分钟封顶)
|
||||
- 不改变现有退避逻辑,仅将代码常量替换为配置注入
|
||||
- 实现后更新 reference.md 状态从「目标参数(未实现)」为实际默认值
|
||||
|
||||
### G-PROC-HST
|
||||
|
||||
- 新建 migration:`PROC_STATE_HST` 表,结构与 `PROC_STATE` 相同,增加 `ARCHIVED_AT` 时间戳
|
||||
- 归档候选条件:
|
||||
- 终态(`STATE IN (SUCCEEDED, SKIPPED, DEAD)`)
|
||||
- 回填已了结:`BACKFILL_AT` 非空,或已放弃且经人工对账(放弃行不写标记,是 `C-8` 清除授权证据,未对账前不得归档)
|
||||
- 终局后经过 `PARAM:msgx.proc-state.archive-after` 阈值;基准是 `UPDATED_AT`(终态与了结都推进它,了结后不再更新),**不是** `ENQUEUED_AT`
|
||||
- 归档操作**不取 `PIPELINE_LOCK`**:人工重放走 `MessageLifecycleGate`、不取该锁,锁是假保护;批量归档持锁会阻塞主泵 FIFO。互斥由条件写入 + `MessageLifecycleGate` 提供:同一事务内先 `INSERT INTO PROC_STATE_HST … SELECT`,再按候选时的 `STATE` 条件写主表;影响 0 行即整体回滚、该行跳过(`DEAD` 已被重放改回 `PENDING` 时谓词不匹配)
|
||||
- 主表**不删除**归档行,而是置 `STATE='ARCHIVED'`(新增状态值)并清空尝试/错误/回填/时间列,仅保留 `MSG_ID`、`IDENTITY_KEY`、`ARCHIVED_AT`
|
||||
- **去重记忆约束**:归档后 `IDENTITY_KEY` 的业务去重能力必须保留(`INV-9`),去重记忆期 ≥ `R_keep`(时间常数排序)。实现取「主表保留去重影子行」:影子行即 `STATE='ARCHIVED'` 行,`IDENTITY_KEY` 唯一约束留在主表不动;**所有非归档谓词必须显式排除 `ARCHIVED`**——队头推进(`INV-3` 只计 `PENDING`/`FAILED`)、`backlog()` 聚合、回填扫描均不靠状态包含列表隐式过滤,而是显式 `STATE <> 'ARCHIVED'` 或等价的排除条件
|
||||
|
||||
### G-FLOP-IDEMPOTENT 收窄
|
||||
|
||||
- 分析生产实际到达的 FLOP 类型分布
|
||||
- 仅对实际到达类型补齐幂等规则
|
||||
- 文档记录:「未到达类型在首次到达时补齐」
|
||||
- `INV-20` / `CLM-3` 在矩阵完整前仍标记为不可声明
|
||||
|
||||
---
|
||||
|
||||
## Testing Decisions
|
||||
|
||||
- **G-KAFKA-D3**:配置级变更,通过 application-test.yml 断言 producer 生效值满足 `D3`
|
||||
- **G-IGNORE**:
|
||||
- codec 单元测试:给定 LDM/REGN/RSTA/EROR XML,断言分派为 SKIPPED 路径(不走 FlightStateEngine)
|
||||
- processing 集成测试:给定忽略类型消息,断言 PROC_STATE 写入 SKIPPED、不触航班表/事件、回填意图同语句落库
|
||||
- 断言 `INV-8`:非业务型终态不触碰航班表 / `MSG_EVENT`,只写 `PROC_STATE`,且终态与回填意图同语句生效
|
||||
- 断言 `INV-7`:回填四分支对 `ignored:<rule>` 行成立(尤其 `MISSING_ROW` 立即放弃并告警)
|
||||
- 断言 `INV-3`:忽略报文到达终态后不占队头、不阻塞后续消息
|
||||
- 不回退既有 `duplicate-of` 行为
|
||||
- **G-EVENT-RETENTION**:
|
||||
- 使用 TestClock 推进时间,断言超过保留期的 `SENT_AT` 行被清除
|
||||
- 断言未超期的 SENT 行、`SENT_AT IS NULL` 的行保留
|
||||
- 断言非 SENT 状态行(PENDING/DEAD)不受影响
|
||||
- 断言 `KAFKA:msg` DEAD 行不被清理(DLQ 保留)
|
||||
- 断言 `KAFKA:schd` 新代次重置出的 `PENDING` 行在同一轮清理中不被删(含两种执行顺序)
|
||||
- 断言清理幂等可重跑;批次执行不改变 `KAFKA:msg` 的投递顺序与队头(`INV-3` / `INV-10`)
|
||||
- **G-BACKFILL-BACKOFF**:断言配置参数注入生效,退避行为与硬编码时一致
|
||||
- **G-PROC-HST**:
|
||||
- 使用 TestClock + 内存适配器
|
||||
- 断言归档候选条件:终态 + 回填了结 + `UPDATED_AT` 阈值过期;未了结与未到阈值均不进入候选
|
||||
- 断言放弃行未对账前不被归档
|
||||
- 断言条件写入复查:DEAD 行被重放为 `PENDING` 后,归档事务整体回滚、该行跳过
|
||||
- 断言归档原子性:HST 有对应行,且主表该行置 `ARCHIVED`
|
||||
- 断言 `DELETE` 影响 0 行时整体回滚,源记录保留并报告计数
|
||||
- 断言去重记忆:归档后同身份新消息仍被识别为 `duplicate-of:<msgId>`
|
||||
- 断言 `ARCHIVED` 行不占队头(`INV-3`)、不参与 `backlog()` 聚合、不被回填扫描选中
|
||||
- 迁移与删除用 `PgTestSupport` / `FlywayMigrationTest` 本地验证
|
||||
- 所有测试使用 `TestClocks.kt` 与内存适配器,禁止 Thread.sleep 和真实外部基础设施
|
||||
- 涉及排序/重试/幂等/投递必须补 `invariants.md` 验证映射回归
|
||||
|
||||
---
|
||||
|
||||
## Out of Scope
|
||||
|
||||
以下内容经 grill 审查后判定为当前迭代过度工程化,推至 phase B:
|
||||
|
||||
| 项 | 缺口 ID | 理由 |
|
||||
|---|---|---|
|
||||
| REQ_TRACK 运行时协调器 | `G-REQ-TRACK` | 当前迭代无 outbound request 用户故事(US-08 未进入迭代) |
|
||||
| RESP 应答守卫 | `G-RESP-GUARD` | 依赖 REQ_TRACK,同上 |
|
||||
| REQ_TRACK 开放态唯一索引运行时 | `G-REQ-OPEN-UNIQUE` | 索引已存在(V8 migration),运行时依赖 REQ_TRACK 协调器 |
|
||||
| MAFL 派生投影实现 | `G-MAFL` | 无下游消费者读取主航班投影;`INV-21`/`INV-22` 保留为设计约束 |
|
||||
| 原始报文留存通道 | `G-REPLAY-CHANNEL` | replay 能力本身未建,mark-means-purge 语义未确认(`Q9`) |
|
||||
| 归档保留期 | `G-HST-RETENTION` | PROC_STATE_HST 尚未建成,保留期在建成时一并定义 |
|
||||
| 航班历史存储保留期 | `G-FLIGHT-HIST-RETENTION` | 历史存储未接入 |
|
||||
| REQ_TRACK 关闭态保留期 | `G-REQ-TRACK-RETENTION` | REQ_TRACK 关闭行不存在 |
|
||||
| FLOP 29 类全矩阵 | `G-FLOP-IDEMPOTENT` | 收窄为按到达类型增量补齐 |
|
||||
| Compat HTTP 完整 ResponseDto | `G-COMPAT-HTTP` | 等 Q3 确认 |
|
||||
| 非阻断 Q 项 | Q4, Q6, Q8, Q10, Q12, Q13, Q14, Q15, Q16 | 不阻塞当前核心路径;Q8/Q13 已标暂缓 |
|
||||
|
||||
---
|
||||
|
||||
## Further Notes
|
||||
|
||||
### 脆弱边界风险
|
||||
|
||||
G1 移除后(commit `b873855`),水位线正确性完全依赖 `PRE-2`/`PRE-3`(库方承诺 ID 单调递增)。Q2 未确认 = 系统在此假设上裸奔。建议将 Q2 确认作为当前迭代的阻断性前置条件。
|
||||
|
||||
### 时间常数序
|
||||
|
||||
design.md「生命周期与清除」的时间常数排序定义的 `R ≤ R_keep ≤ 去重记忆 ≥ 放弃清单可见期` 约束在实现 G-EVENT-RETENTION 和 G-PROC-HST 时必须遵守:
|
||||
|
||||
1. `R ≤ R_keep`(`C-7`)
|
||||
2. 去重记忆期 ≥ `R_keep`(`INV-9`)
|
||||
3. 回填放弃清单可见期 ≥ `R_keep`(`C-16`)
|
||||
4. 归档阈值计的是终局之后的时间,不是入队之后
|
||||
|
||||
### C-16 回填放弃清单
|
||||
|
||||
`C-16` 要求放弃清单在信箱清除前保持可查。G-PROC-HST 归档作业必须确保:已放弃行的记录不会因归档而消失——放弃行未对账前不得归档(已在 Testing Decisions 中断言)。
|
||||
|
||||
### 通用约束
|
||||
|
||||
- 依据:`docs/` 为唯一设计依据;`docs/legacy/` 仅用于 `Q` 项对拍。
|
||||
- 参数:新增或改名 `msgx.*` / 基础设施键,同 PR 登记 `reference.md`;本文只引 `PARAM:` 键。
|
||||
- 存储:共享 MySQL 零 Schema 变更(`C-14`);Flyway 只动自有 PG,且只增不改(改列一律新迁移)。
|
||||
- 测试:用 `TestClocks.kt` 与内存适配器;涉及排序/重试/幂等/投递必须补 `invariants.md` 验证映射回归;禁 `Thread.sleep`、禁真实外部基础设施。真实 PG 用例走 `PgTestSupport` / `FlywayMigrationTest`。
|
||||
- 交付:Conventional Commits + 小写 Plane scope,一单只关一个 work item。
|
||||
|
||||
### 文档修正(实施前完成)
|
||||
|
||||
以下文档段落与已裁定决策不一致,须在对应实施 PR 合入前修正:
|
||||
|
||||
- **design.md**「处理终态归档」段落:把「必须在 `PIPELINE_LOCK` 内复查 `STATE` 未变再删」改为条件 DELETE 语义——同一事务内 `INSERT…SELECT` + 按候选时 `STATE` 条件 `DELETE`,0 行即回滚;互斥由条件写入提供,不取锁
|
||||
- **user-stories.md `US-11`**:
|
||||
- AC1:阈值基准从 `ENQUEUED_AT` 改为 `UPDATED_AT`(与 `design.md` 时间常数排序一致,`UPDATED_AT` 是「终局后且已了结」的保守时刻)
|
||||
- AC2:删除「未完成投递」判据(`INV-1` 明确投递只依赖 `MSG_EVENT`,`PROC_STATE` 不承担投递证据;`MSG_EVENT` 清理归 `G-EVENT-RETENTION` 独立管)
|
||||
|
||||
### 文档同步(实施后完成)
|
||||
|
||||
实现完成后需更新:
|
||||
|
||||
- reference.md 参数注册表:`PARAM:msgx.pipeline.backfill-backoff-ms` / `-cap-ms` / `PARAM:msgx.pipeline.event-retention` 从「目标参数(未实现)」改为实际默认值
|
||||
- reference.md 参数注册表:`PARAM:kafka.producers.default.max-in-flight-requests-per-connection` 的「依据」列从「与 D3 不一致」改为契约一致
|
||||
- reference.md 参数注册表:`PARAM:msgx.proc-state.archive-after` 从「未实现」改为实际默认值
|
||||
- invariants.md 缺口索引:闭合的 `G-x` 从表中移除
|
||||
- 时间测试统一使用 `TestClocks.kt`。
|
||||
- 仓储行为使用内存适配器;迁移结构使用既有 Flyway 测试设施。
|
||||
- 禁止 `Thread.sleep` 和真实外部基础设施。
|
||||
- 只补上述三个缺口对应的回归,不扩展其他功能矩阵。
|
||||
|
||||
@@ -95,7 +95,7 @@
|
||||
|
||||
**验收标准**
|
||||
|
||||
1. 解码 META 后、身份绑定及处理器分派前,大小写不敏感匹配 `TYPE-STYP` 或 `TYPE-*`;基线为 `LDM-* / REGN-* / RSTA-* / EROR-*`,不混用 `ERROR`。
|
||||
1. 解码 META 后、处理器分派前,大小写不敏感匹配 `TYPE-STYP` 或 `TYPE-*`;基线为 `LDM-* / REGN-* / RSTA-* / EROR-*`,不混用 `ERROR`。转 `SKIPPED` 前必须已完成身份绑定(`US-03`、`INV-9`),忽略报文照常绑定身份。
|
||||
2. 命中后转 `SKIPPED`,记录 `ignored:<rule>` 和计数;不更新航班、不创建业务通知。
|
||||
3. 通过 US-09 保存回填意图;命中、未命中、大小写和重扫均有测试。合法忽略报文不应因 `MsgKind` 尚不能表达它而先解码失败。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user