diff --git a/docs/README.md b/docs/README.md index 9b3b9b0..f9b95cb 100644 --- a/docs/README.md +++ b/docs/README.md @@ -29,7 +29,7 @@ |---|---|---| | 收报扫描谓词与幂等登记 | implementation.md「收报」 | specification.md 写对库方的承诺 `C-30`;reference.md 写参数 | | 保留期下界 `R_keep`、清除前置条件 | specification.md「约定」 | implementation.md 只写行为约束;执行步骤在上线前另立 | -| 处理标记(处理时间)写权限 | specification.md `C-5`、`C-15`(只写完成时刻、只填空值) | implementation.md 只写行为约束(`INV-7`) | +| 处理标记(处理时间)写权限 | specification.md 术语「处理标记」;`US-10` | implementation.md「回填」(只填空值、不覆盖) | | 回填四结果、放弃语义、`R` 的作用 | implementation.md「回填」 | specification.md 记结论与可声明性 | | 退避 / `claim-batch` / 回填期限等取值 | reference.md「参数」 | 其余文档只引 `PARAM:` | | 消费权排他、ID 不复位、报文不可变、时钟、单实例 | specification.md「前提」 | 其他文档只引 `PRE-x` | @@ -48,10 +48,10 @@ - `C-x`、`INV-x` 用加粗定义行(`- **C-5** …`); - `CLM-n`、`OPS-n`、`Dn`、`PRE-n`、`Qn`、`G-NAME` 与 `PARAM:` 用注册表首列;首列必须是**单个裸 ID** (`` `ID` `` 或 `ID`)。成组登记(`` `a` / `b` ``)、带括注的首列与写成 `` `ID` `` 的引用行都不算定义。 - - 同一行登记多个 ID(如验证映射的 `INV-20b / CLM-3`)是引用行,不构成定义。 + - 同一行登记多个 ID(如验证映射的 `INV-17b`、`INV-23`)是引用行,不构成定义。 其他位置一律是引用。 - 编号稳定:条款被取代时标 `[作废 by C-y]` 并保留原文;不静默改写,不重编号。 -- **引用只用稳定 ID**,不用章节号:写 `INV-3`、`C-8`、`PARAM:msgx.pipeline.claim-batch`,或「见 implementation.md『收报』」这类文件名 + 小节名指针。章节号随增删章节腐烂,指针失效后必然被改写为复述。 +- **引用只用稳定 ID**,不用章节号:写 `US-03` AC1、`C-8`、`PARAM:msgx.pipeline.claim-batch`,或「见 implementation.md『收报』」这类文件名 + 小节名指针。章节号随增删章节腐烂,指针失效后必然被改写为复述。 - 指针之后**不再复述**被指内容。若两处需要同一段话,说明它放错了位置。 - 外部 SIS 证据统一写 `SIS:
`(如 `SIS:3.16-note-4`),解析到 [legacy/SIS_AODB_RMS-V0.1.md](legacy/SIS_AODB_RMS-V0.1.md) 的章节;它不属于本项目规范 ID,不参与唯一定义检查。 - `G-NAME` 是活跃偏差 ID:定义只在 specification.md「当前已知偏差」注册表,其他位置只写标记;偏差闭合时在同一变更中删除定义与全仓引用,历史与关闭证据只留 Plane。 @@ -75,5 +75,5 @@ ## 6. 不建的文件 - 顶层不再增加 Markdown:`docs/` 顶层固定为上述 6 个文件加允许的专题目录。 -- 不建运行规程文件:设计阶段没有可执行的运行环境,操作步骤在上线/切流前另立(`docs/runbooks/*.md`);设计阶段需要的只有前置条件与红线,它们分别在 specification.md(`C-7`–`C-12`)与 specification.md 的 `CLM-x`。 +- 不建运行规程文件:设计阶段没有可执行的运行环境,操作步骤在上线/切流前另立(`docs/runbooks/*.md`);设计阶段需要的只有前置条件与红线,它们分别在 specification.md(`C-1`、`C-12`)与 specification.md 的 `CLM-x`。 - 声明边界与 Plane 分离:未交付、未确认、不可声明的主张记在 specification.md,逐项处置在 Plane(ACM2),不在正文逐段标注。 diff --git a/docs/architecture.md b/docs/architecture.md index 26fba69..b052e04 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -11,7 +11,7 @@ msgexchange-v2 是 OMMS H5 查询系统的消息网关,替换旧版 `msgexchan - **主要入口**:轮询共享 MySQL 入站表 `CMINMSGS`,只取处理时间为空的行,按编号升序、每批有上限(`US-01`;`INV-2b`)。 - **兼容入口**:`POST /cminmsgs/send` 供联调工具把报文写进信箱,与上游投递走同一条处理路径;返回的编号只表示已进信箱,不代表已处理或下游已收到(`US-02`)。 - **查询入口**:`GET /all/flights` 返回当前全部动态航班(不含共享航班),读 Redis,与网页客户端同源(`US-12`;`INV-24`)。 -- **出站**:只向 AODB 发参考数据请求 `RQRD` 和日计划请求 `RQFD`,经共享 MySQL 出站表 `COUTMSGS`,由 CIIMS adapter 消费;只保证请求写入信箱,不保证 AODB 收到(`US-09`;`C-24`)。 +- **出站**:只向 AODB 发参考数据请求 `RQRD` 和日计划请求 `RQFD`,经共享 MySQL 出站表 `COUTMSGS`,由 CIIMS adapter 消费;只保证请求写入信箱,不保证 AODB 收到(`US-09`;`C-4`)。 - **输出**:Kafka 主题 `msg` 发单条航班变更、`schd` 定时发批量最新状态;Redis 存航班投影;静态参考数据表供 admin-api 只读(`US-08`、`US-13`)。 - **权威**:航班当前态的权威是自有 PostgreSQL(`FLIGHT_SCHD`、资源明细表、`FLIGHT_ROUTE_POINT`);信箱、Redis、Kafka、展示视图都不是(`INV-11b`)。 - **航班历史**:已结束航班先写入 Elasticsearch 历史库,成功后才从实时数据删除(`US-14`;`D1`)。 @@ -41,7 +41,7 @@ delivery 投递 jobs 作业:回填 / 出站重试 / 历史清理 / 四组线程在同一进程、互不调用,协作只经自有 PG 的持久记录交接;HTTP 接口走事件循环,不占这四组线程。重启后各段从记录接着做,不依赖内存进度(`US-01` AC4、`US-03` AC3、`US-10` AC2)。 -线程之间不加锁,靠幂等写入:收报按信箱编号只登记一次(`US-01` AC2)、回填只写空标记(`C-15`)、记录清理只删已回填且超过保留期的行(`INV-25`);发生竞争时后到的操作复查状态并重试。唯一的例外是处理消息的主循环(主泵)与航班历史清理之间要加锁(`INV-18`)。 +线程之间不加锁,靠幂等写入:收报按信箱编号只登记一次(`US-01` AC2)、回填只写空标记(`C-15`)、记录清理只删已回填且超过保留期的行(`US-11`);发生竞争时后到的操作复查状态并重试。主泵与航班历史清理对实时航班表的互斥是实现手段,用以落实「正在被消息处理的航班跳过」(`US-14` AC4)。 运行边界:同一时刻只允许一个实例处理消息(`OPS-1`);切换与回退以信箱处理标记交接,本系统内部处理和回填进度保存在自有 PG 的 `PROC_STATE`,停旧启新时未处理的消息由旧系统继续(`OPS-4`);积压、处理失败、投递失败、回填失败各有指标与告警(`OPS-2`)。 @@ -55,7 +55,7 @@ delivery 投递 jobs 作业:回填 / 出站重试 / 历史清理 / | `codec` | XML 解码,区分非法报文与可修复的解码失败(`US-03`)。 | | `processing` | FIFO 调度、业务身份绑定与去重、领域决策与落库(`SCHD`/`FLOP`/`FDEL`/`ADFT`/静态参考数据),航班类写 Redis 投影,未落到航班当前态的字段写 `UNMAPPED_FIELD`。 | | `delivery` | 读待发事件投 Kafka:按 `FLID` 保序、`schd` 聚合、失败重试(`US-08`;`C-29`)。 | -| `jobs` | 回填扫描、出站请求重试、航班历史清理,以及 `PROC_STATE`、`MSG_EVENT`、`SCHD_SNAP_LOG`、`REQ_TRACK` 的到期清理;单独线程、不进 FIFO(与主泵的互斥见 `INV-18`)。 | +| `jobs` | 回填扫描、出站请求重试、航班历史清理,以及 `PROC_STATE`、`MSG_EVENT`、`SCHD_SNAP_LOG`、`REQ_TRACK` 的到期清理;单独线程、不进 FIFO(与主泵的协作见 `US-14` AC4)。 | | `domain` / `config` | 领域状态、事件和决策模型,以及运行参数。 | | `infra` | 仓储(JDBC/stub)、外部适配器(共享信箱、Kafka、Redis、航班历史存储、AODB 出站)、重试、健康检查与日志;对其他模块只暴露接口。 | @@ -64,12 +64,12 @@ delivery 投递 jobs 作业:回填 / 出站重试 / 历史清理 / 航班动态消息走满全链,其余类别只换其中几步: 1. **收报**:按「处理时间为空」发现信箱行,登记入队(`INV-2b`)。 -2. **主泵**:按最小未完成 `MSG_ID` 取队头,解码,按业务身份去重(`INV-3`、`INV-9`);非法或不支持的报文无副作用,终态留档(`US-03`)。 +2. **主泵**:按最小未完成 `MSG_ID` 取队头,解码,按业务身份去重(`US-03` AC1;implementation「消息、身份与决策」);非法或不支持的报文无副作用,终态留档(`US-03`)。 3. **事务一**:持 `PIPELINE_LOCK`,领域变更、未映射字段记录与待发事件一起提交。 4. **投影**:写 Redis,写成功才算处理完成(`INV-23`);失败保持未完成、下轮重写投影,业务效果幂等(`US-03` AC3)。 5. **事务二**:处理终态与回填意图一起提交。 6. **回填**:作业把处理标记写回共享信箱(`US-10`);写不上的记录在案并告警。 -7. **投递**:读待发事件发 `KAFKA:msg` / `KAFKA:schd`,至少一次、`FLID` 内保序(`INV-10`、`C-29`);一直失败的记录保留可查并告警(`US-08`)。 +7. **投递**:读待发事件发 `KAFKA:msg` / `KAFKA:schd`,至少一次(`C-9`);一直失败的记录保留可查并告警(`US-08`)。 其余类别与这条主干的差异: @@ -77,7 +77,7 @@ delivery 投递 jobs 作业:回填 / 出站重试 / 历史清理 / |---|---|---|---| | 日计划(`DNLD`/`RESP`) | 第 3 步改为每批一个事务,第 5 步在整包完成后 | 整包成功,含 Redis 刷新(`US-07` AC4/AC5) | 不标记已处理,下轮整包重处理(`US-07` AC4) | | 静态参考数据 | 无第 4 步;一个事务完成落库、终态与回填意图 | 该类落库成功(`US-13` AC1/AC5) | 校验不过整类不动,其他类照常(`US-13` AC2) | -| 出站请求 | 不走收报队列:登记新请求并作废同子类型旧请求;`RQRD` 与 `RQFD` 各自最多一条在途,后续请求待前一条结案再写入 `COUTMSGS` | 请求已写入信箱(`US-09` AC1) | `jobs` 按 `REQ_TRACK` 重试仍有效且确认未落信的请求;写入结果不明时记录并告警,不直接重发;交付承诺只到落信(`C-24`) | +| 出站请求 | 不走收报队列:登记新请求并作废同子类型旧请求;`RQRD` 与 `RQFD` 各自最多一条在途,后续请求待前一条结案再写入 `COUTMSGS` | 请求已写入信箱(`US-09` AC1) | `jobs` 按 `REQ_TRACK` 重试仍有效且确认未落信的请求;写入结果不明时记录并告警,不直接重发;交付承诺只到落信(`C-4`) | | 航班历史清理(作业) | 不走消息队列:历史写入成功后物理删除 | 实时数据已删(`US-14` AC3) | 历史写不成功不删,下轮重来(`US-14` AC3) | 出站的后半程:应答按报文类型匹配等待中的请求,发错或迟到的不更新数据、记录后跳过;超过时限未等到应答,标记超时;`EROR` 定位到本系统发出的请求,标记失败并告警(`US-09`)。 @@ -86,14 +86,14 @@ delivery 投递 jobs 作业:回填 / 出站重试 / 历史清理 / 以下约束不得违反: -- 只跑一个实例;主泵处理器写航班当前态,历史清理作物理删除,两者互斥;信箱读取不加锁(`OPS-1`、`PRE-5`、`INV-18`)。 -- 按顺序处理、只处理一次:每次只取编号最小的未完成消息,重复扫描、失败重处理与兼容入口并发都只登记一次、生效一次(`INV-2b`、`INV-3`、`INV-9`);不丢消息依赖「编号即到达顺序」且编号不复用、不回退(`PRE-2`、`PRE-3`)。 +- 只跑一个实例;主泵写航班当前态,历史清理作物理删除;正在被消息处理的航班清理时跳过(`OPS-1`、`PRE-5`、`US-14` AC4)。 +- 按顺序处理、只处理一次:每次只取编号最小的未完成消息(`US-03` AC1),重复扫描、失败重处理与兼容入口并发都只登记一次、生效一次(`INV-2b`);不丢消息依赖「编号即到达顺序」且编号不复用、不回退(`PRE-2`、`PRE-3`)。 - Redis 写成功才算处理完成;查询接口与网页客户端读同一份 Redis,出问题时报错,不返回空列表假装正常(`INV-23`、`INV-24`;`US-05`、`US-06`、`US-12`)。 -- 日计划快照以 AODB 下发为准:快照里没有的航班删除,未携带的字段清除;增量报文不适用这条(`INV-14b`、`INV-15b`;`US-07`)。 -- 对外投递至少一次,同一航班(`FLID`)内保序,跨航班不承诺顺序(`INV-10`、`C-29`;`US-08`)。 -- 航班只在历史写入成功后删除,历史库还没接通时一条也不删(`D1`、`INV-28`;`US-14`)。 -- 静态参考数据一类校验失败只停这一类,其他类照常;空值是「当前没有值」,不是删除(`INV-26`、`INV-27`;`US-13`)。 -- 航班唯一,版本只进不退:`FLID` 唯一,已写入非空的运营日不可改;每次成功写入版本号加一,重复消息不重复加(`INV-12`、`INV-13`)。 +- 日计划快照以 AODB 下发为准:快照里没有的航班删除,未携带的字段清除;增量报文按携带字段更新、未携带的不清空(`INV-15b`;`US-07`;`US-04` AC2)。 +- 对外投递至少一次,跨航班不承诺顺序(`C-9`;`US-08`)。 +- 航班只在历史写入成功后删除,历史库还没接通时一条也不删(`D1`;`US-14`)。 +- 静态参考数据一类校验失败只停这一类,其他类照常;空值是「当前没有值」,不是删除(`US-13`)。 +- 航班唯一:`FLID` 唯一(`INV-12`)。 这些约束是拿速度换来的:单活动主泵一次只推进一条,前一条没处理完,后面都得等。要提速、要多实例,光加线程没有用——得先重新设计消息顺序和数据由谁写,多实例还得补上可靠的互斥保护。 @@ -103,8 +103,8 @@ delivery 投递 jobs 作业:回填 / 出站重试 / 历史清理 / |---|---|---| | 自有 PostgreSQL | 处理锁 `PIPELINE_LOCK`、消息处理状态与回填意图 `PROC_STATE`、待发事件 `MSG_EVENT`、出站请求跟踪 `REQ_TRACK`、航班当前态 `FLIGHT_SCHD`、资源明细表、`FLIGHT_ROUTE_POINT`、未映射字段长期记录表 `UNMAPPED_FIELD`、静态参考数据 `REF_MASTER`、日计划快照留痕 `SCHD_SNAP_LOG` | 本系统唯一的业务数据库,也是航班当前态的唯一权威(`INV-11b`);本地事务只发生在这里,事务怎么分段见「主流程」。`UNMAPPED_FIELD` 不随 `PROC_STATE` 到期清理。 | | Redis | 航班查询投影 | 只作查询,不是权威,也不存处理状态(`INV-11b`);只由本系统写入和移除(`INV-24`),内容来自 PG 当前态;`GET /all/flights` 与网页客户端读的就是它。 | -| 共享 MySQL | `CMINMSGS` 入站信箱、`COUTMSGS` 出站信箱 | 信箱归外部系统所有。本系统只读写消息、回写处理标记,不建表、不改表结构、不清数据、不写历史表(`C-14`);原文保留多久、何时清除由库方定(`C-6`~`C-9`、`Q7`、`Q9`)。出站请求写进去就算交付(`C-24`)。 | -| 航班历史存储(Elasticsearch) | 已结束航班的历史副本 | 已结束航班写入这里作历史副本;写入确认成功后才删实时数据,写不成一条也不删(`D1`、`INV-28`)。保留期与容量上限未定(`G-FLIGHT-HIST-RETENTION`)。 | +| 共享 MySQL | `CMINMSGS` 入站信箱、`COUTMSGS` 出站信箱 | 信箱归外部系统所有。本系统只读写消息、回写处理标记,不建表、不改表结构、不清数据、不写历史表(`C-2`);原文保留与清除见 `C-1`(`Q9`)。出站请求写进去就算交付(`C-4`)。 | +| 航班历史存储(Elasticsearch) | 已结束航班的历史副本 | 已结束航班写入这里作历史副本;写入确认成功后才删实时数据,写不成一条也不删(`D1`、`US-14` AC3)。保留期与容量上限未定(`G-FLIGHT-HIST-RETENTION`)。 | **PG 的事务只管自己库。** Redis 写没写成、信箱标记写没写上、Kafka 发没发出,PG 事务都管不着;这些步骤各自可重试,重做多少遍结果都一样,重启后从 PG 记录接着走。 diff --git a/docs/contracts/interface-contract.md b/docs/contracts/interface-contract.md index d2ceaa0..04241c7 100644 --- a/docs/contracts/interface-contract.md +++ b/docs/contracts/interface-contract.md @@ -63,7 +63,7 @@ AODB 经 CIIMS adapter 把 XML 报文写入 `CMINMSGS`,格式以架构指定 | 表 | 本系统的操作 | 需要对接方提供的物理契约 | |---|---|---| | `CMINMSGS` | 按信箱编号升序、分批读取未处理的报文,扫描与回写用同一处理时间列;兼容 HTTP 入口写入 XML 原文;处理完成后写入处理完成时刻,只填空值、不覆盖已有值;写回失败由后台任务重试,一直写不上的记录保留在案并告警(`US-01`、`US-02`、`US-10`)。 | 表 DDL、信箱编号与报文原文字段、处理时间列的列名、类型与可空性及写入样例(处理标记即该处理时间列,见 [specification.md](../specification.md) 的 `C-5`)、写入必需列、原文保留期和索引;信箱编号按到达顺序单调递增、不复用、不回退的保证(架构「必须保持的约束」)。 | -| `COUTMSGS` | 写入 `RQRD` 参考数据请求与 `RQFD` 日计划请求;CIIMS adapter 消费。交付承诺止于请求落信;写入结果不明时记录并告警,不直接重发(架构「主流程」)。 | 表 DDL、请求原文字段、写入必需列、编号生成方式、ACK/错误列的写入责任、重复落信的识别规则。 | +| `COUTMSGS` | 写入 `RQRD` 参考数据请求与 `RQFD` 日计划请求;CIIMS adapter 消费。交付承诺止于请求落信;写入结果不明时记录并告警,不直接重发(架构「主流程」)。 | 表 DDL、请求原文字段、写入必需列、编号生成方式、重复落信的识别规则。 | 共享 MySQL 归 CIIMS adapter 方所有;本系统不建表、不改表结构、不清除数据,也不写共享历史表。外部表的物理字段必须以对接方提供的现行 DDL 与读写样例核对,不能由本文件推造。 @@ -87,7 +87,7 @@ AODB 经 CIIMS adapter 把 XML 报文写入 `CMINMSGS`,格式以架构指定 | 表或表组 | 边界 | 尚需确定的字段级契约 | |---|---|---| | `REF_MASTER` | 静态参考数据与资源状态按类别、编号保存在独立数据表,admin-api 直接只读;新消息覆盖旧记录,全量消息整体替换,增删改消息逐条处理(`US-13`);一类校验不通过只停这一类、其他类照常,校验失败类别的已有记录不变;字段为空表示「当前没有值」,不是删除(架构「必须保持的约束」)。 | 类别和编号的物理列、各类别字段及类型、主键/唯一键、空值在列中怎样保存、写入后何时可读。类别范围与消息中的识别标签见下表。 | -| `FLIGHT_SCHD`、资源明细表、`FLIGHT_ROUTE_POINT` | 航班当前态的唯一权威;`FLID` 唯一,已写入非空的运营日不可改,每次成功写入版本号加一、重复消息不重复加(架构「必须保持的约束」);Redis 和 Kafka 从处理结果派生,不反向覆盖这些表。 | 主键、字段与类型、资源明细表清单、外键/索引、版本字段的物理列与迁移 DDL。 | +| `FLIGHT_SCHD`、资源明细表、`FLIGHT_ROUTE_POINT` | 航班当前态的唯一权威;`FLID` 唯一(架构「必须保持的约束」);Redis 和 Kafka 从处理结果派生,不反向覆盖这些表。 | 主键、字段与类型、资源明细表清单、外键/索引、版本字段的物理列与迁移 DDL。 | | `PROC_STATE`、`MSG_EVENT`、`REQ_TRACK`、`SCHD_SNAP_LOG`、`PIPELINE_LOCK`、`UNMAPPED_FIELD` | 管道处理、待发事件、请求跟踪、留痕、互斥及未映射字段由本系统维护;不对外提供直接读写接口。 | 字段、约束、索引与迁移 DDL 由内部实现设计确定;若其他系统需读取,须另立读取契约。 | 自有 PostgreSQL 的物理表结构由本系统的迁移 DDL 定稿。生产环境若改用 Oracle 11g,字段类型与迁移方案需先完成适配验证。 diff --git a/docs/implementation.md b/docs/implementation.md index 7d09e53..49ec1aa 100644 --- a/docs/implementation.md +++ b/docs/implementation.md @@ -28,9 +28,9 @@ | `MSG_EVENT` | 等待投递的事件(outbox) | `EVENT_ID` 对 `KAFKA:msg` 是稳定事件身份并决定投递顺序;对 `KAFKA:schd` 是每次接受 upsert 时替换的写代次。`TARGET` 区分 `KAFKA:msg` / `KAFKA:schd`;`PARTITION_KEY` 当前取 `FLID`(`Q4` 定案前为假定,见 `C-29`);`EVENT_TYPE` 区分 UPSERT 与 TOMBSTONE。`KAFKA:schd` 按 `FLID` 单行 upsert,只保留最新 `STATE_VERSION`;`SENT_AT` 在投递确认的同一条 UPDATE 内写入,是保留期判定的唯一基准。 | | `REQ_TRACK` | 上游请求及应答关联 | 状态 `PENDING / SENT / DONE / EXPIRED`;保存请求类型、覆盖运营日、发送方、出站信箱 ID 与发送/完成时间;**「同类只允许一个开放请求」的唯一键 = `(请求类型, 覆盖运营日, 发送方)`,且仅对开放状态生效**。 | | `REF_MASTER` | SIS 消息提供的静态参考数据与资源状态(目标表) | `(RTYPE, RKEY)` 唯一;`RTYPE` 类别、合并语义与资源状态见「静态参考数据」;取数路径见 [requirements.md](requirements.md) `US-13`。 | -| `FLIGHT_SCHD` | 航班标量及单值异常字段 | `FLID` 主键;`OPERATION_DAY` 一经确定不可变;版本与最近消息 ID 用于追踪。变长集合存于资源明细表与 `FLIGHT_ROUTE_POINT`,规则见「航班域」。 | +| `FLIGHT_SCHD` | 航班标量及单值异常字段 | `FLID` 主键;运营日与版本、最近消息 ID 用于追踪。变长集合存于资源明细表与 `FLIGHT_ROUTE_POINT`,规则见「航班域」。 | | `SCHD_SNAP_LOG` | 日计划处理留痕 | 只追加、可重建,不参与状态决策;保留期见 [reference.md](reference.md)。 | -字段与索引以 `src/main/resources/db/migration/` 的迁移链为准(Oracle 11g 目录为占位,未接入 Flyway)。报文原文仍从共享信箱读取,原文保留期必须满足 `C-7`;处理标记值集见 `C-5`,清除前提与保留期下界见 `C-6`~`C-9`、`C-11`。 +字段与索引以 `src/main/resources/db/migration/` 的迁移链为准(Oracle 11g 目录为占位,未接入 Flyway)。报文原文仍从共享信箱读取;信箱清理见 `C-1`。 ## 2. 消息、身份与决策 @@ -41,7 +41,7 @@ **身份绑定是独立的幂等单语句**(`WHERE IDENTITY_KEY IS NULL`),不参与业务事务。它的前提是「报文不可变」(`PRE-7`):同一身份的重发不会被比对内容,若上游改发正文会被判为重复并跳过(`Q15`)。 -分派与落库由 `MessageProcessor` 协调:按 `MsgKind` 把已绑定身份的队头消息交给对应事务协调器(SCHD-DNLD/RESP → `ScheduleProcessor`,ADFT → `AdftProcessor`,FLOP → `FlopProcessor`,FDEL → `FdelProcessor`,`SIS:3.1`~`SIS:3.14` 的静态参考数据消息 → `ReferenceDataProcessor`,其余 → `SKIPPED(unsupported)`)。这些处理器在 `PIPELINE_LOCK` 事务内读取当前完整态,调用纯领域决策逻辑得到下一完整态与待发事件,提交该事务后写 Redis 投影,再在另一事务中登记处理终态与回填意图(`INV-17b`、`INV-23`);它们不直接触碰 Kafka。领域决策逻辑不执行 I/O。处理步骤的锁跨越 Redis 写,因此与航班历史清理的互斥覆盖整个步骤(`INV-18`)。 +分派与落库由 `MessageProcessor` 协调:按 `MsgKind` 把已绑定身份的队头消息交给对应事务协调器(SCHD-DNLD/RESP → `ScheduleProcessor`,ADFT → `AdftProcessor`,FLOP → `FlopProcessor`,FDEL → `FdelProcessor`,`SIS:3.1`~`SIS:3.14` 的静态参考数据消息 → `ReferenceDataProcessor`,其余 → `SKIPPED(unsupported)`)。这些处理器在 `PIPELINE_LOCK` 事务内读取当前完整态,调用纯领域决策逻辑得到下一完整态与待发事件,提交该事务后写 Redis 投影,再在另一事务中登记处理终态与回填意图(`INV-3`、`INV-23`);它们不直接触碰 Kafka。领域决策逻辑不执行 I/O。处理步骤的锁跨越 Redis 写,使与航班历史清理的协作覆盖整个步骤(`US-14` AC4)。 合法但本系统不支持的消息类型:跳过留档、按已处理写回标记(`US-03` AC2),不重试。`REGN` / `RSTA` 是静态参考数据消息,必须分派给 `US-13`,不得跳过。 @@ -81,7 +81,7 @@ ### 4.3 兼容 HTTP 入口 -`POST /cminmsgs/send` 把报文写入共享信箱(处理时间为空),效果与上游投递一致:由收报扫描发现、登记、处理。客户端失败重试可能再次写信箱,业务身份去重仍然必需。响应语义见 `C-28`。 +`POST /cminmsgs/send` 把报文写入共享信箱(处理时间为空),效果与上游投递一致:由收报扫描发现、登记、处理。客户端失败重试可能再次写信箱,业务身份去重仍然必需。响应语义见 `C-8`。 ### 4.4 单实例 @@ -141,11 +141,11 @@ processOne(head): | Redis 投影写 | 否 | 是(处理步骤锁跨越本步) | 否 | 外部副作用,不在 PG 事务内;写成功是终态事务的前置(`INV-23`) | | 业务型终态(`SUCCEEDED` + 回填意图) | 是 | 是 | 否 | 同库事务(`INV-17b`) | | 非业务型终态(`MALFORMED` / `PROTOCOL` / `SKIPPED` / `EXHAUSTED`) | 否 | 否 | 否 | 单语句(终态与回填意图同一条 UPDATE) | -| 航班历史清理的物理删除 | 是 | 是(`INV-18`) | 是 | 同库事务:复查判据 + 历史写入成功后删除 | +| 航班历史清理的物理删除 | 是 | 是(落实 `US-14` AC4) | 是 | 同库事务:复查判据 + 历史写入成功后删除 | | 回填(信箱标记 + `BACKFILL_AT`) | 否 | 否 | 否 | 跨库两次单写;幂等可重跑 | | 人工重放(批量改回 `PENDING`) | 否 | 否 | 否 | 单语句批量;`MessageLifecycleGate` 与回填互斥 | -结论:航班变更与处理终态**不在同一事务**——两者之间夹着 Redis 投影写;同一事务只保证「航班变更 + 事件」与「终态 + 回填意图」各自原子(`INV-17b`、`INV-23`)。处理步骤的 `PIPELINE_LOCK` 跨越 Redis 写,使 `INV-18` 的互斥覆盖整个步骤;该锁的竞争写者是**航班历史清理**,不是别的处理器线程;没有第二写者时该锁不产生额外串行度。 +结论:航班变更与处理终态**不在同一事务**——两者之间夹着 Redis 投影写;同一事务只保证「航班变更 + 事件」与「终态 + 回填意图」各自原子(`INV-3`、`INV-23`)。处理步骤的 `PIPELINE_LOCK` 跨越 Redis 写,使与航班历史清理的协作覆盖整个步骤(`US-14` AC4);该锁的竞争写者是**航班历史清理**,不是别的处理器线程;没有第二写者时该锁不产生额外串行度。 ### 5.4 历史积压 @@ -155,7 +155,7 @@ processOne(head): - 不加速、不分流、不走旁路:不允许并行队头,也不允许实时消息跳过积压。 - 尝试上限与退避对积压同样生效,不因积压而放宽。 - 不再处理的行置 `SKIPPED` 并记录原因,到达终态后走回填通道;不存在「整段 DELETE」的快速通道。 -- 消化期间的可观测项与完成时限口径见 [reference.md](reference.md) 与 `CLM-9`:**扫描周期不是完成时限**。 +- 消化期间的可观测项见 [reference.md](reference.md);完成时限不作对外承诺(`CLM-9`),现场一般为即时处理。 ## 6. 回填 @@ -185,7 +185,7 @@ LIMIT PARAM:msgx.pipeline.backfill-batch | 信箱行不存在 | 写入 0 行且信箱行不存在 | **立即放弃自动重试**(原因 `MISSING_ROW`)并告警。终态行存在而信箱行不存在,只可能是该行在入队后被删除(永久空洞 ID 从不入队,不会进入本扫描) | | 暂时性故障持续超期 | 超时 / 连接失败持续到 `R` 仍未打标 | **停止自动重试**(原因 `TRANSIENT_DEADLINE`)并告警;`R` 之前只退避重试,**不按尝试次数放弃**;保留人工恢复能力 | -**放弃 ≠ 标记已确认**:放弃行不写 `BACKFILL_AT`,处理标记仍为空,「行最终都有标记」因此不能对外承诺(`CLM-4`);放弃清单能否作为清除判定依据,属未确认的清除协议(`C-8`;见 `Q7`/`Q9`)。 +**放弃 ≠ 标记已确认**:放弃行不写 `BACKFILL_AT`,处理标记仍为空;放弃清单能否作为清除判定依据,属未确认的清除协议(`C-1`;见 `Q9`)。 ### 6.3 `R` 的作用 @@ -201,11 +201,11 @@ LIMIT PARAM:msgx.pipeline.backfill-batch 1. 退避重试(`R` 之前不放弃); 2. 到 `R` 仍失败则停止自动重试、告警,保留人工恢复(`reopen`)。 -第三段「库方以放弃清单作为清除判定」未确认,因此「最终一定打标」当前不可承诺(`CLM-4`)。 +第三段「库方以放弃清单作为清除判定」未确认(`C-1`;见 `Q9`)。 -两个边界要说清:`MISSING_ROW`(信箱行不存在)是**确定性结论**,立即放弃,不受 `R` 保护;`R` 只要求 `R ≤ R_keep`,原文保留期的唯一约束来源是 `C-7`。 +两个边界要说清:`MISSING_ROW`(信箱行不存在)是**确定性结论**,立即放弃,不受 `R` 保护;`R` 只要求 `R ≤ R_keep`,原文保留期的唯一约束来源是 `C-1`。 -库方的清除语义未确认(`C-6`、`C-8`;见 `Q7`/`Q9`):若为「打标即可清除」,`C-7` 的保留期下界不成立,必须与库方另定;增大 `R` 无效。 +库方的清除语义未确认(`C-1`;见 `Q9`):若为「打标即可清除」,`C-1` 的清理前提不成立,必须与库方另定;增大 `R` 无效。 ## 7. 日计划快照与请求匹配 @@ -235,7 +235,7 @@ PENDING → SENT → DONE - 应答优先按已确认的回显字段精确匹配;降级匹配的跨代误配风险必须明确接受并审计(`C-23`)。 - 时间比较统一时区与单位,并需定义时钟偏斜容忍;容忍判据未定(`Q5`),在定义前不得把降级匹配描述为精确关联。 - 参考应答写入自有 `REF_MASTER`,日计划应答走快照流程;请求完成必须在相应数据处理成功之后,超时和迟到应答不能修改已关闭请求对应的状态。 -- 出站承诺只到落信(`C-24`、`CLM-8`);主 / 共享删除的 EROR 回报义务见 `C-25`。 +- 出站承诺只到落信(`C-4`、`CLM-8`);主 / 共享删除见 `C-5`。 ## 8. 事件投递 @@ -245,7 +245,7 @@ PENDING → SENT → DONE 本批领取的 `EVENT_ID` 集合在**读取时刻冻结**:发送与标记只作用于这批事件,期间新提交的事件留待下一轮,不参与本批,也不被本批的「完成」带走。 -`EVENT_ID` 由全局串行分配产生:事件生产者在事务内写 outbox,主泵单线程,历史清理与主泵互斥(`INV-18`),因此**分配顺序 = 提交顺序**,不存在「已提交的较大 ID 先于未提交的较小 ID 被投递」。 +`EVENT_ID` 由全局串行分配产生:事件生产者在事务内写 outbox,主泵单线程,历史清理与主泵对实时航班表串行(落实 `US-14` AC4),因此**分配顺序 = 提交顺序**,不存在「已提交的较大 ID 先于未提交的较小 ID 被投递」。 发送确认后才标记 `SENT`,失败记录次数并按退避推后,达到上限转 `DEAD`(记录保留作 DLQ)。所有外部调用需要有界超时,避免阻塞投递线程。 @@ -288,7 +288,7 @@ PENDING → SENT → DONE |---|---|---| | 已落信、未入队 | 信箱行处理时间为空且 PG 无记录 | 重扫补建登记记录 | | 事务执行中 | PG 无该消息终态 | 事务整体回滚,按 `PENDING` 重新处理 | -| 领域事务已提交、Redis 写失败或终态未提交 | 该消息无终态(`PENDING`),仍占队头 | 整条消息重处理:投影按当前完整态重写,领域变更依赖逐类幂等(`INV-20b`,`G-FLOP-IDEMPOTENT`),已提交结果不回滚(`INV-16`) | +| 领域事务已提交、Redis 写失败或终态未提交 | 该消息无终态(`PENDING`),仍占队头 | 整条消息重处理:投影按当前完整态重写;领域再跑是否只留一次效果见 `US-03`、`G-FLOP-IDEMPOTENT`;已提交结果不回滚(`US-03` AC3) | | 事务已提交、标记未写 | 终态行仍持有回填意图 | 仅补写标记;业务处理结果保持不变 | | 标记写入中途 | 标记仍为空 | 重新写入;重复写入同一值无副作用 | | 回填时信箱行已不存在 | 写入 0 行且信箱行不存在 | 立即放弃自动重试(`MISSING_ROW`)并告警;放弃不等于标记已确认,仍需人工对账 | @@ -298,13 +298,13 @@ PENDING → SENT → DONE ### 9.3 生命周期与清除 -`JobRunner` 用独立 daemon 线程按周期触发回填扫描、航班历史清理与留痕清理;作业不参与消息 FIFO,也不使到期消息饥饿。`INV-18` 要求历史清理的删除与主泵处理互斥。 +`JobRunner` 用独立 daemon 线程按周期触发回填扫描、航班历史清理与留痕清理;作业不参与消息 FIFO,也不使到期消息饥饿。历史清理时跳过正在被消息处理的航班(`US-14` AC4)。 **通则**(对本系统所有持久对象适用) -- **时间不构成清除依据**:到期只是必要条件,**终局证据才是充分条件**(航班见 `INV-28`;共享库的清除依据属未确认的清除协议,见 `C-8` 与 `Q9`)。 +- **时间不构成清除依据**:到期只是必要条件,**终局证据才是充分条件**(航班见 `US-14` AC3、`D1`;共享库的清除依据属未确认的清除协议,见 `C-1` 与 `Q9`)。 - **证据不随清除消失**:回填失败与放弃的记录在其覆盖的信箱行被清除前保持可查(`C-16`)。 -- **证据缺失或结果不明时按最保守处置**:航班清理为删 0 条(`INV-28`)。 +- **证据缺失或结果不明时按最保守处置**:航班清理为删 0 条(`US-14` AC3、`D1`)。 **逐对象生命周期**(保留期取值一律见 [reference.md](reference.md)) @@ -318,24 +318,24 @@ PENDING → SENT → DONE | `PROC_STATE` 终态行 | 见下 | 无(到期直接删除) | 回填了结 | 我们 | `G-PROC-CLEANUP` | | `REQ_TRACK` 关闭态行 | 保留期 | 无 | 无 | 我们 | `G-REQ-TRACK-RETENTION` | -**处理记录到期清理**(`INV-25`) +**处理记录到期清理**(`US-11`) 候选 = 终态 **且** 回填已了结 **且** 终局后超过保留期(基准是 `UPDATED_AT`:终态与了结都推进它,了结后不再更新)。两处不可省: -- **回填已了结** = `BACKFILL_AT` 非空(`INV-25`)。放弃行不写标记,按未了结保留,不参与删除。 +- **回填已了结** = `BACKFILL_AT` 非空(`US-11`)。放弃行不写标记,按未了结保留,不参与删除。 - **写入前复查** = `DEAD` 可被人工重放改回 `PENDING`。人工重放走 `MessageLifecycleGate`、不取 `PIPELINE_LOCK`,因此该锁不构成复查依据:删除在同一事务内按候选时的 `STATE` 条件执行;影响 0 行即整体回滚、该行跳过。重放先一步改回 `PENDING` 时谓词不匹配,天然互斥。批量删除不得持 `PIPELINE_LOCK`——那会阻塞主泵 FIFO,与「作业不使到期消息饥饿」冲突。 -清理范围只含「终态且已回填」;保留期内同身份去重成立(`INV-9`)。 +清理范围只含「终态且已回填」;保留期内同身份去重见本章「消息、身份与决策」。 -**时间常数排序**:`R` 的取值与依据见 [reference.md](reference.md) 的 `PARAM:msgx.pipeline.overdue-backfill`,`R_keep` 的下界见 [specification.md](specification.md) 的 `C-7`,本文件不复述。只补一条实现口径:保留期计的是**终局之后**的时间,不是入队之后——终态行未了结回填时不进入候选。 +**时间常数排序**:`R` 的取值与依据见 [reference.md](reference.md) 的 `PARAM:msgx.pipeline.overdue-backfill`,信箱清理前提见 [specification.md](specification.md) 的 `C-1`,本文件不复述。只补一条实现口径:保留期计的是**终局之后**的时间,不是入队之后——终态行未了结回填时不进入候选。 **其余清理** -- **航班历史清理**:按 [reference.md](reference.md) 的历史判据选候选(含 `DELETED`),先成功写入历史存储再删除;语义与红线见「航班域」与 `INV-28`。 +- **航班历史清理**:按 [reference.md](reference.md) 的历史判据选候选(含 `DELETED`),先成功写入历史存储再删除;语义与红线见「航班域」与 `US-14`、`D1`。 - **留痕清理**:`SCHD_SNAP_LOG` 按保留期与 `(SCOPE_END, RECV_AT)` 删除,不依赖历史存储开关。 - **出站事件清理**:见「事件清理」。 -共享信箱保留策略由库方管理(`C-6`~`C-9`、`C-11`)。历史写入与删除事件入队之间仍需恢复方案;顺序调用不构成原子提交。 +共享信箱保留与清理见 `C-1`、`C-2`。历史写入与删除事件入队之间仍需恢复方案;顺序调用不构成原子提交。 ## 10. 容量假设与设计取舍 @@ -371,9 +371,7 @@ PENDING → SENT → DONE ### 11.2 航班身份与运营日 -`FLID` 是主键。`OPERATION_DAY` 从 SCHD 记录的 `SODT` 按配置的机场时区和切日规则推导;它不是消息接收日或落库日。 - -运营日一经写入不可更改(`INV-12`)。遇到冲突,整包日计划按协议错误拒绝,既有状态保持不变(`INV-19`)。尚未由日计划收录的航班可以为 `NULL`;这不表示该航班没有运营日,只表示当前模型无法为它确定归属日。 +`FLID` 是主键。`OPERATION_DAY` 从 SCHD 记录的 `SODT` 按配置的机场时区和切日规则推导;它不是消息接收日或落库日。尚未由日计划收录的航班可以为 `NULL`;这不表示该航班没有运营日,只表示当前模型无法为它确定归属日。运营日冲突如何处置见 `Q23`(`INV-4` 只管整份校验不过时本地不改)。 ### 11.3 字段与集合 @@ -408,18 +406,18 @@ PENDING → SENT → DONE - `ORDINAL` 是持久化顺序,从 1 开始;`SOURCE_SEQ` 是上游序号,允许为空或重复。 - 相同资源号不代表同一条分配,禁止按资源号去重。 -- 每次持久化完整航班状态时,明细表按该 `FLID` 先删后插,以完整合并结果为准(`INV-14b`)。 +- 每次持久化完整航班状态时,明细表按该 `FLID` 先删后插,以完整合并结果为准(`US-04` AC2)。 - ROUT 与 ERUT 是两类独立集合,不能因相同序号覆盖彼此。 - `CHDT` 的类字段固定为 `CCLS`/`CTYP`;当前 wire DTO 与持久化列误写成 `CHCLS`/`CHTYP`,见 `G-FLOP-UNMAPPED`。 - 主/共享关系以主表的 `MAID` 为事实来源:`MAID` 是共享航班指向主航班 `FLID` 的引用(非共享航班为 `NULL`);`MAFL` 只在读取和事件投影时从子航班事实派生,不按入站标量解析或保存。 ### 11.4 主/共享投影(`MAFL`) -`MAFL` 是主航班的派生集合,元素为子航班的 `FLID` 与 `FLNO`;内容与变更传播分别由 `INV-21`、`INV-22` 保证。 +`MAFL` 是主航班的派生集合,元素为子航班的 `FLID` 与 `FLNO`;内容与变更传播见本章与 `US-06` AC2。 -- 已 FDEL 的子航班(`STATE = DELETED`)自然退出投影(内容见 `INV-21`),不需要改写主航班行。 +- 已 FDEL 的子航班(`STATE = DELETED`)自然退出投影,不需要改写主航班行。 - 只有 `MAID` 为空的主航班携带 `MAFL`;共享航班只携带自身 `MAID`、`CSOP`、`CSFT`,不携带 `MAFL`,避免下游双向合并。 -- 同一 `STATE_VERSION` 的投影逐字节稳定(顺序见 `INV-21`),与到达顺序及 `FLNO` 变更无关;重发与消费端比对才有意义。 +- 同一写代次下投影逐字节稳定,与到达顺序及 `FLNO` 变更无关;重发与消费端比对才有意义。 - `MAID = FLID` 的自引用行不进入任何 `MAFL`;`MAID` 指向不存在主航班的悬挂引用不阻断该子航班自身处理,只是不产生投影。 - 子航班集合变化的传播见 `INV-22`,事件类型为 `KAFKA:msg` + `KAFKA:schd`;否则整态投影的只进不退写入会丢弃它(见「`schd` 聚合」)。共享航班自身不单独发通知。 - 派生主航班投影与产生它的状态写入必须同一事务或一致读快照;按 `MAID` 取子航班要求该列有索引(`INV-17b`、`INV-22`)。 @@ -432,13 +430,13 @@ PENDING → SENT → DONE SCHD DNLD/RESP 在整包校验通过后,分批将报文携带的航班写入当前态,每批一个事务;快照里没有的航班删除:标记已删除、登记删除事件、从 Redis 投影移除(`INV-15b`)。日计划就是主动与 AODB 全量同步一次,以 AODB 下发的数据为准。 -日计划里某航班没携带的字段,视为 AODB 已删除该值,本地同步清除(`C-26`)。每个成功写入的航班推进 `STATE_VERSION`(`INV-13`),并在同一事务登记 `KAFKA:schd` 与 `KAFKA:msg` 事件。 +日计划里某航班没携带的字段,视为 AODB 已删除该值,本地同步清除(`C-26`)。每个成功写入的航班推进 `STATE_VERSION`,并在同一事务登记 `KAFKA:schd` 与 `KAFKA:msg` 事件。 -消息重复处理由 `PROC_STATE` 的消息 ID 与 `IDENTITY_KEY` 控制;已成功提交的消息不得再次写入或重复登记事件。整包校验失败或运营日冲突时,整包不落地(`INV-19`)。 +消息重复处理由 `PROC_STATE` 的消息 ID 与 `IDENTITY_KEY` 控制;已成功提交的消息不得再次写入或重复登记事件。整包校验失败时整包不落地(`INV-4`);运营日冲突见 `Q23`。 ### 12.2 动态运行事件(FLOP) -FLOP 只修改报文表达的字段或集合,其余状态保持不变;目标形态与合并规则见「字段与集合」。`STYP` 必须命中下表白名单,未知值按不支持类型跳过留档(`US-03` AC2),不得进入通用合并。已确认的动态更新在同一事务推进 `STATE_VERSION` 并登记 `KAFKA:msg` 与 `KAFKA:schd`(`INV-17b`);处理终态与回填意图在 Redis 投影写成功后的另一事务中提交(`INV-23`)。同一消息重复处理不得重复产生业务效果(`INV-20b`)。 +FLOP 只修改报文表达的字段或集合,其余状态保持不变;目标形态与合并规则见「字段与集合」。`STYP` 必须命中下表白名单,未知值按不支持类型跳过留档(`US-03` AC2),不得进入通用合并。已确认的动态更新在同一事务推进 `STATE_VERSION` 并登记 `KAFKA:msg` 与 `KAFKA:schd`(`INV-17b`);处理终态与回填意图在 Redis 投影写成功后的另一事务中提交(`INV-23`)。同一消息不产生两次效果见 `US-03`;逐类规则未补齐见 `G-FLOP-IDEMPOTENT`。 逐类语义以 SIS 的字段表、空标签规则与 Processing Exceptions 为准;下表每一行都必须有一条回归用例钉住「输入与前态 → 目标状态 → 终态与事件」。 @@ -470,7 +468,7 @@ FLOP 只修改报文表达的字段或集合,其余状态保持不变;目标 | `SIS:3.42` | `TRML` | `TRML` 标量 | 空 = 删除航站楼 | | `SIS:3.43` | `VIPP` | `VIPP`/`VIPR` 标量 | 空 = 删除;SIS 另要求 RMS 忽略 `VIPP`(忽略事件还是忽略字段,SIS 未写明,按 `Q8` 对拍确认) | -**方向约束**:`SIS:3.24`、`SIS:3.25`、`SIS:3.26`、`SIS:3.34`、`SIS:3.38` 五节写的是 RMS→AODB 方向。需求已定案:这五类资源分配报文 AODB 实际会发,照常接收处理(`US-05` AC3);与 AODB 的核对确认是实施任务。 +`CKDT`、`CLDT`、`CHDT`、`GTDT`、`PSDT` 等 FLOP 子类型是否入站、如何合并,见 `US-05` 与本章「动态运行事件」;SIS 中的 RMS→AODB 方向说明不约束本系统(OMMS 只收不发)。 **SIS 未定义但 legacy 处理的子类型**:`ABTM`、`DELY`、`PADT`、`FTSS`、`STND`、`BDPB`、`REMC`。它们在 `XSD` 的 `FLOP` 段里是普通字段或集合,没有独立事件节;legacy 按 `STYP` 命中它们处理,说明真实流量可能存在 SIS 未记载的子类型。其报文形态与是否存在必须以真实报文确认(`Q8`),不得据 legacy 行为直接定案。 @@ -483,7 +481,7 @@ FLOP 只修改报文表达的字段或集合,其余状态保持不变;目标 ### 12.3 删除与重建 FDEL 是业务删除入口:仅在 `ACTIVE → DELETED` 时推进版本、保留明细并与 tombstone 同事务登记;重复 FDEL 或不存在的航班按幂等成功处理。 -物理删除仅由独立历史清理在历史写入成功后执行(`INV-28`)。快照里没有的航班删除(`INV-15b`);FDEL 仍是最先的删除入口。清理前需要登记一次 tombstone;已经 FDEL 的记录不重复发出。 +物理删除仅由独立历史清理在历史写入成功后执行(`US-14` AC3、`D1`)。快照里没有的航班删除(`INV-7`);FDEL 仍是最先的删除入口。清理前需要登记一次 tombstone;已经 FDEL 的记录不重复发出。 ADFT 的字段缺失语义尚待上游确认。在确认前采用保守的 Set-only 规则:出现字段可更新,缺失字段不清空;不得把它当成日计划或动态全量替换。新建 ADFT 若带可解析的 `SODT`,按同一运营日规则计算 `OPERATION_DAY`;否则保留为 `NULL`。 diff --git a/docs/reference.md b/docs/reference.md index d5b3315..ee0c1f3 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -20,7 +20,7 @@ | `msgx.pipeline.max-commit-delay` | `5m` | Duration | **假定(无依据)** | **待退役**:水位空洞老化阈值,随收报改谓词扫描(`G-SCAN-PREDICATE`)删除;依据 `C-2` 已作废 | | `msgx.pipeline.overdue-backfill` | `30d` | Duration | 契约(`R ≤ R_keep`) | 即 `R`:进入强补写窗口、**取消退避**的阈值;**不是兜底保证** | | `msgx.pipeline.backfill-batch` | `100` | 条 | 假定 | 回填扫描单批条数 | -| `msgx.pipeline.backfill-scan-period` | `30s`(代码常量,无配置键) | Duration | 现役 | 回填扫描作业周期;批次积压与单行超时会延长实际标记延迟(`CLM-9`) | +| `msgx.pipeline.backfill-scan-period` | `30s`(代码常量,无配置键) | Duration | 现役 | 回填扫描作业周期 | | `msgx.pipeline.backfill-max-attempts` | `100` | 次 | 假定 | 单行重试的**告警阈值**;放弃判据是 `R` 超期,不是次数 | | `msgx.pipeline.backfill-backoff-ms` | `30000` | ms | 假定 | 回填独立退避起步间隔;`BackfillService` 指数退避的首档 | | `msgx.pipeline.backfill-backoff-cap-ms` | `900000` | ms | 假定 | 回填退避封顶(15 分钟) | diff --git a/docs/specification.md b/docs/specification.md index f93778f..b33ca99 100644 --- a/docs/specification.md +++ b/docs/specification.md @@ -11,8 +11,6 @@ - 本系统单方遵守、不依赖对方认可的,末尾标 `(本系统单方承诺)`。 - 未标注的,表示需求或架构已定,本系统按此执行。 -作废条款集中放在各节末尾,保留原文与作废标记。 - ## 1. 术语 | 术语 | 含义 | @@ -26,9 +24,9 @@ | 处理完成 | 这条消息的结果已经确定——业务改动生效,或明确跳过、进入死信;之后才回填信箱标记、发 Kafka。 | | 回填 | 处理完成后,把完成时刻写回信箱行的处理时间字段,告知上游该消息已处理。 | | 投递 | 读待发事件,发往 Kafka。 | -| Redis 投影 | 供网页客户端(`GET /all/flights`)查询的航班投影,内容来自自有 PG 当前态;读写边界见 `INV-24`。 | +| Redis 投影 | 供网页客户端(`GET /all/flights`)查询的航班投影,内容来自自有 PG 当前态;读写边界见 `INV-11`。 | | 自有 PG | 本系统唯一的业务数据库;航班当前态、管道记录与静态参考数据都在这里。 | -| 权威 | 航班当前态以自有 PG 为准(`INV-11b`)。 | +| 权威 | 航班当前态以自有 PG 为准(`INV-5`)。 | | 出站请求 | 经 `COUTMSGS` 发向 AODB 的 `RQRD` 参考数据请求与 `RQFD` 日计划请求,消费方为 CIIMS adapter。 | | 运营航班显示界面 | 需求所称网页客户端:Kafka 侧称运营航班显示界面,查询侧称网页客户端(`GET /all/flights` / Redis)。 | | 航班历史 | 已结束航班写入 Elasticsearch 后的副本;写成功后才从实时数据删除。 | @@ -68,78 +66,41 @@ | 编号 | 前提 | 若不成立的影响 | 状态 | |---|---|---|---| -| PRE-1 | 信箱消费权排他:同一时刻只有一个系统有权处理、打标与判定可清除 | 采集谓词、身份去重与清除前提全部失效 | 由切流规程保证,上线前另立 | -| PRE-2 | 信箱编号的分配顺序即到达顺序 | 「编号即到达顺序」的顺序声称失去依据 | 待确认 `Q2` | -| PRE-3 | 信箱编号空间不复位、不复用、不回退 | 同一编号可能重复登记 | 待确认 `Q2` | -| PRE-4 | 信箱时间列的时钟基准可解释,偏斜有界 | 与本地时间的比较会提前或推迟判定 | 待确认 `Q7` | -| PRE-5 | 单活动实例运行,信箱读取不加锁 | 并行读会让「已处理」判定互相踩踏 | 部署约束 | -| PRE-6 | 信箱与自有 PG 之间没有跨库事务 | 回填与清除都不能声称原子 | 架构事实 | -| PRE-7 | 报文不可变:同一业务身份的重发是同一内容 | 上游改发正文会被判为重复并静默跳过 | 待确认 `Q15` | -| PRE-9 | 测试环境与生产隔离:测试实例使用独立的数据库、Redis 与 Kafka 主题,不连接生产信箱 | 测试结论与隔离验收(`OPS-3`)失效 | 部署与配置约束 | +| PRE-1 | 测试环境与生产隔离:独立的数据库、Redis 与 Kafka 主题,不连接生产信箱 | 测试结论与隔离验收(`OPS-3`)失效 | 部署与配置约束 | ## 4. 不变量 ### 4.1 管道 -- **INV-1** 五个事实互不替代:落信、登记、处理完成、回填、投递各有独立证据,前一个不蕴含后一个。 -- **INV-2b** 扫描谓词与幂等登记:每轮按编号升序读取「处理时间为空」的信箱行,每批有上限;同一编号只登记一次;重复扫描与重启恢复不重复登记、不丢行。失败后的重处理是 `US-03` AC3 的要求;成功业务效果至多一次由 `INV-20b` 承载,当前被 `G-FLOP-IDEMPOTENT` 阻断。 -- **INV-3** 队头唯一:任一时刻只有一个可执行的队头(编号最小的未完成消息),队头未完成时后面的消息不得越过。 -- **INV-6** 处理终态不可逆:已提交的成功不因回填或投递失败回改。 -- **INV-7** 处理标记单调:任何路径只把空标记写成已处理值,不回撤、不覆盖。 -- **INV-8** 回填只针对已有终态的消息;「还欠一次回填」与终态由同一条语句落库,不存在第二处落账。 -- **INV-9** 一信一行、一身份一记录:处理记录按信箱编号唯一,同一业务身份至多绑定一条有效记录。再次发现同一身份的信箱行时不得直接跳过:须与既有记录比对内容,一致才按重复跳过留档(`PRE-7`:重发是同一内容);不一致即 `PRE-7` 失效信号,留档、告警、不产生业务效果,处置待 `Q15` 定案。 -- **INV-10** 对外投递至少一次;端到端恰好一次不在交付范围。 -- **INV-16** 回填与 Kafka 投递失败可重试,不回滚已提交的本地业务结果;出站请求写入结果不明时记录告警、不直接重发。 -- **INV-17b** 航班类消息(增量、删除)的状态变更与待发事件在同一事务提交;处理终态与回填意图在另一事务提交,且晚于 Redis 投影写成功。待发事件在处理完成前不可投递(`US-03` AC4),本句对所有类别生效。日计划的分批与静态参考数据的单事务见架构「主流程」。 -- **INV-19** 日计划整包校验失败时整包不落地,既有状态与版本不变(`US-07` AC1)。运营日冲突时的处置未在需求与架构定案(`Q23`)。 -- **INV-20b** 处理器幂等(目标不变量,被 `G-FLOP-IDEMPOTENT` 阻断,当前不成立):同一消息在失败重处理与重复发现下都只产生一次业务效果。身份唯一只防「重复记录」,不防「重新执行」;逐类幂等规则补齐并回归前,依赖本条的收敛声明(`CLM-3`)不可作出。 -- **INV-30** 非法报文进死信、合法但不支持的类型跳过留档,两者都无业务副作用、按已处理回填(`US-03` AC2);处理错误记在对应消息的处理记录上,不被外层吞掉(`US-03` AC5)。 +- **INV-1** 处理时间仍为空的信箱行会登记到本地;同一编号只登记一次。重扫或重启不会重复登记,也不会漏掉未登记的行。 +- **INV-2** 只有本地处理结束后,才写信箱里的处理时间;本地记「已结束」时,必须同时记下还要去更新信箱,两笔记在同一次写入里。 +- **INV-3** 航班增量与删除:改状态和记「待发 Kafka」同一次写入;记「已结束」和「还要更新信箱」另一次写入,且须在 Redis 写成功之后。处理未结束前不发 Kafka(`US-03` AC4)。日计划与静态参考数据见架构「主流程」。 +- **INV-4** 日计划整份报文校验不过时,本地航班数据一律不改(`US-07` AC1)。 ### 4.2 航班域 -- **INV-11b** 自有 PG 的航班当前态是唯一权威;信箱、Redis 投影、Kafka、展示视图都不是权威。 -- **INV-12** `FLID` 唯一;已写入非空的运营日不可改。 -- **INV-13** 每次成功写入版本号加一;重复消息不重复加,版本不回退。 -- **INV-14b** 增量报文未携带的字段不被隐式清空。日计划快照不适用本条(`INV-15b`)。 -- **INV-15b** 日计划快照以 AODB 下发为准:快照里没有的航班删除——标记已删除、登记删除事件、从 Redis 投影移除;未携带的字段同步清除(`US-07` AC2/AC3)。 -- **INV-18** 航班表的写者是主泵处理器与航班历史清理,两者必须互斥,不得出现清理删除与处理器更新同一个 `FLID` 的竞态。 -- **INV-21** 主航班与共享航班的删除各自独立标记(`US-06` AC2、`C-5`)。 -- **INV-22** 航班标记删除后从 Redis 投影移除。移除成功后才算处理完成(`INV-17b`、`INV-23`);失败即未完成,下轮重做。共享航班在 Redis 投影中的呈现方式、删除通知使用共享还是主航班 `FLID`,待 `Q20`、`Q4`。 -- **INV-29** 已删除航班收到 `ADFT` 时按报文内容恢复为有效航班(`US-04` AC3):恢复走正常处理路径,推进版本、刷新投影并登记事件,不因已删除而跳过或拒绝。 -- **INV-31** 日计划以整包为完成单位:每批提交只是持久化进度,处理完成在整包写入完成且 Redis 按快照刷新成功后判定(`US-07` AC4/AC5);刷新在整包写入完成后才开始(架构「主流程」)。刷新期间查询可见的投影状态不作承诺,是否提供原子切换视图取决于 `Q20`。失败后整包重处理,收敛依赖版本只进不退与逐航班重写幂等(`INV-13`);日计划的逐航班重写幂等未补齐(`G-SCHD-SNAPSHOT`),闭合前收敛不可声明。 +- **INV-5** 航班当前态只以自有 PG 为准;信箱、Redis、Kafka、展示视图都不是。 +- **INV-6** `FLID` 唯一。 +- **INV-7** 日计划:快照里没有的航班标记删除、登记删除事件并从 Redis 去掉;报文没带的字段本地清掉(`US-07` AC2/AC3)。 +- **INV-8** 航班标成已删除后,要从 Redis 去掉;去掉成功才算这条消息处理完(`US-06` AC1)。 +- **INV-9** 日计划可以分批写库,但整份写完并且 Redis 按这份结果刷完,才算处理完;失败就整份重来(`US-07` AC4/AC5)。 -### 4.3 投影、投递、历史与参考数据 +### 4.3 Redis 投影 -- **INV-23** 航班类消息(增量、删除、日计划)的 Redis 投影写成功才算处理完成。失败分两类:PG 事务提交前失败,事务整体回滚、消息保持未完成(`US-03` AC3);投影写失败发生在事务提交后,不回滚已提交的业务结果,保持未完成、下轮重做投影与后续步骤(`US-05` AC4)。两类都不置终态、不回填、不投递。 -- **INV-24** Redis 航班投影只由本系统写入和移除;`GET /all/flights` 与网页客户端读同一份,返回当前全部动态航班(不含共享航班),不分页;Redis 异常时报错,不返回空列表伪装成功。 -- **INV-25** 处理记录清理:只删「已有终态且已回填」并超过保留期的记录,未完成的不删,保留期可配置;未映射字段的记录不随处理记录到期清理(`US-05` AC3)。 -- **INV-26** 静态参考数据按类别与编号保存,新消息覆盖旧记录;全量消息整体替换,增删改消息逐条处理;字段为空表示「当前没有值」,不是删除。 -- **INV-27** 一类参考数据校验不通过就只停这一类,其他类照常;该类的已有记录不变。 -- **INV-28** 航班只在历史写入确认成功后才从实时数据删除,删除前按 `D1` 必要时登记待发删除事件,历史存储未接通时一条也不删;写失败的下轮重来,已写入的不重复写入,单个航班失败不影响其他航班。与消息处理的互斥见 `INV-18`。 - -### 4.4 已作废条款 - -- **INV-2** 水位与入队同事务:不允许出现「水位已推进、消息未入队」的持久化状态;水位只增不减,遇空洞即停,只有判定为永久空洞才放行,且放行只跳过空洞本身、不越过任何已存在的行。**[作废 by INV-2b]** -- **INV-4** 只领取已发现的行:主泵只领 `MSG_ID ≤ W`;水位之外的行只可能来自兼容入口,必须等水位追平后按序处理。**[作废 by INV-2b]**(水位取消,兼容入口写入的行即普通行。) -- **INV-5** 发现与处理互不阻塞:收报只看 `ID > W`,不以处理标记为谓词;终态而未回填的行不阻断后续消息的发现。**[作废 by INV-2b]** -- **INV-11** 自有 PG 的航班当前态是唯一权威;信箱、Kafka、展示视图都不是权威。**[作废 by INV-11b]** -- **INV-14** 报文未携带的字段不被隐式清空;集合按完整合并结果写入,保留输入顺序与源序号。**[作废 by INV-14b]** -- **INV-15** 缺席于某个日计划不构成删除理由;删除只由 FDEL 或受控历史清理触发。**[作废 by INV-15b]** -- **INV-17** 状态变更、待发事件、处理终态与回填意图在同一 PG 事务内原子提交。**[作废 by INV-17b]** -- **INV-20** 处理器幂等:同一消息重复执行只产生一次业务效果。身份唯一只防「重复记录」,不防「重新执行」;SIS 25 类与经 `Q8` 定案启用的 legacy 子类型完成逐类幂等矩阵前,本条**不可声明**(`G-FLOP-IDEMPOTENT`)。**[作废 by INV-20b]** +- **INV-10** Redis 写成功,这条消息才算处理完;写失败就还没处理完,不回填、不发 Kafka,下轮再写(`US-05` AC4)。本地已经写进库的,不因为 Redis 失败而撤掉(`US-03` AC3)。 +- **INV-11** Redis 航班投影只由本系统维护;查询与网页客户端读同一份;Redis 异常时报错,不交空列表(`US-12`)。 ## 5. 声明边界 | 编号 | 承诺 | 依赖 | 现在能否作出 | 限制或原因 | |---|---|---|---|---| -| CLM-3 | 失败重处理与重复发现不产生重复业务副作用 | `INV-20b` | 不能 | 逐类幂等规则未补齐(`G-FLOP-IDEMPOTENT`) | -| CLM-4 | 信箱行最终都被写上处理标记 | `INV-7` | 不能 | 一直写不上的行留案并告警(`US-10` AC2) | -| CLM-6 | 单实例内按信箱编号升序的 FIFO 调度 | `PRE-5`、`INV-3` | 能 | 只声明按编号排序;「编号即到达顺序」依赖 `PRE-2`(`Q2`),未定案前不声明真实到达顺序 | -| CLM-7 | 主题 `msg` 上同一 `FLID` 内保序 | `INV-10`、`C-9`、`D2` | 不能 | 无按 `FLID` 的分区设计;`D2` 只保证单分区内生产顺序。跨 `FLID` 不承诺;不覆盖 `schd` | -| CLM-8 | 出站经 `COUTMSGS` | `C-4` | 能 | — | -| CLM-9 | 消息将在某个时限内完成处理 | — | 不能 | 处理标记的写入时刻不是完成时限:扫描周期、批次积压与单行重试都会延长实际延迟 | -| CLM-10 | 容量与吞吐量级 | — | 不能 | 无实测数据,取得信箱日量、峰值与单报文上限后重估 | -| CLM-11 | 配置不完整时拒绝启动;「双实例同时启动只允许一个处理」无系统侧机制承载 | `OPS-1`、`PRE-5` | 启动拒绝能;双实例验收不能 | 运行时跨实例互斥不在此版设计内——不引入并行主泵或分布式锁是非目标(requirements「范围与非目标」),单实例靠部署保证(`PRE-5`);达成方式待 `Q29` | +| CLM-1 | 同一消息再处理不会多出一份业务效果 | `US-03` | 不能 | 各类报文的细则未补齐(`G-FLOP-IDEMPOTENT`) | +| CLM-2 | 按信箱编号升序处理 | `US-03` AC1 | 能 | — | +| CLM-3 | 主题 `msg` 上同一 `FLID` 内按发送顺序保序 | `US-08` AC2、`C-9` | 能 | `msg` 单分区;至少一次重发时消费者仍可能见到乱序 | +| CLM-4 | 出站请求写入共享出站表 `COUTMSGS` | `C-4` | 能 | 只保证写入信箱,不保证 AODB 收到 | +| CLM-5 | 消息在固定时限内处理完 | — | 不能 | 需求未定完成时限;现场一般为即时处理,但不作时限保证 | +| CLM-6 | 容量与吞吐量级 | — | 不能 | 现场量级暂无;有日量、峰值等数据后再估 | +| CLM-7 | 配置不完整时拒绝启动 | `OPS-1` | 能 | — | ## 6. 待确认事项台账 @@ -147,13 +108,12 @@ | 编号 | 事项 | 当前假定 | 影响 | |---|---|---|---| -| Q2 | 信箱编号的分配顺序、单调与不复用 | 见 `PRE-2`、`PRE-3` | 「编号即到达顺序」目前无法确认 | +| Q2 | 信箱编号的分配顺序、单调与不复用 | 按编号升序处理,不声称是到达顺序 | 编号空间特性未确认,但不影响正确性 | | Q3 | 兼容入口的请求体上限、编码与 `Content-Type` 精确规则、HTTP 状态码、成功与失败响应结构 | 响应语义与解析限制已定(`C-7`);旧系统线索为共用 `ResponseDto`、成功 `body` 是信箱编号(接口契约「HTTP」) | 兼容入口无法验收 | -| Q4 | Kafka 载荷与去重标识 | 两个主题名、`msg` 单条变更、`schd` 定时批量已定(`US-08`;`C-9`)。无特别分区设计。`schd` 每条 record 装什么、批次边界未定 | `schd` 粒度及去重标识仍须确定 | +| Q4 | Kafka 载荷与去重标识 | 两个主题名、`msg` 单条变更、`schd` 定时批量已定(`US-08`;`C-9`)。`msg` 单分区,同一 `FLID` 按发送顺序保序可声明(`CLM-3`)。`schd` 每条 record 装什么、批次边界未定 | `schd` 粒度及去重标识仍须确定 | | Q7 | 处理时间列的列名、类型与可空性;信箱时钟基准与时区 | 处理标记即处理时间列;回填未完成前行须在,未完成消息原文须在(`C-1`) | 列元数据与时钟基准未定 | | Q8 | 现场会发但 SIS 未定义的子类型(靠桥、延误等)的报文形态与逐类终态 | 按现有处理逻辑延续(`US-05` AC1) | 逐类终态与幂等规则未定(`G-FLOP-SEMANTICS`) | | Q9 | 清除方案与保留期 | 本系统自行清理(`C-1`);旧系统按接收超过 1 天归档并删除入站行,是否沿用待确认 | 清除边界与保留期未定 | -| Q15 | 上游是否会以同一业务身份改发正文 | 假定不可变(`PRE-7`) | 身份去重语义未定 | | Q20 | Redis 投影的 key/value 结构、序列化方式与网页客户端读取约定 | 旧系统线索为 hash `flightInfo`、field 取 `FLID`(见接口契约「Redis:航班查询投影」) | 消费方读取契约无法定稿 | | Q24 | `GET /all/flights` 的返回字段与类型、集合外层结构、状态码与错误响应 | 读 Redis 投影、返回当前全部动态航班、不分页(`C-11`);旧系统线索为非共享航班 `SCHD.FLTR` 列表(接口契约「HTTP」) | 查询契约无法定稿 | | Q25 | `RQRD`/`RQFD` 的子类型清单、发送方取值、时间与序号的构造规则 | 出站只有这两类报文(架构「系统定位与范围」);旧系统线索为 `TYPE=RQFD`、`STYP=NONE`、`SNDR=OSH5`(接口契约「HTTP」) | 出站报文无法编码 | @@ -177,14 +137,13 @@ | Q1 | 生产库选型 | 自有 PG 是唯一权威;生产环境用 PostgreSQL 还是 Oracle 11g 不能从三份依据确定,Oracle 适配验证通过前不作支持承诺 | 生产部署验收 | | Q17 | `POST /schd/sync` 的请求字段与时间格式、成功响应表示已登记还是已落信、状态码与错误响应 | 交付承诺止于落信(`C-4`);旧系统线索为 `{startDate, endDate}` 与 12 小时制时间(接口契约「HTTP」) | 响应契约无法定稿(`C-8`) | | Q18 | 人工发起 `RQRD` 的方式 | `US-09` 要求人工发起,HTTP 接口清单没有对应入口 | 参考数据请求无法人工触发 | -| Q19 | `msg` 的版本与去重标识是否直接采用航班当前态的版本号 | 可用依据是 `FLIGHT_SCHD` 的版本规则(`INV-13`) | 消费方去重规则未定(与 `Q4` 衔接) | -| Q21 | 回退时在途消息(已提交业务变更、未到处理完成)的处置 | 回填了结后切换(`OPS-4`、`PRE-1`);在途消息无跨系统幂等保障 | 回退演练的验收口径(`OPS-4`) | +| Q19 | `msg` 的版本与去重标识是否直接采用航班当前态的版本号 | 可用依据见 implementation「航班域」与投递中的 `STATE_VERSION` | 消费方去重规则未定(与 `Q4` 衔接) | +| Q21 | 回退时在途消息(已提交业务变更、未到处理完成)的处置 | 回填了结后切换(`OPS-4`);在途消息无跨系统幂等保障 | 回退演练的验收口径(`OPS-4`) | | Q22 | 是否在自有 PG 留存入站原文副本,及原文提前清除时已登记消息的处置 | 不留存,原文只从信箱读取 | 提前清除的消息不可恢复,处置未定 | -| Q23 | 日计划中运营日冲突的处置 | 整包校验失败不落地(`INV-19`);冲突是否属于整包校验未定 | 冲突场景无法验收(`INV-12`) | +| Q23 | 日计划中运营日冲突的处置 | 未定 | 冲突场景无法验收 | | Q26 | `REF_MASTER` 的物理列、唯一键、空值存储与写入后可见时点 | 记录用类别码加识别标签识别(接口契约「静态参考数据类别与编号来源」) | admin-api 读取契约无法定稿(`C-10`) | -| Q27 | Elasticsearch 历史索引、文档 ID、字段映射、成功判据、保留期与容量上限、写入结果不明的对账与幂等策略 | 历史写入确认成功才删实时数据(`D1`、`INV-28`) | 历史链路无法验收(`US-14`;`G-FLIGHT-HIST-RETENTION`) | +| Q27 | Elasticsearch 历史索引、文档 ID、字段映射、成功判据、保留期与容量上限、写入结果不明的对账与幂等策略 | 历史写入确认成功才删实时数据(`D1`、`US-14` AC3) | 历史链路无法验收(`US-14`;`G-FLIGHT-HIST-RETENTION`) | | Q28 | `REQ_TRACK` 已结案记录的保留期取值 | 到期清理没有可依据的窗口(`G-REQ-TRACK-RETENTION`) | 请求历史清理无法实现(`US-09`) | -| Q29 | 「双实例同时启动只允许一个处理」的达成方式 | 由部署规程保证单实例(`CLM-11`);是否需要系统侧启动互斥未决 | `OPS-1` 双实例验收不闭环 | ## 7. 当前已知偏差 @@ -195,51 +154,49 @@ | `G-REQ-TRACK` | 出站请求没有跟踪:登记、编码、超时与应答匹配都没有实现 | `US-09` | | `G-REQ-OPEN-UNIQUE` | 同一报文类型同时最多一条已落信、未结案请求的限制没有实现;待发送登记不算占用该名额 | `US-09` | | `G-REQ-TRACK-RETENTION` | `REQ_TRACK` 已结案记录的保留期取值未定,到期清理作业没有可依据的窗口(取值待 `Q28`) | `US-09` | -| `G-FLIGHT-HIST-RETENTION` | 历史存储的保留期与容量上限未定(取值待 `Q27`) | `INV-28`;实时数据删除后历史是唯一副本 | -| `G-FLOP-IDEMPOTENT` | 逐类幂等规则未补齐 | `INV-20b`;`CLM-3` | -| `G-FLOP-SEMANTICS` | `STYP` 没有白名单,`ROUT` 未限制 4 条,运行状态落点与已删除航班的处理与 `US-05` 不符 | `US-05`;`INV-14b` | +| `G-FLIGHT-HIST-RETENTION` | 历史存储的保留期与容量上限未定(取值待 `Q27`) | `US-14`;实时数据删除后历史是唯一副本 | +| `G-FLOP-IDEMPOTENT` | 逐类幂等规则未补齐 | `US-03`;`CLM-1` | +| `G-FLOP-SEMANTICS` | `STYP` 没有白名单,`ROUT` 未限制 4 条,运行状态落点与已删除航班的处理与 `US-05` 不符 | `US-05` | | `G-FLOP-UNMAPPED` | [XSD](legacy/unisysaodbsis.xsd)「FLOP 元素」里有些字段没有解码或映射错了,会被静默丢掉 | `US-05` | -| `G-MAFL` | 主航班的共享航班列表未实现 | `INV-21` | +| `G-MAFL` | 主航班的共享航班列表未实现 | `US-06` AC2;`C-5` | | `G-SRVT-VIPF` | `SRVT`、`VIPF` 两个集合没有落到持久化明细 | `US-05` | -| `G-SCAN-PREDICATE` | 收报仍按水位扫描,不是按「处理时间为空」读取 | `US-01`;`INV-2b` | +| `G-SCAN-PREDICATE` | 收报仍按水位扫描,不是按「处理时间为空」读取 | `US-01`;`INV-1` | | `G-REDIS-PROJECTION` | Redis 投影没有写入与移除路径 | `US-05`、`US-06`、`US-07`、`US-12` | -| `G-SCHD-SNAPSHOT` | 日计划快照不删除缺席航班、不清除未携带字段,也没有分批;逐航班重写幂等未补齐 | `INV-15b`;`INV-17b`;`INV-31` | -| `G-PROC-CLEANUP` | 处理记录的到期清理作业未实现 | `US-11`;`INV-25` | +| `G-SCHD-SNAPSHOT` | 日计划快照不删除缺席航班、不清除未携带字段,也没有分批;逐航班重写幂等未补齐 | `INV-7`;`INV-3`;`INV-9` | +| `G-PROC-CLEANUP` | 处理记录的到期清理作业未实现 | `US-11` | | `G-REF-DATA` | 静态参考数据没有处理,当前按「合法但不支持」跳过并回填;参考数据表与 admin-api 直读未落地 | `US-13`;`US-03` | ## 8. 验证映射 | 不变量 | 需求验收 | 要观察的结果 | |---|---|---| -| INV-2b | `US-01` AC1/AC2/AC4 | 扫描重来与重启后登记数不变,行不丢 | -| INV-2b、C-7 | `US-02` AC1~AC5 | 兼容入口接受三种媒体类型、默认 UTF-8;空报文、超上限、非法 XML 不落信并返回错误,解析禁用外部实体与外部资源;成功返回编号且只表示落信;内网来源由网络层配置核对;写入的行与上游投递同路径被发现、登记 | -| INV-3 | `US-01` AC3、`US-03` AC1 | 队头未完成时,后面的消息不被处理;处理顺序为编号升序 | -| CLM-6、PRE-2 | `US-01` AC3 | 「编号即到达顺序」的到达顺序声称待 `PRE-2`(`Q2`)定案,当前只声明按编号排序(`CLM-6`) | -| INV-30 | `US-03` AC2/AC5 | 死信与跳过留档且无业务副作用;错误在对应处理记录上可查 | -| INV-6、INV-16 | `US-03` AC3 | 失败回滚后消息仍在未完成;已提交结果不被副作用回滚 | -| INV-7、INV-8 | `US-10` AC1/AC2 | 标记只写一次;重启后继续,写不上的有记录与告警 | -| INV-9、PRE-7 | `US-01` AC2、`US-03` AC5 | 同一编号重复出现时处理记录数不增加;相同业务身份、不同编号的行先比对内容——一致跳过留档,不一致留档、告警、不产生业务效果,处置待 `Q15` | -| INV-10、C-9 | `US-08` AC1/AC2/AC3 | `msg` 单条变更、`schd` 批量;失败重试后仍能投出;一直失败的记录保留可查并告警;`FLID` 端到端保序不可声明(无分区设计,见 `CLM-7`) | -| INV-11b | 架构「系统定位与范围」 | 航班当前态的权威写入只在自有 PG | -| INV-12、INV-13 | 架构「必须保持的约束」、`US-04` AC1 | 运营日写入后不变,没带计划时间的航班留空;重复消息不推进版本 | -| INV-14b | `US-04` AC2 | 未携带的字段保持原值 | -| INV-29 | `US-04` AC3 | 已删除航班收到 `ADFT` 后恢复有效并推进版本 | -| INV-15b | `US-07` AC2/AC3 | 缺席的航班在 PG 标为已删除并从 Redis 投影移除;未携带的字段被清空 | -| INV-17b | `US-03` AC4、`US-05` AC4、`US-06` AC1 | 投影写失败时,没有终态与回填意图落库;处理完成前事件不可投递 | -| INV-18 | `US-14` AC4 | 历史清理跳过正在被消息处理的航班 | -| INV-19 | `US-07` AC1 | 校验失败后本地数据与版本不变 | -| INV-31 | `US-07` AC4/AC5 | 分批失败后整包重处理收敛到同一目标(`G-SCHD-SNAPSHOT` 闭合前无法验证);Redis 在整包写入完成后按快照刷新,成功即与快照一致 | -| INV-20b、CLM-3 | `US-01` AC2、`US-03` AC3、`US-05` AC1/AC2 | 重复执行不增加业务效果;逐子类型规则与幂等矩阵在 `Q8`、`G-FLOP-IDEMPOTENT`、`G-FLOP-SEMANTICS` 闭合前无法验证 | -| INV-21、INV-22、INV-28 | `US-06` AC1/AC2、`US-14` AC3 | 航班标记删除后从投影移除;删除事件在实时数据删除前登记(`D1`),投递按至少一次随后完成;移除失败时下轮重做 | -| INV-23 | `US-05` AC4、`US-06` AC1 | 投影写失败的消息下轮仍被处理 | -| INV-24 | `US-12` AC1/AC2 | 返回全部非共享航班,且与 Redis 一致;Redis 故障时返回错误 | -| INV-25 | `US-11` AC1/AC2 | 未完成的记录不被删除 | -| INV-26、INV-27 | `US-13` AC1~AC5 | 全量消息整体替换、增删改消息逐条生效;字段空值保存为「当前没有值」而非删除;失败类别的已有记录不变;参考数据落独立数据表供 admin-api 只读(`C-10`) | -| INV-28 | `US-14` AC1~AC4 | 按判史条件与定时选出候选;历史写入失败的航班仍在实时数据中,单个失败不影响其他 | -| CLM-4(留案告警) | `US-10` AC2 | 一直写不上的行有记录与告警 | -| CLM-8 | `US-09` AC1~AC3 | 出站请求写入 `COUTMSGS` | -| CLM-9(不承诺完成时限) | `OPS-2` | 积压、处理失败、投递失败、回填失败各有指标与告警(架构「总体架构」) | -| CLM-10(容量假设) | 无对应运行验收 | 上线前用现场量级重估,取值见 [reference.md](reference.md)「参数注册表」 | +| INV-1 | `US-01` AC1/AC2/AC4 | 扫描重来与重启后登记数不变、行不丢;同一编号重复出现时处理记录数不增加 | +| INV-1、C-7 | `US-02` AC1~AC5 | 兼容入口接受三种媒体类型、默认 UTF-8;空报文、超上限、非法 XML 不落信并返回错误,解析禁用外部实体与外部资源;成功返回编号且只表示落信;内网来源由网络层配置核对;写入的行与上游投递同路径被发现、登记 | +| CLM-2 | `US-01` AC3、`US-03` AC1 | 按编号升序处理;队头未完成时后面的消息不被处理 | +| US-03 AC2/AC5 | `US-03` AC2/AC5 | 死信与跳过留档且无业务副作用;错误在对应处理记录上可查 | +| US-03 AC3 | `US-03` AC3 | 失败回滚后消息仍在未完成;已提交结果不被写信箱处理时间或发 Kafka 失败回滚 | +| INV-2 | `US-10` AC1/AC2 | 终态后才回填;重启后继续,写不上的有记录与告警 | +| C-9 | `US-08` AC1/AC2/AC3 | `msg` 单条变更、`schd` 批量;失败重试后仍能投出;一直失败的记录保留可查并告警;`msg` 同一 `FLID` 按发送顺序保序(`CLM-3`);同一条可能多发(`C-9`) | +| INV-5 | 架构「系统定位与范围」 | 航班当前态的权威写入只在自有 PG | +| INV-6 | 架构「必须保持的约束」 | `FLID` 唯一 | +| US-04 AC2 | `US-04` AC2 | 未携带的字段保持原值 | +| INV-7 | `US-07` AC2/AC3 | 缺席的航班在 PG 标为已删除并从 Redis 投影移除;未携带的字段被清空 | +| INV-3 | `US-03` AC4、`US-05` AC4、`US-06` AC1 | 投影写失败时,没有终态与回填意图落库;处理完成前事件不可投递 | +| US-14 AC4 | `US-14` AC4 | 历史清理跳过正在被消息处理的航班 | +| INV-4 | `US-07` AC1 | 校验失败后本地数据与版本不变 | +| INV-9 | `US-07` AC4/AC5 | 分批失败后整包重处理收敛到同一目标(`G-SCHD-SNAPSHOT` 闭合前无法验证);Redis 在整包写入完成后按快照刷新,成功即与快照一致 | +| CLM-1 | `US-03` | 同一消息不产生两次效果;逐子类型规则与幂等矩阵在 `Q8`、`G-FLOP-IDEMPOTENT`、`G-FLOP-SEMANTICS` 闭合前无法验证 | +| INV-8 | `US-06` AC1 | 航班标记删除后从投影移除;移除失败时下轮重做 | +| US-14、D1 | `US-14` AC3 | 历史写入成功才删实时数据;删除事件在实时数据删除前登记(`D1`) | +| US-06 AC2 | `US-06` AC2 | 删共享联动主航班;删主航班级联删共享 | +| INV-10 | `US-05` AC4、`US-06` AC1 | 投影写失败的消息下轮仍被处理 | +| INV-11 | `US-12` AC1/AC2 | 返回全部非共享航班,且与 Redis 一致;Redis 故障时返回错误 | +| US-11 | `US-11` AC1/AC2 | 未完成的记录不被删除 | +| US-13 | `US-13` AC1~AC5 | 全量整体替换、增删改逐条;空值是「没有值」不是删除;一类校验失败只停该类;参考数据落独立表供 admin-api 只读(`C-10`) | +| US-10 AC2 | `US-10` AC2 | 写失败重试与告警可观测 | +| CLM-4 | `US-09` AC1~AC3 | 出站请求写入 `COUTMSGS` | +| CLM-5(不承诺完成时限) | — | 需求未定完成时限;现场一般为即时处理 | +| CLM-6(容量) | — | 现场量级暂无;有数据后再估 | | OPS-4 | `OPS-4` | 回退演练:回填了结后切回,旧系统不重处理已产生业务效果的消息;在途窗口处置见 `Q21` | -| CLM-11(单实例防线) | `OPS-1` | 配置错误启动失败测试可验收;「双实例同时启动只允许一个处理」未闭环——系统无运行时互斥,达成方式待 `Q29` | -| PRE-9(测试隔离) | `OPS-3` | 测试环境配置核对:独立的数据库、Redis 与 Kafka 主题,不连接生产信箱 | +| CLM-7 | `OPS-1` | 配置错误启动失败测试可验收 | +| PRE-1(测试隔离) | `OPS-3` | 测试环境配置核对:独立的数据库、Redis 与 Kafka 主题,不连接生产信箱 | diff --git a/src/main/kotlin/com/gzzn/omms/msgexchange/infra/kafka/KafkaDeliveryPort.kt b/src/main/kotlin/com/gzzn/omms/msgexchange/infra/kafka/KafkaDeliveryPort.kt index b98307c..6826f98 100644 --- a/src/main/kotlin/com/gzzn/omms/msgexchange/infra/kafka/KafkaDeliveryPort.kt +++ b/src/main/kotlin/com/gzzn/omms/msgexchange/infra/kafka/KafkaDeliveryPort.kt @@ -12,7 +12,7 @@ import org.apache.kafka.clients.producer.ProducerRecord * Kafka 真实投递端口:通过 [ProducerRegistry] 拿 default 生产者,D2 参数 * (acks=all / enable-idempotence=true / max-in-flight=1)由 `KafkaD3Check` 启动自检。 * - * 发送同步等 broker 确认(`Future.get()`),语义是至少一次——满足 INV-10 / C-29。 + * 发送同步等 broker 确认(`Future.get()`),语义是至少一次——满足 C-9 / US-08。 * 仅在 `msgx.stubs != true` 时装配;stub 模式走 `StubDeliveryPort`。 */ @Requires(property = "msgx.stubs", notEquals = "true") diff --git a/src/main/kotlin/com/gzzn/omms/msgexchange/ingress/InboxService.kt b/src/main/kotlin/com/gzzn/omms/msgexchange/ingress/InboxService.kt index b9567f1..191c2e3 100644 --- a/src/main/kotlin/com/gzzn/omms/msgexchange/ingress/InboxService.kt +++ b/src/main/kotlin/com/gzzn/omms/msgexchange/ingress/InboxService.kt @@ -31,7 +31,7 @@ class InboxService( data class Receipt(val msgId: Long, val receivedAt: Instant) fun accept(rawXml: String): Receipt { - // 接收结果边界 = insertRaw 是否成功返回 ID(C-28 只承诺到落信)。只有这一步失败才返回失败; + // 接收结果边界 = insertRaw 是否成功返回 ID(C-8 只承诺到落信)。只有这一步失败才返回失败; // 之后的读取/入队失败若冒泡成 HTTP 失败,客户端重试会在信箱里多写一行。 val id = inbox.insertRaw(rawXml) val now = clock.instant() @@ -49,7 +49,7 @@ class InboxService( // 入队时间单独传本地时钟:它是超期判据 R 的比较对象,不能借用库方时间(PRE-4)。 procState.insertIfAbsent(id, receivedAt, enqueuedAt = now) } catch (e: Exception) { - // PG 入队失败不回退接收结果:原文已在信箱,轮询会按 ID 补建,且只建一条(INV-1、US-01)。 + // PG 入队失败不回退接收结果:原文已在信箱,轮询会按 ID 补建,且只建一条(INV-2b、US-01)。 pgEnqueueFailures.incrementAndGet() log.error("compat accepted but PG enqueue failed msgId={}; poller will re-create", id, e) } diff --git a/src/main/resources/db/migration/V1__flight_state_baseline.sql b/src/main/resources/db/migration/V1__flight_state_baseline.sql index fe14de9..50c1109 100644 --- a/src/main/resources/db/migration/V1__flight_state_baseline.sql +++ b/src/main/resources/db/migration/V1__flight_state_baseline.sql @@ -36,22 +36,22 @@ CREATE TABLE PIPELINE_LOCK ( INSERT INTO PIPELINE_LOCK (LOCK_ID, UPDATED_AT) VALUES (1, now()); -- ② 处理伴生状态:与共享信箱 CMINMSGS 一一对应(MSG_ID = CMINMSGS_ID)。 --- 一信一行、一身份一记录(INV-9);MSG_ID 已有成功终态即判重放,直接记幂等成功。 +-- MSG_ID 主键(INV-2b);IDENTITY_KEY 唯一(implementation「消息、身份与决策」);MSG_ID 已有成功终态即判重放,直接记幂等成功。 -- 处理终态与回填意图在本表同表同行:不存在第二处落账,也不存在独立的回填待办表(INV-8)。 --- 状态不可逆:已提交的 SUCCEEDED 不因回填或投递失败回改(INV-6)。 +-- 已提交的 SUCCEEDED 不因写信箱处理时间或发 Kafka 失败回改(US-03 AC3)。 CREATE TABLE PROC_STATE ( MSG_ID BIGINT NOT NULL PRIMARY KEY, STATE VARCHAR(16) NOT NULL, -- PENDING/FAILED/SUCCEEDED/SKIPPED/DEAD IDENTITY_KEY VARCHAR(200), -- SNDR|TYPE|STYP|SEQN,解码后首次绑定 ATTEMPTS INT NOT NULL DEFAULT 0, - NEXT_ATTEMPT_AT TIMESTAMP(6) WITH TIME ZONE, -- 退避到期时刻;未到期不得被后续消息越过(INV-3) + NEXT_ATTEMPT_AT TIMESTAMP(6) WITH TIME ZONE, -- 退避到期时刻;未到期不得被后续消息越过(US-03 AC1) ERROR_CLASS VARCHAR(20), -- MALFORMED/PROTOCOL/INFRA/UNSUPPORTED/EXHAUSTED LAST_ERROR VARCHAR(1000), UPDATED_AT TIMESTAMP(6) WITH TIME ZONE NOT NULL, -- ==== 时间基准 ==== RECEIVED_AT TIMESTAMP(6) WITH TIME ZONE, -- 复制自信箱 DATE_RECEIVED:库方时钟、可为 NULL,仅用于对账与展示(PRE-4) ENQUEUED_AT TIMESTAMP(6) WITH TIME ZONE NOT NULL DEFAULT now(), -- 本地入队时间:非空、与判据 NOW 同源,是超期期限 R 的唯一比较对象(PRE-4、CLM-4) - -- ==== 回填事实(INV-7/INV-8)==== + -- ==== 回填事实(INV-8;写规则见 US-10)==== BACKFILL_AT TIMESTAMP(6) WITH TIME ZONE, -- 非空 = 处理标记已确认写入 BACKFILL_NEXT_AT TIMESTAMP(6) WITH TIME ZONE, -- 非空 = 还欠一次回填;与终态同语句写下 BACKFILL_ATTEMPTS INT NOT NULL DEFAULT 0, @@ -60,7 +60,7 @@ CREATE TABLE PROC_STATE ( BACKFILL_ABANDONED_REASON VARCHAR(64), -- MISSING_ROW(确定性)/ TRANSIENT_DEADLINE(暂时性耗尽 R) CONSTRAINT uk_proc_identity UNIQUE (IDENTITY_KEY) ); -CREATE INDEX idx_proc_head ON PROC_STATE (STATE, MSG_ID); -- 主泵队头查询(严格 FIFO,INV-3) +CREATE INDEX idx_proc_head ON PROC_STATE (STATE, MSG_ID); -- 主泵队头查询(US-03 AC1) -- 回填扫描:只覆盖仍需自动回填的行;按 (ATTEMPTS, MSG_ID) 轮转,避免最旧失败行长期占满批次。 CREATE INDEX idx_proc_backfill_due ON PROC_STATE (BACKFILL_ATTEMPTS, MSG_ID) WHERE BACKFILL_AT IS NULL AND BACKFILL_ABANDONED_AT IS NULL; @@ -85,7 +85,7 @@ CREATE TABLE FLIGHT_SCHD ( FLID VARCHAR(32) NOT NULL PRIMARY KEY, -- AODB 实例 ID(Number(1-12));不得由航班号或资源号推断 OPERATION_DAY DATE NULL, -- 运营保障日:未由日计划收录时为 NULL;非空后不可改变(INV-12) STATE VARCHAR(8) NOT NULL, -- ACTIVE/DELETED,无第三态;删除只由 FDEL 或受控历史清理触发(INV-15) - STATE_VERSION BIGINT NOT NULL DEFAULT 0, -- 每次成功写入 +1,重复消息不重复推进(INV-13) + STATE_VERSION BIGINT NOT NULL DEFAULT 0, -- 航班写代次;成功写入时推进 LAST_MSG_ID BIGINT NULL, -- 最近一次成功写入的消息 ID -- ==== SCHD.FLTR 标量字段(保持 AODB 字符串原样;未携带不隐式清空,INV-14)==== ALCD VARCHAR(64) NULL, -- 航空公司代码 @@ -278,7 +278,7 @@ CREATE INDEX idx_flight_delay_flid ON FLIGHT_DELAY (FLID); CREATE INDEX idx_flight_bridge_flid ON FLIGHT_BRIDGE_OP (FLID); CREATE INDEX idx_flight_chock_flid ON FLIGHT_CHOCK_OP (FLID); --- ⑥ 统一投递事件 outbox(INV-10、INV-17):KAFKA:schd 发整态、KAFKA:msg 只通知变化; +-- ⑥ 统一投递事件 outbox(C-9、INV-17b):KAFKA:schd 发整态、KAFKA:msg 只通知变化; -- tombstone 仅在 ACTIVE→DELETED 时与删除同事务登记,投递失败按退避持续重试。 -- 目标级全序投递是当前实现,按 FLID 保序见 CLM-7。 CREATE TABLE MSG_EVENT ( diff --git a/src/test/kotlin/com/gzzn/omms/msgexchange/infra/retry/ProcFailureTest.kt b/src/test/kotlin/com/gzzn/omms/msgexchange/infra/retry/ProcFailureTest.kt index 34fc2c6..86fd047 100644 --- a/src/test/kotlin/com/gzzn/omms/msgexchange/infra/retry/ProcFailureTest.kt +++ b/src/test/kotlin/com/gzzn/omms/msgexchange/infra/retry/ProcFailureTest.kt @@ -13,7 +13,7 @@ import java.time.Instant /** * 处理失败的落账规矩:没到尝试上限才写 FAILED 退避重试;一到上限立刻写 DEAD(EXHAUSTED) 终态, - * 不允许同一次失败调用再把它覆写回 FAILED(INV-6 终态不可逆、INV-8 终态与回填意图同一次写入)。 + * 不允许同一次失败调用再把它覆写回 FAILED(US-03 AC3、INV-8 本地结束与「还要更新信箱」同一次写入)。 */ class ProcFailureTest { diff --git a/src/test/kotlin/com/gzzn/omms/msgexchange/ingress/InboxServiceTest.kt b/src/test/kotlin/com/gzzn/omms/msgexchange/ingress/InboxServiceTest.kt index 6ad907c..4e5b0fa 100644 --- a/src/test/kotlin/com/gzzn/omms/msgexchange/ingress/InboxServiceTest.kt +++ b/src/test/kotlin/com/gzzn/omms/msgexchange/ingress/InboxServiceTest.kt @@ -18,7 +18,7 @@ import org.junit.jupiter.api.Test import java.time.Instant /** - * 兼容入口的接收边界(`C-28`、`INV-1`):`insertRaw` 返回 ID 即"已落信"。 + * 兼容入口的接收边界(`C-8`):`insertRaw` 返回 ID 即"已落信"。 * 此后的接收时间读取失败或 PG 入队失败都只留痕/计数,不改接收结果——否则客户端重试会在 * 信箱里多写一行;缺的 `PROC_STATE` 由收报轮询按 `US-01` 补建,且只建一条。 */ diff --git a/src/test/kotlin/com/gzzn/omms/msgexchange/processing/BackfillServiceTest.kt b/src/test/kotlin/com/gzzn/omms/msgexchange/processing/BackfillServiceTest.kt index c6d3cfc..266d6ab 100644 --- a/src/test/kotlin/com/gzzn/omms/msgexchange/processing/BackfillServiceTest.kt +++ b/src/test/kotlin/com/gzzn/omms/msgexchange/processing/BackfillServiceTest.kt @@ -364,7 +364,7 @@ class BackfillServiceTest { } /** - * `INV-7`:超期判据是**本地入队时间** `ENQUEUED_AT`,与 `RECEIVED_AT` 无关。 + * 超期判据是**本地入队时间** `ENQUEUED_AT`,与 `RECEIVED_AT` 无关。 * 上游没给接收时间(NULL)不再让 `R` 兜底失效——这正是引入 `ENQUEUED_AT` 要消除的窗口。 */ @Test