diff --git a/docs/README.md b/docs/README.md index e5199ed..4f1f9fe 100644 --- a/docs/README.md +++ b/docs/README.md @@ -10,7 +10,7 @@ | [requirements.md](requirements.md) | 阶段范围与非目标、`US-xx` / `OPS-x` 验收目标、需求覆盖与依赖。 | | [architecture.md](architecture.md) | 系统边界、模块职责、存储归属、总体流程与 `D1`–`D2` 决策。 | | [specification.md](specification.md) | 术语、对外约定 `C-x`、前提 `PRE-x`、不变量 `INV-x`、声明边界 `CLM-x`、待确认 `Qn`、当前已知偏差 `G-NAME`、验证映射。 | -| [implementation.md](implementation.md) | 数据模型、状态机、管道机制、事务、投递、作业与恢复;航班域与静态参考数据的权威模型和合并语义。 | +| [implementation.md](implementation.md) | 数据模型、状态机、管道机制、事务、投递、作业与恢复;航班域与静态参考数据的合并规则。 | | [reference.md](reference.md) | 参数 `PARAM:`、指标与健康、模块与代码入口、错误分类。 | | [legacy/](legacy/) | 外部协议与旧系统基线:现役行为对拍、[SIS 规范](legacy/SIS_AODB_RMS-V0.1.md)、[XSD](legacy/unisysaodbsis.xsd)、[历史决策记录](legacy/decision-flight-state-history.md)。外部协议事实(报文结构、字段语义、上游行为)以 SIS/XSD 为准;legacy 现役行为只是基线,已知缺陷不作依据。 | @@ -27,7 +27,7 @@ | 事实 | 唯一归属 | 其他文档怎么写 | |---|---|---| -| 收报扫描谓词与幂等登记 | implementation.md「收报」 | specification.md 写对库方的承诺 `INV-1`;reference.md 写参数 | +| 收报扫描谓词与幂等建立处理记录 | implementation.md「收报」 | specification.md 写对库方的承诺 `INV-1`;reference.md 写参数 | | 保留期下界 `R_keep`、清除前置条件 | specification.md「约定」 | implementation.md 只写行为约束;执行步骤在上线前另立 | | 处理标记(处理时间)写权限 | specification.md 术语「处理标记」;`US-10` | implementation.md「回填」(只填空值、不覆盖) | | 回填四结果、放弃语义、`R` 的作用 | implementation.md「回填」 | specification.md 记结论与可声明性 | @@ -36,8 +36,8 @@ | 航班身份、合并语义、`OPERATION_DAY` | implementation.md「航班域」 | 其余文档只引域规则与 `INV-x` | | 航班动态逐类语义与空标签规则(FLOP) | implementation.md「动态运行事件」 | specification.md 记 `Q3` 与 `G-*`;requirements.md 写验收口径 | | SIS 消息中的参考数据类别、结构与资源状态 | implementation.md「静态参考数据」 | requirements.md 写取数与刷新验收;admin-api 只从处理后的业务数据库读取 | -| 对外术语(落信 / 入站 / 库方 / 处理标记) | specification.md「术语」 | — | -| 管道内部术语(队头 / 终态 / 待标记 / 回填意图) | implementation.md「术语与持久化记录」 | — | +| 对外术语(写入信箱 / 建立处理记录 / 库方 / 处理标记) | specification.md「术语」 | — | +| 管道内部术语(队头 / 终态 / 回填意图) | implementation.md「术语与持久化记录」 | — | ## 4. ID 定义语法与引用纪律 diff --git a/docs/implementation.md b/docs/implementation.md index 3766672..4115d47 100644 --- a/docs/implementation.md +++ b/docs/implementation.md @@ -1,12 +1,10 @@ # 实现设计 -本文件是实现设计的唯一出处,分三章: +msgexchange-v2 怎么处理报文:记录模型、状态机、事务边界、航班与静态参考数据合并规则。 -- **处理管道**章:记录模型、状态机、收报与扫描谓词、主泵与事务边界、回填、快照与请求、投递、失败恢复与维护作业; -- **航班域**章:航班当前态的权威模型、合并与写入语义、删除与重建; -- **静态参考数据**章:SIS 消息中的主数据类别与编码、`REF_MASTER` 结构与合并语义、资源状态,以及 admin-api 的下游读取边界。 +对外术语见 [specification.md](specification.md)「术语」;本文保留管道内部说法(队头、终态、回填意图)。参数见 [reference.md](reference.md) `PARAM:<键>`;不变量见 `INV-x`/`C-x`;能否对外承诺见 specification「声明边界」。不写交付进度。 -正文描述**目标设计**,不标注交付状态:可声明性见 [specification.md](specification.md)「声明边界」与「当前已知偏差」,进度在 Plane(ACM2)。契约与不变量只引稳定 ID;参数取值只引 `PARAM:<完整键>`(见 [reference.md](reference.md))。 +三块:**管道**(收报→主泵→写回→Kafka)、**航班域**(PG + Redis 快照)、**静态参考数据**(`REF_MASTER`)。 ## 1. 术语与持久化记录 @@ -14,36 +12,45 @@ | 术语 | 语义 | |---|---| -| 扫描谓词 | 信箱读取条件「处理时间为空」(`INV-1`);本系统不以 ID 区间或水位作为消费边界。 | -| 队头 | 最小的未完成消息(`PENDING` 与 `FAILED` 都占位)。 | -| 终态 | `SUCCEEDED` / `SKIPPED` / `DEAD`;到达后队列方可推进。 | -| 回填意图 | 「还欠一次信箱标记」的持久化事实,与终态同一条语句落库,且发生在 Redis 投影写成功之后(`INV-10`)。 | +| 扫描谓词 | 读信箱时只取「处理时间为空」的行(`INV-1`);不用 ID 区间或水位当消费边界。 | +| 队头 | 编号最小的未完成消息(`PENDING` 与 `FAILED` 都占位)。 | +| 终态 | `SUCCEEDED` / `SKIPPED` / `DEAD`;到达后后面的消息才能处理。 | +| 回填 / 回填意图 | 写回处理时间的实现机制([specification.md](specification.md)「写回处理时间」)。`BACKFILL_*` 列记录进度;`BACKFILL_NEXT_AT` 非空 = 还欠一次写回。意图与终态同一条语句落库,且在 Redis 航班快照写成功之后(`INV-10`)。 | | `R`、`R_keep`、处理标记 | 定义见 [specification.md](specification.md)。 | ### 1.2 持久化记录 | 记录 | 用途 | 关键约束 | |---|---|---| -| `PROC_STATE` | 入站消息的处理状态、身份、尝试次数、错误原因与回填事实 | `MSG_ID = CMINMSGS_ID` 主键防重复入队;`IDENTITY_KEY` 唯一约束防业务重复;按最小未完成 `MSG_ID` 取队头;`BACKFILL_NEXT_AT` 非空 = 还欠一次回填,`BACKFILL_AT` 非空 = 标记已确认,`BACKFILL_ABANDONED_AT/REASON` 非空 = 已停止自动重试(**不等于**标记已确认);`RECEIVED_AT` 复制自信箱接收时间、**可能为 NULL**、仅用于对账与展示;`ENQUEUED_AT` 是本地入队时间、非空、是超期判据的唯一依据。 | -| `MSG_EVENT` | 等待投递的事件(outbox) | `EVENT_ID` 对 `KAFKA:msg` 是稳定事件身份并决定投递顺序;对 `KAFKA:schd` 是每次接受 upsert 时替换的写代次。`TARGET` 区分 `KAFKA:msg` / `KAFKA:schd`;`PARTITION_KEY` 当前取 `FLID`,`msg` 单分区下不参与路由(`CLM-3`);`EVENT_TYPE` 区分 UPSERT 与 TOMBSTONE。`KAFKA:schd` 按 `FLID` 单行 upsert,只保留最新 `STATE_VERSION`;`SENT_AT` 在投递确认的同一条 UPDATE 内写入,是保留期判定的唯一基准。 | -| `REQ_TRACK` | 上游请求及应答关联 | 状态 `PENDING / SENT / DONE / EXPIRED / FAILED`;保存请求类型、覆盖运营日、出站信箱 ID 与发送/完成时间。发送方恒为 `OMMS`(`C-4`),不进开放唯一键。开放唯一键 = **`RQRD` \| `RQFD` 两类各一条**,仅开放态(`PENDING`/`SENT`)生效(`US-09` AC1);**不是** 14 类参考数据 `STYP` 各一条。同一子类型发新请求时旧请求作废,属流程,不是唯一键。`FAILED`/`DONE`/`EXPIRED` 关闭开放槽。 | +| `PROC_STATE` | 消息处理状态、身份、重试、错误、回填 | `MSG_ID` 主键;`IDENTITY_KEY` 唯一;最小未完成 `MSG_ID` = 队头。`BACKFILL_NEXT_AT` 非空 = 待写回;`BACKFILL_AT` 非空 = 已写回;`BACKFILL_ABANDONED_*` 非空 = 停自动重试(≠ 已写回)。`ENQUEUED_AT` 非空,超期只看它;`RECEIVED_AT` 可 NULL,仅对账。 | +| `MSG_EVENT` | 待发 Kafka 事件(outbox) | `TARGET` = `KAFKA:msg` / `KAFKA:schd`。`msg`:`EVENT_ID` 稳定、保序。`schd`:按 `FLID` 单行 upsert,每次更新换 `EVENT_ID`。`SENT_AT` 与投递确认同事务写入,保留期从它起算。 | +| `REQ_TRACK` | 出站请求与应答 | 状态 `PENDING/SENT/DONE/EXPIRED/FAILED`。开放槽:`RQRD` 与 `RQFD` 各一条(`US-09` AC1),不是 14 类 `STYP` 各一条。`OMMS` 发送(`C-4`)。结案态关闭槽。 | | `REF_MASTER` | SIS 消息提供的静态参考数据与资源状态的逻辑视图(物理为独立数据表组) | `(RTYPE, RKEY)` 唯一;`RTYPE` 类别、合并语义与资源状态见「静态参考数据」;取数路径见 [requirements.md](requirements.md) `US-13`。 | | `FLIGHT_SCHD` | 航班标量及单值异常字段 | `FLID` 主键;运营日与版本、最近消息 ID 用于追踪。变长集合存于资源明细表与 `FLIGHT_ROUTE_POINT`,规则见「航班域」。 | | `SCHD_SNAP_LOG` | 日计划处理留痕 | 只追加、可重建,不参与状态决策;保留期见 [reference.md](reference.md)。 | -字段与索引以 `src/main/resources/db/migration/` 的迁移链为准(Oracle 11g 目录为占位,未接入 Flyway)。报文原文仍从共享信箱读取;信箱清理见 `C-1`。 +字段与索引以 `src/main/resources/db/migration/` 为准。原文读共享信箱;清理见 `C-1`。 ## 2. 消息、身份与决策 -`XmlCodec`(实装 `JacksonXmlCodec`)把 XML 解码为 `DecodedMessage`,包含 `SNDR / TYPE / STYP / SEQN / DTTM` 元数据、`MsgKind` 与业务载荷。解码失败区分 `MALFORMED`(报文非法,不重试)与可随 codec 修复的 `CODEC_ERROR`。`MsgKind` 是一等分派键:`Schd(RESP/DNLD/ADFT)`、`Flop`、`Fdel`、`RefData`(`SIS:3.1`~`SIS:3.15`)、`Unsupported`。 +`XmlCodec` 解码 XML → `DecodedMessage`(元数据 + `MsgKind` + 载荷)。`MALFORMED` 不重试;`CODEC_ERROR` 可退避重试。分派键:`Schd`、`Flop`、`Fdel`、`RefData`(`SIS:3.1`~`3.15`)、`Unsupported`。 -业务身份统一由 `Identity.of` 生成:`SNDR | TYPE | STYP | SEQN`。接收时只按信箱 ID 去重,解码后才首次绑定业务身份;重试保留原有绑定,因此自身重试不会被判为重复。身份被另一条记录占用时,当前消息转 `SKIPPED`,记录 `duplicate-of:`。是否加入日期边界取决于上游 `SEQN` 重置周期(见 `C-3`/`Q10` 与 `PARAM:msgx.identity.include-day-boundary`);上线后不能随意更换身份算法。 +身份 `Identity.of` = `SNDR|TYPE|STYP|SEQN`(`C-3`)。入队只按信箱 ID 去重;解码后首次绑定,重试不改绑定。占用 → `SKIPPED(duplicate-of:)`。日期边界见 `PARAM:msgx.identity.include-day-boundary`;上线后不可换算法。绑定是独立单语句(`IDENTITY_KEY IS NULL`),只认身份不比正文。 -**身份绑定是独立的幂等单语句**(`WHERE IDENTITY_KEY IS NULL`),不参与业务事务。它的前提是「报文不可变」(`Q4`):同一身份的重发不会被比对内容,若上游改发正文会被判为重复并跳过(`Q4`)。 +`MessageProcessor` 按 `MsgKind` 分派队头消息: -分派与落库由 `MessageProcessor` 协调:按 `MsgKind` 把已绑定身份的队头消息交给对应事务协调器(SCHD-DNLD/RESP → `ScheduleProcessor`,ADFT → `AdftProcessor`,FLOP → `FlopProcessor`,FDEL → `FdelProcessor`,`SIS:3.1`~`SIS:3.14` 落库与 `SIS:3.15` 应答配对 → `ReferenceDataProcessor`(`3.15` 是 RESP 配对路径,不是第 15 个 `RTYPE`),其余 → `SKIPPED(unsupported)`)。这些处理器在 `PIPELINE_LOCK` 事务内读取当前完整态,调用纯领域决策逻辑得到下一完整态与待发事件,提交该事务后写 Redis 投影,再在另一事务中登记处理终态与回填意图(`INV-3`、`INV-10`);它们不直接触碰 Kafka。领域决策逻辑不执行 I/O。处理步骤的锁跨越 Redis 写,使与航班历史清理的协作覆盖整个步骤(`US-14` AC4)。 +| `MsgKind` | 处理器 | +|---|---| +| SCHD-DNLD/RESP | `ScheduleProcessor` | +| SCHD-ADFT | `AdftProcessor` | +| FLOP | `FlopProcessor` | +| FDEL | `FdelProcessor` | +| RefData 3.1~3.14 / 3.15 配对 | `ReferenceDataProcessor` | +| 其余 | `SKIPPED(unsupported)` | -合法但本系统不支持的消息类型:跳过留档、按已处理写回标记(`US-03` AC2),不重试。`REGN` / `RSTA` 是静态参考数据消息,必须分派给 `US-13`,不得跳过。 +处理器在 `PIPELINE_LOCK` 内读态、算下一态与待发事件 → 提交 → 写 Redis → 另事务写终态与回填意图(`INV-3`、`INV-10`)。不直接发 Kafka。锁跨 Redis 写,覆盖历史清理协作(`US-14` AC4)。 + +不支持类型:跳过留档(`US-03` AC2)。`REGN`/`RSTA` 必须走 `US-13`,不得跳过。 ## 3. 状态与错误分类 @@ -55,52 +62,37 @@ 投递:PENDING → SENT → PENDING(退避后重试) - → DEAD(重试耗尽,记录保留作 DLQ) + → DEAD(重试耗尽,记录保留作死信) ``` -`SUCCEEDED / SKIPPED / DEAD` 是处理终态,不再阻塞后续消息;`FAILED` 不是终态,仍占据队头。`DEAD` 表示需要处置,不等于业务成功。 - -重试次数用尽时统一转 `DEAD(EXHAUSTED)`:`ERROR_CLASS` 被覆写为 `EXHAUSTED`,原始错误类别不再保留(`LAST_ERROR` 保留原因文本)。重放白名单与错误分类表见 [reference.md](reference.md)「错误分类与重放白名单」。 +`SUCCEEDED`/`SKIPPED`/`DEAD` 是终态,不挡后续;`FAILED` 仍占队头。用尽重试 → `DEAD(EXHAUSTED)`。错误分类与重放白名单见 [reference.md](reference.md)。 ## 4. 收报 -### 4.1 收报流程 +### 4.1 收报 -`InboxPoller` 按配置周期查信箱中「处理时间为空」的行,按编号升序、每批有上限,在自有 PG 登记 `PENDING`(`INV-1`)。每轮: +`InboxPoller` 周期扫描「处理时间为空」的行,升序、每批 `PARAM:msgx.pipeline.claim-batch` 条,在 PG 建 `PENDING` 记录(`INV-1`): -1. 信箱不可读时记日志、等下一轮——这是基础设施失败,不能当成「没有新消息」。 -2. 取「处理时间为空」的行的升序前 `PARAM:msgx.pipeline.claim-batch` 条。 -3. 在同一个 PG 事务内对每一行 `insertIfAbsent(MSG_ID, RECEIVED_AT, ENQUEUED_AT)`;主键冲突表示已登记(重复扫描与兼容入口并发都安全),不计入、不报错。 -4. 提交。已打标的行不再出现在扫描结果里;终态但未回填的行会被重复读到,按已有记录幂等跳过。 +1. 信箱不可读 → 记日志,下轮再试(不是「无新消息」)。 +2. 同事务 `insertIfAbsent(MSG_ID, RECEIVED_AT, ENQUEUED_AT)`;冲突 = 已有记录,跳过。 +3. 已写回处理时间的行不再出现;终态未写回的行会重复扫到,幂等跳过。 -**代价(必须接受并观测)**:回填延迟期内同一行会被反复读到并幂等跳过,扫描量随「终态未回填」行数增长。运行期必须观测扫描量与积压(指标见 [reference.md](reference.md))。 +写回延迟期间扫描量会涨,须观测积压([reference.md](reference.md) 指标)。 -### 4.2 顺序依据 - -编号即到达顺序的前提已定案(`Q7`):ID 按提交顺序分配、空间不复位不复用。较小编号迟提交的最坏结果是被发现得晚(下一轮扫描仍会读到),不会丢。 - -### 4.3 兼容 HTTP 入口 - -`POST /cminmsgs/send` 把报文写入共享信箱(处理时间为空),效果与上游投递一致:由收报扫描发现、登记、处理。客户端失败重试可能再次写信箱,业务身份去重仍然必需。响应语义见 `C-7`。 - -### 4.4 单实例 - -信箱读取不加锁,本设计只在单活动实例下成立(`OPS-1`)。多实例并发扫描会重复登记同一行,主键幂等兜底不会丢消息,但实例级排他仍是前提。 +信箱 ID 单调不复用(`INV-1`)。`POST /cminmsgs/send` 与 adapter 同路径(`C-7`)。单活动实例(`OPS-1`);多实例靠主键幂等,仍须单实例排他。 ## 5. 主泵调度与单条处理 ### 5.1 调度 -每次 `Pump.tick` 只围绕最小未完成消息: +`Pump.tick` 只处理队头(最小未完成 `MSG_ID`): -1. 无队头:按轮询间隔休眠。 -2. 队头为 `FAILED` 且尝试次数达上限:转 `DEAD(EXHAUSTED)`,终态与回填意图同一条 UPDATE 落库,**不做跨库写**。该分支只写 `PROC_STATE`,不取 `PIPELINE_LOCK`、不在处理器事务内,也不在 `MessageLifecycleGate` 内。 -3. 队头为 `FAILED` 且未到 `next_attempt_at`:休眠到可重试时刻,不处理后续消息。 -4. 其余(新消息或退避到期的重试):调用 `MessageProcessor.processOne`,失败迁移在该边界内完成。 +1. 无队头 → 休眠。 +2. `FAILED` 且次数用尽 → `DEAD(EXHAUSTED)`,同事 UPDATE 写终态与回填意图;只写 `PROC_STATE`,不取锁。 +3. `FAILED` 且未到 `next_attempt_at` → 等到点,不跳队。 +4. 其余 → `MessageProcessor.processOne`。 -**终态判据只有尝试上限,没有按时间的毒丸**:处理卡死应由外部调用的有界超时兜底;用一个时间阈值把消息直接推入 `DEAD` 会绕过人工复核,并制造与人工重放并发的旁路写入者。队头年龄由 `msgx.pipeline.backlog.oldest_unprocessed_seconds` 观测,主泵不做这项判定。 - -**所有取时统一经注入 `Clock`**(主泵调度、处理器落库时间、回填重试、作业切日),不使用系统时钟。 +不按时间推 `DEAD`;卡死靠外部有界超时。队头年龄见指标 `msgx.pipeline.backlog.oldest_unprocessed_seconds`。时间一律用注入 `Clock`。 ### 5.2 processOne @@ -124,13 +116,12 @@ processOne(head): 整包协议拒绝 → DEAD(PROTOCOL),不落半包 5. 业务型成功,分三步(INV-3、INV-10): ① 事务提交:航班变更 + 待发事件 - ② 写 Redis 投影;失败 → 保持未完成,下轮重处理 + ② 写 Redis 航班快照;失败 → 保持未完成,下轮重处理 ③ 事务提交:SUCCEEDED + 回填意图 6. 结束:主泵不做回填;回填意图已随终态落库,由扫描补写信箱标记 ``` -- 类型未覆盖不等于报文非法:不支持的消息类型跳过留档、按已处理写回标记,不按 `MALFORMED` 处理。 -- **主泵不回填**:终态与回填意图由同一条 UPDATE 落库,回填一律由扫描驱动,不占用 FIFO 关键路径。回填只需消息 ID,缺 META 或解码失败的死信同样可补写。影子环境禁写。 +不支持类型 ≠ 报文非法,走 `SKIPPED`。主泵不写回信箱;写回由扫描做。测试/影子实例禁写信箱。 ### 5.3 事务边界 @@ -139,30 +130,24 @@ processOne(head): | 收报入队(`insertIfAbsent`) | 是 | 否 | 否 | 同库事务 | | 身份首次绑定 | 否 | 否 | 否 | 单语句 + 唯一约束 | | 业务型领域变更(航班变更 + 待发事件) | 是 | 是 | 是 | 同库事务(`INV-3`) | -| Redis 投影写 | 否 | 是(处理步骤锁跨越本步) | 否 | 外部副作用,不在 PG 事务内;写成功是终态事务的前置(`INV-10`) | +| Redis 航班快照写 | 否 | 是(处理步骤锁跨越本步) | 否 | 外部副作用,不在 PG 事务内;写成功是终态事务的前置(`INV-10`) | | 业务型终态(`SUCCEEDED` + 回填意图) | 是 | 是 | 否 | 同库事务(`INV-3`) | | 非业务型终态(`MALFORMED` / `PROTOCOL` / `SKIPPED` / `EXHAUSTED`) | 否 | 否 | 否 | 单语句(终态与回填意图同一条 UPDATE) | | 航班历史清理的物理删除 | 是 | 是(落实 `US-14` AC4) | 是 | 同库事务:复查判据 + 历史写入成功后删除 | | 回填(信箱标记 + `BACKFILL_AT`) | 否 | 否 | 否 | 跨库两次单写;幂等可重跑 | | 人工重放(批量改回 `PENDING`) | 否 | 否 | 否 | 单语句批量;`MessageLifecycleGate` 与回填互斥 | -结论:航班变更与处理终态**不在同一事务**——两者之间夹着 Redis 投影写;同一事务只保证「航班变更 + 事件」与「终态 + 回填意图」各自原子(`INV-3`、`INV-10`)。处理步骤的 `PIPELINE_LOCK` 跨越 Redis 写,使与航班历史清理的协作覆盖整个步骤(`US-14` AC4);该锁的竞争写者是**航班历史清理**,不是别的处理器线程;没有第二写者时该锁不产生额外串行度。 +航班变更与终态不在同一事务,中间夹 Redis 写(`INV-3`、`INV-10`)。`PIPELINE_LOCK` 跨 Redis 写,与历史清理互斥(`US-14` AC4);无第二写者时不额外串行。 ### 5.4 历史积压 -信箱中的成规模存量(上线前遗留、停机累积)**不是特殊模式**:它逐条走与日常完全相同的 FIFO 路径。 - -- 顺序由 `MSG_ID` 决定,不由执行方式决定。入队与处理由不同线程驱动、可以并发,「先入队后处理」只是可选的运维规程,不是正确性前提;系统不提供「只入队」模式。 -- 不加速、不分流、不走旁路:不允许并行队头,也不允许实时消息跳过积压。 -- 尝试上限与退避对积压同样生效,不因积压而放宽。 -- 不再处理的行置 `SKIPPED` 并记录原因,到达终态后走回填通道;不存在「整段 DELETE」的快速通道。 -- 消化期间的可观测项见 [reference.md](reference.md);完成时限不作对外承诺(`CLM-5`)。 +积压走同一 FIFO 路径,无加速/旁路/跳队。退避与次数上限不变。跳过的行 `SKIPPED` 后走写回。完成时限不作承诺(`CLM-5`);指标见 [reference.md](reference.md)。 ## 6. 回填 ### 6.1 事实与扫描谓词 -终态落库时登记回填意图;标记回写由扫描驱动,跨库单写、幂等可重跑。扫描谓词(与实现一一对应): +终态落库时记回填意图;写回由扫描驱动,跨库单写、幂等。扫描谓词: ```text STATE ∈ {SUCCEEDED, SKIPPED, DEAD} -- 终态 @@ -175,7 +160,7 @@ ORDER BY BACKFILL_ATTEMPTS ASC, MSG_ID ASC -- 公平轮转,永久失败 LIMIT PARAM:msgx.pipeline.backfill-batch ``` -超期判据使用**本地入队时间**(`ENQUEUED_AT`),不使用信箱的 `RECEIVED_AT`:后者来自外部时钟,前偏会扭曲超期窗口;本系统清理信箱行以 `C-1` 为准(回填了结且超保留期),不以库方时钟为准。 +超期只看 `ENQUEUED_AT`,不用 `RECEIVED_AT`。信箱清理见 `C-1`。 ### 6.2 四种结果与放弃 @@ -183,41 +168,31 @@ LIMIT PARAM:msgx.pipeline.backfill-batch |---|---|---| | 写入成功 | 标记为空、写入 1 行 | 记 `BACKFILL_AT`,不再重试 | | 早已有标记 | 写入 0 行且信箱行存在 | **视为成功**,不覆盖已有值,记 `BACKFILL_AT` | -| 信箱行不存在 | 写入 0 行且信箱行不存在 | **立即放弃自动重试**(原因 `MISSING_ROW`)并告警。终态行存在而信箱行不存在,只可能是该行在入队后被删除(永久空洞 ID 从不入队,不会进入本扫描) | -| 暂时性故障持续超期 | 超时 / 连接失败持续到 `R` 仍未打标 | **停止自动重试**(原因 `TRANSIENT_DEADLINE`)并告警;`R` 之前只退避重试,**不按尝试次数放弃**;保留人工恢复能力 | +| 信箱行不存在 | 写入 0 行且行已删 | 立即放弃(`MISSING_ROW`)并告警 | +| 暂时性故障超 `R` | 超时/连接失败到 `R` 仍未写回 | 停自动重试(`TRANSIENT_DEADLINE`)并告警;`R` 前只退避 | -**放弃 ≠ 标记已确认**:放弃行不写 `BACKFILL_AT`,处理标记仍为空;按 `C-1`,回填未了结的行不删;放弃行因此不进入共享信箱清理。 +放弃 ≠ 已写回:不写 `BACKFILL_AT`,信箱行不删(`C-1`)。 -### 6.3 `R` 的作用 +### 6.3 `R` -`R`(取值见 [reference.md](reference.md))有两个作用: +`R` 见 [reference.md](reference.md) `PARAM:msgx.pipeline.overdue-backfill`: -1. **取消退避**:已终态但超期未打标的行,每轮扫描都被尝试,不再等退避到期; -2. **暂时性故障的放弃期限**:超时、连接失败这类暂时性故障**在 `R` 之前只退避重试、不放弃**;到 `R` 仍未打标才停止自动重试、记入放弃清单并告警。 - -放弃判据用**时间**而不是**尝试次数**:固定次数不能稳定表达允许的故障持续时间,因此按 `R` 判断放弃,`PARAM:msgx.pipeline.backfill-max-attempts` 只用于告警。 - -关于「最终一定打标」,本系统能保证的只有两段: - -1. 退避重试(`R` 之前不放弃); -2. 到 `R` 仍失败则停止自动重试、告警,保留人工恢复(`reopen`)。 - -两个边界要说清:`MISSING_ROW`(信箱行不存在)是**确定性结论**,立即放弃,不受 `R` 保护;`R` 只要求 `R ≤ R_keep`。入站原文由本系统按 `C-1` 清理:回填已了结且超过保留期(约 1 个月,`Q9`)后删;放弃行无 `BACKFILL_AT`,不删。 +- 超 `R` 未写回 → 每轮都试,不等退避。 +- 暂时性故障到 `R` 仍失败 → 停自动重试;`backfill-max-attempts` 只告警。 +- `MISSING_ROW` 立即放弃。`R ≤ R_keep`。写回已了结且超保留期(约 1 月,`C-1`)后删原文;放弃行不删。 ## 7. 日计划快照与请求匹配 -### 7.1 快照发布 +### 7.1 快照 -`SCHD-DNLD` 与 `SCHD-RESP` 共用 `ScheduleProcessor.applyScheduleRecords`: +`SCHD-DNLD`/`RESP` 共用 `ScheduleProcessor.applyScheduleRecords`: -1. **重复处理判定**:`PROC_STATE` 已存在成功终态 → 幂等成功,仅追加留痕,不重复写入。 -2. **整包校验**:声明记录数、航班标识与运营日推导等校验失败 → 整包 `DEAD(PROTOCOL)`,不写半包,既有状态保持不变。 -3. **分批写入**:锁内按 `FLID` 点查归属日,发现同一航班跨运营日即整包回滚并 `DEAD(PROTOCOL)`;通过后分批合并写主表与资源明细,每批一个事务(状态变更 + 待发事件,`INV-3`)。快照里没有的航班删除:标记已删除、登记删除事件、从 Redis 投影移除(`INV-7`)。 -4. **提交结果**:整包完成后在同一事务置消息 `SUCCEEDED` 并预登记回填意图;提交后信箱回填由扫描承接,留痕在事务外追加。处理失败不标记已处理,下轮整包重新处理。 +1. 已成功 → 幂等,只追加留痕。 +2. 整包校验失败 → `DEAD(PROTOCOL)`,不写半包(`INV-4`)。 +3. 分批写:跨运营日整包失败;快照缺席航班标删、发删除事件、删 Redis(`INV-7`)。每批同事务写变更 + 事件(`INV-3`)。 +4. 整包成功 → `SUCCEEDED` + 回填意图;留痕在事务外。 -字段缺失与清空语义、运营日规则见「航班域」。 - -`RESP` 应匹配开放请求:无匹配、过期或报文早于发送时间时不更新快照(`G-RESP-GUARD`)。 +字段语义见「航班域」。`RESP` 须匹配开放请求,否则不更新(`G-RESP-GUARD`)。 ### 7.2 上游请求与静态数据 @@ -229,65 +204,57 @@ PENDING → SENT → DONE └──→ FAILED(收到 EROR,US-09 AC3) ``` -- 开放槽按 `RQRD` 与 `RQFD` 两类各一条(`US-09` AC1);注册同子类型新请求前使该类下旧开放请求过期,新请求待在途结案后再落信。只有 `COUTMSGS` 写入确认后才标记 `SENT` 并关联出站记录;写信箱成功但本地未确认的情况需要补偿与去重,不能无条件重新发送。 -- 收到 EROR:定位本系统发出的开放请求,标 `FAILED` 并告警(`US-09` AC3);`FAILED` 关闭开放槽。 -- 应答按报文类型匹配等待中的开放请求(`US-09` AC2);降级匹配的跨代误配风险必须明确接受并审计。 -- 时间比较统一时区与单位,判据一律用本地时钟(入队、发送时间),不引入库方或对方时钟。 -- 参考应答写入自有 `REF_MASTER`,日计划应答走快照流程;请求完成必须在相应数据处理成功之后,超时和迟到应答不能修改已关闭请求对应的状态。 -- 出站承诺只到落信(`C-4`、`CLM-4`);主 / 共享删除见 `C-5`。 +- 开放槽:`RQRD`/`RQFD` 各一条(`US-09` AC1);同类新请求前作旧请求过期。 +- `COUTMSGS` 确认后才 `SENT`;本地未确认须补偿,不能盲重发。 +- EROR → 标 `FAILED` 并告警(`US-09` AC3)。 +- 应答按类型匹配开放请求(`US-09` AC2);时间用本地时钟。 +- 参考应答写 `REF_MASTER`;日计划走快照。结案后迟到应答不改状态。 +- 出站只保证写入信箱(`C-4`、`CLM-4`)。 ## 8. 事件投递 -### 8.1 普通事件(`KAFKA:msg`) +### 8.1 `KAFKA:msg` -`Dispatcher` 从 `MSG_EVENT` 取待发事件。**保序边界是 `FLID`**(与分区键一致):同一 `FLID` 内按 `EVENT_ID` 保序,队头失败即暂停该 `FLID`;不同 `FLID` 之间不互相阻塞,也不承诺跨 `FLID` 顺序。消费者按 `(FLID, STATE_VERSION, UPDATED_AT)` 防旧覆盖新。 +同 `FLID` 内按 `EVENT_ID` 保序;跨 `FLID` 不承诺顺序。消费端用 `(FLID, STATE_VERSION, UPDATED_AT)` 防旧盖新。 -本批领取的 `EVENT_ID` 集合在**读取时刻冻结**:发送与标记只作用于这批事件,期间新提交的事件留待下一轮,不参与本批,也不被本批的「完成」带走。 +每批在读取时冻结 `EVENT_ID` 集合。`EVENT_ID` 全局串行分配,顺序 = 提交顺序(`US-14` AC4)。 -`EVENT_ID` 由全局串行分配产生:事件生产者在事务内写 outbox,主泵单线程,历史清理与主泵对实时航班表串行(落实 `US-14` AC4),因此**分配顺序 = 提交顺序**,不存在「已提交的较大 ID 先于未提交的较小 ID 被投递」。 +确认发送后标 `SENT`;失败退避,用尽 → 死信。外部调用须有界超时。 -发送确认后才标记 `SENT`,失败记录次数并按退避推后,达到上限转 `DEAD`(记录保留作 DLQ)。所有外部调用需要有界超时,避免阻塞投递线程。 +至少一次投递(`D2`);配置见 [reference.md](reference.md)「信箱与外部依赖」。 -投递是至少一次:Broker 或其他目标已接受但本地未标记成功时可能重发;目标端接受不等于业务消费者已消费。Kafka 生产端按 `D2` 保持顺序,当前配置见 [reference.md](reference.md)「信箱与外部依赖(成组登记)」;生产者幂等不替代应用层事件去重。 +### 8.2 `KAFKA:schd` -### 8.2 `schd` 聚合 +按 `FLID` 单行 upsert,只保留最新 `STATE_VERSION`。每次更新换 `EVENT_ID`(写代次)。 -`KAFKA:schd` 只提供最新状态通知,不保留每次中间变化,因此 outbox 按 `FLID` 单行 upsert:同一 `FLID` 只保留最新 `STATE_VERSION` 的事件与投递状态。两条写规则: +- **只进不退**:`STATE_VERSION` 只升不降。 +- **条件标记**:`SENT` 时 `WHERE STATE_VERSION = 本批版本`;已被新版本覆盖则下轮重发。 -`KAFKA:schd` 行的 `EVENT_ID` 不是跨代次稳定的事件句柄:每次接受更新都从全局序列取得新值并替换原主键,用作条件确认的写代次。重放和人工处置只能针对当前 `(TARGET, PARTITION_KEY, EVENT_ID)`;旧代次被替换后不再能按旧 ID 寻址。 +发送:取未发送行(`PARAM:msgx.schd.flush-limit`)→ 整批 `SCHD.FLTR` JSON(`C-9`)→ 条件标 `SENT`。删航班只走 `msg`。 -- **只进不退**:仅当新事件的 `STATE_VERSION ≥` 行内现有版本才覆盖,防止迟到的旧事件把新状态压回去。该合并规则以 `FLID` 在保留期内不复用(设计前提)为前提。 -- **条件标记**:发送成功后按**读取时刻的版本**做条件标记(`WHERE STATE_VERSION = <本批版本>`);该行若期间已被更新的版本覆盖,则不标记,留待下一轮重发。 - -发送时: - -1. 到期领取批次:按 `FLID` 取未发送行,批次大小受 `PARAM:msgx.schd.flush-limit` 约束; -2. 整批发送:将批次内所有航班的最新整态序列化为 `SCHD.FLTR` 数组 JSON,作为单条 record 发出(`C-9`);删除航班不进入 `schd`,只由 `msg` 发删除通知; -3. 成功后按上条规则标记完成并推进 `lastFlush`;失败按退避推后,达到上限转 `DEAD`。 - -聚合周期与批上限见 [reference.md](reference.md)。`KAFKA:msg` 与 `KAFKA:schd` 之间不承诺顺序。`schd` 行与 `msg` 行共用 `MSG_EVENT`,靠 `TARGET` 区分。 +周期与批上限见 [reference.md](reference.md)。`msg` 与 `schd` 不承诺顺序。 ### 8.3 事件清理 -已 `SENT` 的事件行按 `PARAM:msgx.pipeline.event-retention` 由维护作业清理。`KAFKA:msg` 的 `DEAD` 行保留作 DLQ,人工处置后再清理;`KAFKA:schd` 的 `DEAD` 行只保留到同一 `FLID` 出现新的、可接受的状态代次,新代次会把单行投影重置为 `PENDING` 并清空旧错误。该取舍服从 schd 只保存最新状态的契约,因此被替换的 schd DEAD 代次不再由 `MSG_EVENT` 提供持久审计句柄。 +已 `SENT` 行按 `PARAM:msgx.pipeline.event-retention` 清理。`msg` 死信须人工后再删;`schd` 死信在新代次出现时重置为 `PENDING`。 ## 9. 失败恢复与维护作业 ### 9.1 失败、重试与重放 -`ProcFailure` 与 `FailureScheduler` 统一处理侧失败落账,投递侧按同一套次数与退避规则迁移事件。启动自检强制退避档位数与尝试上限匹配,让「表里有档但永不触发」的配置无法通过。失败必须在持有具体消息、事件或批次的位置记录,外层循环只做兜底日志和等待,不重复增加次数。线程中断应恢复中断标记并向上传递;不把 JVM `Error` 当普通业务失败捕获。 +`ProcFailure`/`FailureScheduler` 统一失败落账与退避。启动自检:退避档位数须比尝试上限少一。失败在具体消息/事件/批次处记数;外层不重复加次数。 -`ReplayService` 只允许重放白名单内的错误类(见 [reference.md](reference.md))从 `FAILED / DEAD` 回到 `PENDING`,并重置尝试次数、下次重试时间与错误原因,**不重置 `IDENTITY_KEY`**(保留身份,避免重放时把自己判成重复消息)。重放与回填通过 `MessageLifecycleGate` 在同一实例内互斥;**旧消息进入终态后后续消息可能已执行,重新入队不等于恢复历史顺序**,重放前必须评估状态覆盖和版本保护(`CLM-3`)。 +`ReplayService` 白名单内可从 `FAILED`/`DEAD` 回 `PENDING`,重置次数与时间,**不改 `IDENTITY_KEY`**。与写回互斥(`MessageLifecycleGate`)。重放不恢复历史顺序(`CLM-3`)。 ### 9.2 中断恢复 -恢复的唯一依据是各存储中已持久化的记录,不依赖进程内存状态。在 PG 从备份恢复的场景下,已提交的终态与已发出的事件可能回退,后果是重复投递与重复回填(按至少一次与幂等接受),但不得据此重放业务;留痕(`SCHD_SNAP_LOG`)在业务事务外追加,崩溃会丢该条留痕,不影响状态。 +恢复只看持久化记录。PG 备份回退可能重复发 Kafka/写回,接受至少一次,不重放业务。`SCHD_SNAP_LOG` 在事务外,崩溃可丢。 | 中断位置 | 重启后的判定 | 恢复动作 | |---|---|---| -| 已落信、未入队 | 信箱行处理时间为空且 PG 无记录 | 重扫补建登记记录 | +| 已写入信箱、未入队 | 信箱行处理时间为空且 PG 无记录 | 重扫补建处理记录 | | 事务执行中 | PG 无该消息终态 | 事务整体回滚,按 `PENDING` 重新处理 | -| 领域事务已提交、Redis 写失败或终态未提交 | 该消息无终态(`PENDING`),仍占队头 | 整条消息重处理:投影按当前完整态重写;领域再跑是否只留一次效果见 `US-03`、`G-FLOP-IDEMPOTENT`;已提交结果不回滚(`US-03` AC3) | +| 领域事务已提交、Redis 写失败或终态未提交 | 该消息无终态(`PENDING`),仍占队头 | 整条消息重处理:Redis 航班快照按当前完整态重写;领域再跑是否只留一次效果见 `US-03`、`G-FLOP-IDEMPOTENT`;已提交结果不回滚(`US-03` AC3) | | 事务已提交、标记未写 | 终态行仍持有回填意图 | 仅补写标记;业务处理结果保持不变 | | 标记写入中途 | 标记仍为空 | 重新写入;重复写入同一值无副作用 | | 回填时信箱行已不存在 | 写入 0 行且信箱行不存在 | 立即放弃自动重试(`MISSING_ROW`)并告警;放弃不等于标记已确认,仍需人工对账 | @@ -297,19 +264,15 @@ PENDING → SENT → DONE ### 9.3 生命周期与清除 -`JobRunner` 用独立 daemon 线程按周期触发回填扫描、航班历史清理与留痕清理;作业不参与消息 FIFO,也不使到期消息饥饿。历史清理时跳过正在被消息处理的航班(`US-14` AC4)。 +`JobRunner` 周期跑写回扫描、历史清理、留痕清理;不挡主泵 FIFO。历史清理跳过处理中的航班(`US-14` AC4)。 -**通则**(对本系统所有持久对象适用) +清除须终局证据(航班 `US-14`/`D1`;信箱 `C-1`)。证据不明 → 删 0 条。写回失败记录在信箱删前可查(`US-10` AC2)。 -- **时间不构成清除依据**:到期只是必要条件,**终局证据才是充分条件**(航班见 `US-14` AC3、`D1`;共享信箱见 `C-1`:回填已了结且超保留期)。 -- **证据不随清除消失**:回填失败与放弃的记录在其覆盖的信箱行被清除前保持可查(`US-10` AC2)。 -- **证据缺失或结果不明时按最保守处置**:航班清理为删 0 条(`US-14` AC3、`D1`)。 - -**逐对象生命周期**(保留期取值一律见 [reference.md](reference.md)) +**逐对象**(保留期见 [reference.md](reference.md)) | 对象 | 终局判据 | 归档目标 | 清除证据 | 执行方 | 偏差 | |---|---|---|---|---|---| -| 共享信箱 `CMINMSGS` 原文 | 回填了结(`BACKFILL_AT` 非空)且超保留期(约 1 个月,`C-1`/`Q9`) | — | 回填已了结 + 超保留期;放弃行无 `BACKFILL_AT`,不删 | 我们 | — | +| 共享信箱 `CMINMSGS` 原文 | 回填了结(`BACKFILL_AT` 非空)且超保留期(约 1 个月,`C-1`) | — | 回填已了结 + 超保留期;放弃行无 `BACKFILL_AT`,不删 | 我们 | — | | `FLIGHT_SCHD` + 资源明细 | 判史规则 | 历史存储 | 历史写入确认 + 版本复查 | 我们 | — | | 航班历史存储 | 保留期 | — | — | 我们 | `G-FLIGHT-HIST-RETENTION` | | `SCHD_SNAP_LOG` | 保留期 | 无(本地可重建) | 无 | 我们 | — | @@ -317,47 +280,19 @@ PENDING → SENT → DONE | `PROC_STATE` 终态行 | 见下 | 无(到期直接删除) | 回填了结 | 我们 | `G-PROC-CLEANUP` | | `REQ_TRACK` 关闭态行 | 保留期 | 无 | 无 | 我们 | `G-REQ-TRACK-RETENTION` | -**处理记录到期清理**(`US-11`) +**`PROC_STATE` 清理**(`US-11`):终态 + `BACKFILL_AT` 非空 + 超保留期(从 `UPDATED_AT` 起)。删除按 `STATE` 条件执行,0 行则跳过(与人工重放互斥)。批量删除不持 `PIPELINE_LOCK`。 -候选 = 终态 **且** 回填已了结 **且** 终局后超过保留期(基准是 `UPDATED_AT`:终态与了结都推进它,了结后不再更新)。两处不可省: +保留期从终局后起算,未写回不进候选。其余:历史清理先写 ES 再删(「航班域」);留痕按 `(SCOPE_END, RECV_AT)`;事件见「事件清理」。信箱见 `C-1`/`C-2`。 -- **回填已了结** = `BACKFILL_AT` 非空(`US-11`)。放弃行不写标记,按未了结保留,不参与删除。 -- **写入前复查** = `DEAD` 可被人工重放改回 `PENDING`。人工重放走 `MessageLifecycleGate`、不取 `PIPELINE_LOCK`,因此该锁不构成复查依据:删除在同一事务内按候选时的 `STATE` 条件执行;影响 0 行即整体回滚、该行跳过。重放先一步改回 `PENDING` 时谓词不匹配,天然互斥。批量删除不得持 `PIPELINE_LOCK`——那会阻塞主泵 FIFO,与「作业不使到期消息饥饿」冲突。 +## 10. 容量假设 -清理范围只含「终态且已回填」;保留期内同身份去重见本章「消息、身份与决策」。 +单机场、单实例;日消息千~万级;单条 ≤ 10⁴ 字节;延迟秒级可接受。不引入多实例/分布式锁/分区表。参数依据见 [reference.md](reference.md)(`CLM-6`)。 -**时间常数排序**:`R` 的取值与依据见 [reference.md](reference.md) 的 `PARAM:msgx.pipeline.overdue-backfill`,信箱清理前提见 [specification.md](specification.md) 的 `C-1`,本文件不复述。只补一条实现口径:保留期计的是**终局之后**的时间,不是入队之后——终态行未了结回填时不进入候选。 +## 11. 航班域:数据模型与合并 -**其余清理** +PG 是航班数据源(`INV-5`);Redis 从 PG 同步,处理完成前写入(`INV-10`)。`FLID` 全局唯一。写入与对账只看 `FLIGHT_SCHD` 及明细;Redis 只读。 -- **航班历史清理**:按 [reference.md](reference.md) 的历史判据选候选(含 `DELETED`),先成功写入历史存储再删除;语义与红线见「航班域」与 `US-14`、`D1`。 -- **留痕清理**:`SCHD_SNAP_LOG` 按保留期与 `(SCOPE_END, RECV_AT)` 删除,不依赖历史存储开关。 -- **出站事件清理**:见「事件清理」。 - -共享信箱保留与清理见 `C-1`、`C-2`。历史写入与删除事件入队之间仍需恢复方案;顺序调用不构成原子提交。 - -## 10. 容量假设与设计取舍 - -本设计按以下量级选型(参数默认值的依据列见 [reference.md](reference.md),可声明性见 `CLM-6`): - -- 单机场、单活动实例、单维护者;入站日消息量千级到万级;单条报文量级 ≤ 10⁴ 字节。 -- 处理延迟秒级可接受;航班可见性延迟不劣于现役(轮询间隔 + 聚合周期秒级)。 -- 因此:不引入多实例并行、分布式锁、分区表;用单行锁与单线程换确定性。 - -容量假设变化时,需要重新评估的项:批次大小与轮询间隔、聚合周期与批上限、指标取数口径(`backlog()` 是 `PROC_STATE` 聚合)、以及 `MSG_EVENT` 保留期。 - -## 11. 航班域:权威模型与合并写入语义 - -本章是航班状态的唯一现行设计规范。其他各章只描述管道机制,不重复定义航班域规则。 - -系统从共享 MySQL 信箱接收 SIS/AODB 报文,把结果合并到自有 PostgreSQL 中的航班当前态并同步写 Redis 投影,再通过 outbox 投递 Kafka。共享信箱、Redis 投影和 Kafka 都不是状态权威;Redis 投影写在处理完成之前(`INV-10`)。 - -- `FLID` 是航班实例的唯一标识;不得由航班号、日期或资源号推断身份。 -- `FLIGHT_SCHD` 及其明细表是唯一权威当前态;Redis 投影与展示视图只读,不能作为写入或对账来源(`INV-5`)。 -- 单活动主泵按信箱 FIFO 推进。事务内 `PIPELINE_LOCK` 只串行化本地状态提交,不替代选主或消息认领。 -- 状态变更与 outbox 事件在同一 PostgreSQL 事务中提交;处理终态与回填意图在同一事务中提交、且晚于 Redis 投影写成功(`INV-3`);回填与 Kafka 投递在提交后独立重试。 - -### 11.1 权威模型 +### 11.1 数据模型 | 对象 | 职责 | |---|---| @@ -365,16 +300,16 @@ PENDING → SENT → DONE | 资源明细表 | 保存登机门、值机柜台、转盘、计划机位、滑槽、延误、靠撤桥、轮挡等变长集合;`SRVT`/`VIPF` 专用明细见 `G-SRVT-VIPF`。主键为 `(FLID, ORDINAL)`。 | | `FLIGHT_ROUTE_POINT` | ROUT 与 ERUT 两类路线点,使用 `ROUTE_KIND` 区分;主键应包含该列,避免两类路线的序号冲突。 | | `PROC_STATE` | 信箱消息的处理终态、业务身份幂等记录,以及回填事实(`RECEIVED_AT` / `BACKFILL_*`)。 | -| `MSG_EVENT` | 事务 outbox,承载整态投影、变更通知和删除 tombstone。 | +| `MSG_EVENT` | 事务 outbox,承载整态快照、变更通知和删除事件(tombstone)。 | | `SCHD_SNAP_LOG` | 日计划处理留痕,只追加、可重建,不参与状态决策。 | ### 11.2 航班身份与运营日 -`FLID` 是主键。`OPERATION_DAY` 从 SCHD 记录的 `SODT` 按配置的机场时区和切日规则推导;它不是消息接收日或落库日。尚未由日计划收录的航班可以为 `NULL`;这不表示该航班没有运营日,只表示当前模型无法为它确定归属日。运营日冲突按 [reference.md](reference.md) 错误分类 **`PROTOCOL`** 处置:立即 `DEAD`,整包不落地(`INV-4` 只管整份校验不过时本地不改)。 +`OPERATION_DAY` 从 `SODT` + 机场时区/切日规则推导,不是接收日。未收录可为 `NULL`。跨运营日冲突 → `DEAD(PROTOCOL)`,整包不写(`INV-4`)。 ### 11.3 字段与集合 -航班当前态分三层:主表标量、集合明细、路线点。**键名一律取 SIS 标签名**(不改写、不合并同义标签),集合元素内的键取属性名或子标签名。字段的业务语义、值域与长度以 [SIS 接口规范](legacy/SIS_AODB_RMS-V0.1.md) 与 [XSD](legacy/unisysaodbsis.xsd) 为准;本节只定义目标形态与合并语义。 +三层:主表标量、集合明细、路线点。键名取 SIS 标签;语义见 [SIS 规范](legacy/SIS_AODB_RMS-V0.1.md) 与 [XSD](legacy/unisysaodbsis.xsd)。 **标量**(存于 `FLIGHT_SCHD` 主行,出现即覆盖;空串为显式清空) @@ -401,45 +336,30 @@ PENDING → SENT → DONE | `SRVT` | `OPER`、`SRTC`、`SRQT`、`SRST`、`SRET`、`SRPR`、`SANR`、`SARR` | 无界 | `G-SRVT-VIPF` | | `VIPF` | `OPER`、`VPCD`、`VFES`、`VIPT/OPER`、`VIPT/VSCD`、`VIPT/VTQY`、`VIPT/VTST`、`VIPT/VTET` | 无界 | `G-SRVT-VIPF` | -`SRVT`、`VIPF` 只保留出现事实与原始内容,不参与合并与投递。`MAFL` 不是 SIS/XML 入站字段,而是由共享航班的 `MAID`、`FLID`、`FLNO` 生成的主航班派生投影(`G-MAFL`)。 +`SRVT`/`VIPF` 只存原文,不参与合并。`MAFL` 由子航班派生(`G-MAFL`)。 - `ORDINAL` 是持久化顺序,从 1 开始;`SOURCE_SEQ` 是上游序号,允许为空或重复。 - 相同资源号不代表同一条分配,禁止按资源号去重。 - 每次持久化完整航班状态时,明细表按该 `FLID` 先删后插,以完整合并结果为准。 - ROUT 与 ERUT 是两类独立集合,不能因相同序号覆盖彼此。 - `CHDT` 的类字段固定为 `CCLS`/`CTYP`;当前 wire DTO 与持久化列误写成 `CHCLS`/`CHTYP`,见 `G-FLOP-UNMAPPED`。 -- 主/共享关系以主表的 `MAID` 为事实来源:`MAID` 是共享航班指向主航班 `FLID` 的引用(非共享航班为 `NULL`);`MAFL` 只在读取和事件投影时从子航班事实派生,不按入站标量解析或保存。 +- 主/共享:`MAID` 指向主航班 `FLID`;`MAFL` 从子航班派生,不入站。 -### 11.4 主/共享投影(`MAFL`) +### 11.4 `MAFL` -`MAFL` 是主航班的派生集合,元素为子航班的 `FLID` 与 `FLNO`;内容与变更传播见本章与 `US-06` AC2。 - -- 已 FDEL 的子航班(`STATE = DELETED`)自然退出投影,不需要改写主航班行。 -- 只有 `MAID` 为空的主航班携带 `MAFL`;共享航班只携带自身 `MAID`、`CSOP`、`CSFT`,不携带 `MAFL`,避免下游双向合并。 -- 同一写代次下投影逐字节稳定,与到达顺序及 `FLNO` 变更无关;重发与消费端比对才有意义。 -- `MAID = FLID` 的自引用行不进入任何 `MAFL`;`MAID` 指向不存在主航班的悬挂引用不阻断该子航班自身处理,只是不产生投影。 -- 子航班集合变化的传播见 `US-06` AC2,事件类型为 `KAFKA:msg` + `KAFKA:schd`;否则整态投影的只进不退写入会丢弃它(见「`schd` 聚合」)。共享航班自身不单独发通知。 -- 派生主航班投影与产生它的状态写入必须同一事务或一致读快照;按 `MAID` 取子航班要求该列有索引(`US-06` AC2)。 +主航班派生集合:`{FLID, FLNO}`(`US-06` AC2)。仅主航班(`MAID` 空)带 `MAFL`;共享航班不带。已删子航班自动退出。子航班变更须通知主航班(`msg`+`schd`)。同事务写入;`MAID` 须索引。 ## 12. 航班域:合并、删除与生命周期 -本章的领域规则只描述「合并成什么态」;决策纯度、事务边界与落库职责见「消息、身份与决策」与 `INV-3`(`US-03`)。 +### 12.1 SCHD -### 12.1 SCHD 日计划 - -SCHD DNLD/RESP 在整包校验通过后,分批将报文携带的航班写入当前态,每批一个事务;快照里没有的航班删除:标记已删除、登记删除事件、从 Redis 投影移除(`INV-7`)。日计划就是主动与 AODB 全量同步一次,以 AODB 下发的数据为准。 - -日计划里某航班没携带的字段,视为 AODB 已删除该值,本地同步清除(`C-6`)。每个成功写入的航班推进 `STATE_VERSION`,并在同一事务登记 `KAFKA:schd` 与 `KAFKA:msg` 事件。 - -消息重复处理由 `PROC_STATE` 的消息 ID 与 `IDENTITY_KEY` 控制;已成功提交的消息不得再次写入或重复登记事件。整包校验失败时整包不落地(`INV-4`);运营日冲突按 [reference.md](reference.md) 错误分类 **`PROTOCOL`**。 +整包校验通过后分批写入;快照缺席标删(`INV-7`)。未带字段清空(`C-6`)。成功航班推进 `STATE_VERSION` 并写 `schd`+`msg` 事件。重复由 `PROC_STATE` 控制;校验失败整包不写(`INV-4`)。 ### 12.2 动态运行事件(FLOP) -FLOP 只修改报文表达的字段或集合,其余状态保持不变;目标形态与合并规则见「字段与集合」。`STYP` 必须命中现行白名单(基线见下段),未知值按不支持类型跳过留档(`US-03` AC2),不得进入通用合并。事务边界见「主泵调度与单条处理」、`INV-3`、`INV-10`。同一消息不产生两次效果见 `US-03`;逐类规则未补齐见 `G-FLOP-IDEMPOTENT`。 +只改报文带的字段/集合(见「字段与集合」)。`STYP` 须命中白名单;未知 → `SKIPPED`(`US-03` AC2)。逐类规则见 `G-FLOP-IDEMPOTENT`。 -`US-05` AC1 **现行基线仅 7 类**:`ABTM`、`DELY`、`PSDT`、`CKDT`、`CLDT`、`CHDT`、`GTDT`。下表其余行是 `Q3`/`G-FLOP-SEMANTICS` 的闭合目标,**不是现行白名单**。`Q3` 只管 SIS 未记载者的形态与逐类终态;`ABTM`/`DELY` **是否处理已定案**。 - -逐类语义以 SIS 的字段表、空标签规则与 Processing Exceptions 为准;下表每一行都必须有一条回归用例钉住「输入与前态 → 目标状态 → 终态与事件」。 +**现行白名单 7 类**(`US-05` AC1):`ABTM`、`DELY`、`PSDT`、`CKDT`、`CLDT`、`CHDT`、`GTDT`。下表其余行是闭合目标(`Q3`),不是现行白名单。 | SIS | STYP | 目标 | 空标签 / 缺失语义 | |---|---|---|---| @@ -471,44 +391,31 @@ FLOP 只修改报文表达的字段或集合,其余状态保持不变;目标 | `SIS:3.42` | `TRML` | `TRML` 标量 | 空 = 删除航站楼 | | `SIS:3.43` | `VIPP` | `VIPP`/`VIPR` 标量 | 空 = 删除;SIS 另要求 RMS 忽略 `VIPP`(忽略事件还是忽略字段,SIS 未写明,须以真实报文确认,见 `G-FLOP-SEMANTICS`) | -`CKDT`、`CLDT`、`CHDT`、`GTDT`、`PSDT` 等 FLOP 子类型是否入站、如何合并,见 `US-05` 与本章「动态运行事件」;SIS 中的 RMS→AODB 方向说明不约束本系统(OMMS 只收不发)。 - -**SIS 未定义但 legacy 处理的子类型**:`ABTM`、`DELY`(现行基线已要求处理,`US-05` AC1)、`PADT`、`FTSS`、`STND`、`BDPB`、`REMC`。它们在 `XSD` 的 `FLOP` 段里是普通字段或集合,没有独立事件节。`ABTM`/`DELY` 以外者的报文形态与是否存在必须以真实报文确认(`Q3`),不得据 legacy 行为直接定案。 - -**SIS 定义但 legacy 无处理器**:`CHDT`、`FINT`、`MAXP`。 - -**未映射字段**:[XSD](legacy/unisysaodbsis.xsd)「FLOP 元素」中尚未解码或错误映射的字段见 `G-FLOP-UNMAPPED`;其中 `FRET`、`FDIV`、`BDPB` 在 legacy 有对应处理,逐项必须定案(补齐映射或明确声明忽略),不得静默丢弃。 - -动态事件保留既有 `OPERATION_DAY`,也不基于接收时间重新推导它。除 FDEL 外,SIS 未规定目标航班不存在或已删除时的结果;逐类终态必须经 `Q3` 定案,不能统一推定为成功。当前实现的白名单、`ROUT`、BOTM/LACL 状态与已删除航班行为偏差见 `G-FLOP-SEMANTICS`。 +**legacy 有、SIS 无**:`PADT`、`FTSS`、`STND`、`BDPB`、`REMC`(形态待 `Q3`)。**SIS 有、legacy 无**:`CHDT`、`FINT`、`MAXP`。**未映射**:`G-FLOP-UNMAPPED`。不改 `OPERATION_DAY`;目标航班缺失时的终态待 `Q3`。实现偏差见 `G-FLOP-SEMANTICS`。 ### 12.3 删除与重建 -FDEL 是业务删除入口:仅在 `ACTIVE → DELETED` 时推进版本、保留明细并与 tombstone 同事务登记;重复 FDEL 或不存在的航班按幂等成功处理。 -物理删除仅由独立历史清理在历史写入成功后执行(`US-14` AC3、`D1`)。快照里没有的航班删除(`INV-7`);FDEL 仍是最先的删除入口。清理前需要登记一次 tombstone;已经 FDEL 的记录不重复发出。 +FDEL:`ACTIVE→DELETED`,写 tombstone。物理删除仅历史清理成功后(`US-14`、`D1`)。快照缺席也标删(`INV-7`)。 -ADFT 按 `US-04` AC2:未携带的字段不清空(Set-only);出现字段可更新。不得把它当成日计划或动态全量替换。新建 ADFT 若带可解析的 `SODT`,按同一运营日规则计算 `OPERATION_DAY`;否则保留为 `NULL`。 +ADFT:Set-only(`US-04` AC2),未带字段不清。有 `SODT` 则算 `OPERATION_DAY`。 -主/共享航班级联:删除共享航班时重算主航班 `MAFL`(见「主/共享投影」)并向主航班通知;删除主航班时级联删除其子共享关联并发出删除通知;主/共享关系必须一次原子变更,不出现主已删、子残留的半状态。共享航班增量通常只更新并通知主航班,不直接发共享通知。这些语义同样约束 FDEL 之外的生命周期清理。主/共享关联的增删按 `FLID` 做值比较,不使用引用比较。 +主/共享:删共享 → 重算主航班 `MAFL` 并通知;删主 → 级联删子并通知。须原子,按 `FLID` 值比较。 ### 12.4 Kafka 与读取 -`KAFKA:schd` 是按 `FLID` 的完整状态投影。Dispatcher 可以合并同一 `FLID` 尚未投递的中间版本,只发最新状态;消费端用 `(FLID, STATE_VERSION, UPDATED_AT)` 防止旧投影覆盖新状态。 - -`KAFKA:msg` 只通知变化,不承载权威状态;两个 topic 不承诺顺序。删除航班不进 `schd`,只由 `msg` 发删除通知(`C-9`:不设消息键)。删除通知的 value 与变更/删除区分方式待 `Q5` 定案。 - -读取完整航班必须在明确的一致性读边界内批量加载主表和全部明细。 +`schd` = 整态快照;`msg` = 变更通知。不承诺 topic 间顺序。删航班只走 `msg`(`C-9`)。读航班须同事务/一致读边界批量加载。 ### 12.5 生命周期 -运营日过去不等于航班结束。历史清理须同时满足配置保留期与终态证据或足够静默期,先成功写入历史存储,后物理删除当前态;历史存储失败时必须删除零行(`D1`、`G-FLIGHT-HIST-RETENTION`)。 +运营日过去 ≠ 航班结束。历史清理:保留期 + 终态证据 → 先写 ES 再删;失败删 0 行(`D1`)。 ## 13. 静态参考数据 -本章定义由入站 SIS 消息获得的航班基础数据(静态参考数据)的目标形态与合并语义。字段的值域、长度与业务含义以 [SIS 接口规范](legacy/SIS_AODB_RMS-V0.1.md) 对应节为准,本章只定义类别、结构与落库口径;取数路径与刷新责任见 [requirements.md](requirements.md) `US-13`。下游读取边界见「admin-api 下游读取边界」。 +静态参考数据:类别、结构、合并语义。字段语义见 [SIS 规范](legacy/SIS_AODB_RMS-V0.1.md);验收见 `US-13`。 ### 13.1 来源 -| 来源 | 权威节 | 形态 | +| 来源 | SIS 节 | 形态 | |---|---|---| | AODB 参考数据事件 | `SIS:3.1`~`SIS:3.13` | `META.TYPE` 即类别码;`STYP` ∈ `DNLD` / `RESP` / `ADD` / `UPD` / `DEL` | | AODB 资源状态事件 | `SIS:3.14` | `TYPE=RSTA`;`DNLD` 是单条状态更新,`RESP` 才可包含多条 | @@ -516,7 +423,7 @@ ADFT 按 `US-04` AC2:未携带的字段不清空(Set-only);出现字段 ### 13.2 类别 -| `RTYPE` | 含义 | 唯一键 `RKEY` | 权威节 | +| `RTYPE` | 含义 | 唯一键 `RKEY` | SIS 节 | |---|---|---|---| | `COUL` | 国家代码 | `COUC` | `SIS:3.1` | | `ARPT` | 机场代码 | `ITCD` | `SIS:3.2` | @@ -537,28 +444,20 @@ ADFT 按 `US-04` AC2:未携带的字段不清空(Set-only);出现字段 ### 13.3 结构与合并语义 -`REF_MASTER` 是对业务暴露的有效参考数据视图,逻辑身份为 `(RTYPE, RKEY)`;普通类别的 `RKEY` 见上表,`RSTA` 的身份必须同时包含 `RTYP` 与 `RSID`。记录保存消息来源、消息批次、刷新时间与按 SIS 标签名组织的字段载荷;重复字段保留输入顺序并表示为有序数组。参考数据物理为独立数据表组。 +逻辑视图 `REF_MASTER`,键 `(RTYPE, RKEY)`;`RSTA` 加 `RTYP`/`RSID`。物理为独立表组。 -- **13 类参考数据**:`DNLD`/`RESP` 是类别全量,整批校验通过后原子发布;`ADD`/`UPD`/`DEL` 是单条全字段增量,按 `(RTYPE, RKEY)` 处理。全量替换只作用于消息指定的同一 `RTYPE`。 -- **资源状态**:`RSTA-DNLD` 是单条状态更新,按 (`RTYP`, `RSID`) 覆盖;`RSTA-RESP` 是请求返回的多条记录。两者都不以“本包未出现”为理由删除其他资源状态。 -- **删除只由 `STYP=DEL` 表达**:参考数据**没有字段级删除语义**——可选字段的空标签表示「数据不可用」,不是删除。这与航班动态的空标签语义相反,两者不得套用同一套合并规则。 -- **请求配对**:参考应答统一为 `STYP=RESP`,以响应 `TYPE` 对应原 `RQRD.STYP`;`RSTA` 的请求范围还要校验 `RTYP`,不能只按 `RESP` 或消息到达时间匹配。 -- `SRVT`、`VIPF` 与参考数据无关;参考数据不参与航班状态推进,也不进入 `MSG_EVENT`。 +- 13 类:`DNLD`/`RESP` 全量替换(同 `RTYPE`);`ADD`/`UPD`/`DEL` 单条增量。 +- `RSTA`:单条覆盖或 RESP 多条;不以「包内未出现」删其他状态。 +- 删除只认 `STYP=DEL`;空标签 = 不可用,不是删(与航班动态相反)。 +- 应答 `STYP=RESP`,按 `TYPE` 配对 `RQRD.STYP`。 +- 不进 `MSG_EVENT`,不参与航班推进。 SIS 声明的上游忽略与截断口径见 `SIS:3.1`、`SIS:3.2`、`SIS:3.4`、`SIS:3.5`、`SIS:3.6`、`SIS:3.7`、`SIS:3.8`、`SIS:3.10`、`SIS:3.11`、`SIS:3.12`。未确认本地用途前不入模型;`US-13` 增补须说明用途。 ### 13.4 资源状态 -资源状态是「带时间窗的可用性事实」,按 `RTYPE=RSTA` 存入 `REF_MASTER`,`RTYP` 限定资源类型(`BELT` / `CNTR` / `GATE` / `STND`),`STAT` 取值 `E`(可用)/ `D`(不可用)。 - -- 资源**默认为可用**;只有禁用需要下发。 -- 禁用可带起止时间;**结束时间到达后自动恢复可用,不再补发启用消息**。 -- 结束时间缺失表示**一直禁用,直到该资源收到新的状态事件**。 -- 重新启用只带起始时间,不带结束时间。 +`RSTA`:`RTYP` ∈ `BELT`/`CNTR`/`GATE`/`STND`;`STAT` = `E`/`D`。默认可用;禁用可带时间窗,到期自动恢复;无结束时间 = 一直禁用至新事件。 ### 13.5 admin-api 下游读取边界 -- 数据方向固定为「SIS 消息 → 本网关 → 业务数据库 → admin-api」;本网关不调用 admin-api,不读取其数据库或缓存。 -- admin-api 只读取已提交的航班状态与 `REF_MASTER` 有效视图,不参与消息解码、合并、批次发布或处理终态判定。 -- 开发运行时使用自有 PostgreSQL;Oracle 只有通过 `Q14` 要求的方言与集成验证后才可替代,单次部署不得同时把两库作为权威。 -- admin-api 直接读取本系统写入的静态参考数据表(`C-10`);共享 MySQL 始终只是信箱边界,不承载该读取模型。 +SIS → 本网关 → PG → admin-api(`C-10`)。本网关不调用 admin-api。Oracle 待 `Q14` 验证。MySQL 只是信箱。 diff --git a/docs/specification.md b/docs/specification.md index 8b55f5b..a4d3817 100644 --- a/docs/specification.md +++ b/docs/specification.md @@ -179,7 +179,7 @@ | INV-3、INV-10 | `US-03` AC4、`US-05` AC4、`US-06` AC1 | Redis 失败时不记「处理结束」与「还要写回」;完成前不发 Kafka | | INV-4 | `US-07` AC1 | 校验失败后 PG 航班数据不变 | | INV-5 | 架构「数据归属与一致性」 | 航班数据只写入自有 PG | -| INV-6 | implementation.md「权威模型」 | `FLID` 唯一 | +| INV-6 | implementation.md「数据模型」 | `FLID` 唯一 | | INV-7 | `US-07` AC2/AC3 | 快照缺席航班标删并从 Redis 删;未带字段清空 | | INV-8 | `US-06` AC1 | 标删后从 Redis 删;失败下轮重做 | | INV-9 | `US-07` AC4/AC5 | 分批失败全部重来(`G-SCHD-SNAPSHOT` 做完前不可验);PG 整份写完后再刷 Redis |