From d37a9eb38e92561c5aa5da454da733f01922af34 Mon Sep 17 00:00:00 2001 From: windyboy Date: Sun, 13 Sep 2026 13:49:06 +0800 Subject: [PATCH] =?UTF-8?q?docs(spec):=20=E6=94=B6=E6=95=9B=E9=A2=86?= =?UTF-8?q?=E5=9F=9F=E8=BE=B9=E7=95=8C=E9=97=AD=E5=90=88=E6=96=B9=E6=A1=88?= =?UTF-8?q?=E4=B8=BA=E4=B8=89=E4=B8=AA=E7=BC=BA=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 本轮只做 G-KAFKA-D3 / G-IGNORE / G-EVENT-RETENTION,其余缺口不再写入 spec - 删除约束与表结构副本、User Stories 重复、phase B 清单与进度记录 - US-04 AC1 明确先绑定身份再转 SKIPPED,引用改用稳定 ID US-03 --- docs/spec-boundary-closure.md | 406 ++++++---------------------------- docs/user-stories.md | 2 +- 2 files changed, 69 insertions(+), 339 deletions(-) diff --git a/docs/spec-boundary-closure.md b/docs/spec-boundary-closure.md index 239d283..e3625f5 100644 --- a/docs/spec-boundary-closure.md +++ b/docs/spec-boundary-closure.md @@ -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:` 保存原因。 +- 只增加 `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:` 和计数;不更新航班、不创建业务通知。 -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:` 行成立(尤其 `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:` - - 断言 `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` 和真实外部基础设施。 +- 只补上述三个缺口对应的回归,不扩展其他功能矩阵。 diff --git a/docs/user-stories.md b/docs/user-stories.md index beedb74..b466039 100644 --- a/docs/user-stories.md +++ b/docs/user-stories.md @@ -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:` 和计数;不更新航班、不创建业务通知。 3. 通过 US-09 保存回填意图;命中、未命中、大小写和重扫均有测试。合法忽略报文不应因 `MsgKind` 尚不能表达它而先解码失败。