- 事务边界表:区分业务型/非业务型终态;回填改为扫描驱动(主泵不再 inline 回填) - 符号与配置:补 delivery-batch / backfill-max-attempts / cutover-watermark / late-detect-* / backlog-cache-ttl-ms / 邮箱超时与指标口径 - 闭环状态:G2/G3/G9 标记已修;G1 标注「阶段 0 只读迟到检测已交付」 - 修正 R/R_keep 论证:R 不保护重放窗口;原文保留归 R_keep 与 Q7/Q9, 并删除「由 SIS Expiry 推导提交时延」的无效推论 - 回填语义补全:四种结果、放弃 ≠ 标记已确认、公平轮转与饥饿说明 - 兼容入口与水位:登记行超出水位、水位追平前不被领取,并写明运维含义 Plane: ACM2-35 ACM2-37 ACM2-38 ACM2-39 ACM2-41
38 KiB
msgexchange-v2 设计文档
1. 阅读说明
本文说明模块如何协作、状态如何流转,以及失败后如何恢复。系统范围、存储归属和部署约束见 architecture.md,不在这里重复。
运营航班权威状态只落自有 PostgreSQL(FLIGHT_SCHD 及资源明细表),Redis 已彻底退出动态权威与全部写路径;ES 历史投影属暂缓范围,不参与当前设计。
本文描述处理机制与流程;尚未交付的能力在本文件中明确标注,并以 §10 差异为准。航班状态规则统一由 运营航班状态设计 维护。
文档标记约定(全文沿用,message-lifecycle.md 同):
- 【目标】= 设计要求,是否已交付以「实现差异」节为准;
- 【现状】= 已按设计实现的行为;
- 【缺口】= 尚未实现,条目登记在本文 §10 与 message-lifecycle.md §12;
- 【待确认】= 依赖开放问题(Q 编号),当前取值只是假定。
正文只描述目标设计。需要点明交付状态时,用上述标记写一句,不展开解释——缺口的唯一清单是 §10 与 message-lifecycle.md §12。
1.1 符号与术语
| 符号 / 术语 | 语义 | 存储与字段 | 配置键 | 当前取值 | 约束与唯一定义处 |
|---|---|---|---|---|---|
W(水位) |
信箱 ID 的连续上界:(min, W] 已全部读入自有 PG |
INBOX_CURSOR.COMMITTED_UP_TO |
— | 初值 0 | 只随新 ID 成功入队推进(永久空洞放行是唯一例外);message-lifecycle.md §5.1 |
holeSince |
W+1 处空洞最早被观测到的时刻;无空洞时为 NULL |
INBOX_CURSOR.HOLE_SINCE |
— | NULL | 跨重启保留;旧空洞补齐后新空洞重新计时;message-lifecycle.md §5.1 |
R(超期补写期限) |
回填长期失败时的强制补写上限(按接收时间计) | 判据用 PROC_STATE.RECEIVED_AT |
msgx.pipeline.overdue-backfill |
30 天 | 仅 R ≤ R_keep;不保护重放窗口(回填由扫描驱动、不等 R,打标时刻与 R 解耦);message-lifecycle.md §5.2 |
R_keep(清除保留期) |
信箱行可被物理清除前的最短保留时间 | 库方侧 | 库方策略 | 待定(Q9) | R_keep ≥ max(人工重放期限 + 人工处置期限, 审计期限, 回填重试上限);仅在"标记 + 保留期"清除语义下成立;message-lifecycle.md §6 |
head-deadline |
队头滞留上限,超过即毒丸升级 | 计时起点 PROC_STATE.PROCESSING_STARTED_AT |
msgx.pipeline.head-deadline |
10 分钟 | 为空时以 UPDATED_AT 兜底;§3.2 |
| 队头 | 最小的未完成消息(PENDING 与 FAILED 都占位) |
PROC_STATE(ORDER BY MSG_ID) |
— | — | 单线程串行处理,后续消息不得越过;§3.2 |
| 终态 | SUCCEEDED / SKIPPED / DEAD |
PROC_STATE.STATE |
— | — | 到达后队列方可推进;§2.3 |
| 回填意图 | 「还欠一次信箱标记」的持久化事实 | PROC_STATE.BACKFILL_NEXT_AT 非空 |
— | 随终态写入 | 与终态同一条记录、同一条语句;§3.3 |
| 处理标记 | 信箱行上表示「本系统已处理」的约定字段 | 信箱 DATE_PROCESSED / STATUS |
mailbox.processed-value |
PROCESSED |
只写空标记,不回撤、不覆盖;message-lifecycle.md §5.2 |
2. 数据与领域模型
2.1 持久化记录
所有内部表都属于自有 PostgreSQL;共享 MySQL 只保留约定的信箱读写边界。
| 记录 | 用途 | 关键约束 |
|---|---|---|
PROC_STATE |
入站消息的处理状态、身份、重试次数、错误原因与回填事实 | MSG_ID = CMINMSGS_ID 主键防止重复入队;IDENTITY_KEY 唯一约束防止业务重复;按最小未完成消息 ID 取队头;PROCESSING_STARTED_AT 是 HOL deadline 的稳定起点(为空时以 UPDATED_AT 兜底,见 §3.2);BACKFILL_NEXT_AT 非空 = 还欠一次回填(回填意图),BACKFILL_AT 非空 = 标记已确认,BACKFILL_ABANDONED_AT/REASON 非空 = 已停止自动重试(不等于标记已确认);RECEIVED_AT 复制自信箱接收时间,可能为 NULL,为 NULL 时 message-lifecycle.md §5.2 的超期兜底不生效。 |
MSG_EVENT |
等待投递的事件(outbox) | EVENT_ID 决定投递顺序;TARGET 区分 KAFKA:msg / KAFKA:schd;PARTITION_KEY 恒为 FLID;EVENT_TYPE 区分 UPSERT 与 TOMBSTONE。 |
REQ_TRACK |
上游请求及应答关联 | 保存请求类型、覆盖运营日、发送方、出站信箱 ID 与发送/完成时间;同类只允许一个开放请求。登记、超时与应答匹配尚未实现(§10)。 |
REF_MASTER |
静态参考数据(目标表) | (RTYPE, RKEY) 唯一;尚未建表,客户端与刷新流程见 user-stories.md US-13/US-14(US-14 两类映射的存储落点未定)。 |
FLIGHT_SCHD |
航班标量及单值异常字段 | FLID 主键;OPERATION_DAY 一经确定不可变;版本与最近消息 ID 用于追踪。变长资源集合存于 8 张资源明细表与 FLIGHT_ROUTE_POINT,规则见 flight-state.md §2,不在此重复。 |
INBOX_CURSOR |
共享信箱消费水位 W 与空洞计时 holeSince |
单行游标;W 只随新 ID 成功入队推进(永久空洞放行是唯一例外),遇空洞即停;HOLE_SINCE 持久化空洞观测时刻,进程重启不丢失计时;message-lifecycle.md §5.1。 |
PROC_STATE_HST |
终态处理记录的归档目标 | 尚未建表;不得改写为共享库历史表。 |
字段与索引定义以 src/main/resources/db/migration/V1__flight_state_baseline.sql 为准;Oracle 11g 的迁移形态见 src/main/resources/db/migration/oracle11g/(占位,未接入任何 Flyway 配置)。报文原文仍从共享信箱读取,因此必须协调原文保留期,不能在消息尚需处理或重放时提前清理;生命周期、回填与清除契约的唯一定义见 message-lifecycle.md。
2.2 消息、身份与决策
XmlCodec(实装 JacksonXmlCodec)将 XML 解码为 DecodedMessage,包含 SNDR / TYPE / STYP / SEQN / DTTM 元数据、MsgKind 与业务载荷。解码失败区分 MALFORMED(报文非法,不重试)与可随 codec 修复的编码错误。MsgKind 为一等分派键:Schd(RESP/DNLD/ADFT)、Flop、Fdel、Unsupported。
业务身份统一由 Identity.of 生成:
SNDR | TYPE | STYP | SEQN
接收时只按信箱 ID 去重;解码后才首次绑定业务身份。重试保留原有绑定,不能把自己判为重复消息。身份被另一条记录占用时,当前消息转为 SKIPPED,记录 duplicate-of:<id>。是否加入日期边界取决于上游序号重置周期,默认关闭(SEQN 的取值范围与回绕已由 SIS_AODB_RMS-V0.1.md §2.8.1 定义,重置周期见 Q11);上线后不能随意更换身份算法。身份绑定是独立的幂等单语句(WHERE IDENTITY_KEY IS NULL),不参与业务事务,见 §3.3 表 1。
分派与落库由 MessageProcessor 统一协调:按 MsgKind 把已绑定身份的队头消息交给对应事务协调器(DNLD/RESP → ScheduleProcessor,ADFT → AdftProcessor,FLOP → FlopProcessor,FDEL → FdelProcessor,其余 → FAILED(UNSUPPORTED))。这些 Processor 在 PIPELINE_LOCK 事务内读取当前完整态,调用纯领域决策逻辑得到下一完整态与待发事件,再统一落库并登记回填意图;它们不直接触碰 Kafka。处理终态与业务变更在同一事务边界提交,该结论只对处理器产出的「业务型终态」成立;MALFORMED / PROTOCOL / SKIPPED(重复) / EXHAUSTED 这类非业务型终态不涉及航班表,只需一条 PROC_STATE UPDATE(终态与回填意图同语句写入),不取 PIPELINE_LOCK。完整的事务边界见 §3.3 表 1。
2.3 状态与错误分类
处理:PENDING / FAILED → SUCCEEDED(成功)
→ SKIPPED(业务重复已实现;忽略/无匹配分支见 US-04/US-06,尚未实现)
→ FAILED(等待退避重试)
→ DEAD(MALFORMED / PROTOCOL / EXHAUSTED,均需人工处置)
投递:PENDING → SENT
→ PENDING(退避后重试)
→ DEAD(重试耗尽,保留记录作 DLQ)
SUCCEEDED / SKIPPED / DEAD 是处理终态,不再阻塞后续消息;FAILED 不是终态,仍占据队头。DEAD 表示需要处置,不等于业务成功。
| 错误类别 | 处理方式 |
|---|---|
MALFORMED |
报文非法或原文缺失,直接 DEAD,不在重放白名单内。 |
PROTOCOL |
整包协议拒绝(运营日冲突、声明数量不符、缺载荷等),立即 DEAD,整包不落地、不重试。 |
CODEC_ERROR |
解码能力问题,退避重试;修复后允许重放。 |
UNSUPPORTED |
处理器或快照能力未实现,按可恢复失败处理,不直接当作非法报文;仍受重试上限约束。 |
INFRA |
基础设施或执行异常,退避重试。 |
EXHAUSTED |
重试耗尽或滞留超时,转 DEAD,人工复核后允许重放。 |
重试次数用尽时统一转 DEAD(EXHAUSTED):ERROR_CLASS 被覆写为 EXHAUSTED,原始错误类别不再保留(LAST_ERROR 保留原因文本)。由于重放白名单包含 EXHAUSTED,这类记录仍可人工重放(§6.1)。
3. 收报与主泵
3.1 收报
InboxPoller 默认每秒按 ID 升序、有限批次(claim-batch,默认 50)读取水位之后的信箱记录(ID > W,不以处理标记为谓词),在自有 PG 建立 PENDING 并把水位推进到连续上界;入队与水位推进在同一 PG 事务内提交,重复扫描幂等、中断后重扫补建。收报层不解析业务载荷,也不回填已处理标记。
收报流程(每轮 pollOnce;完整判据、代价与前提见 message-lifecycle.md §5.1,本节不重复):
- 读游标
(W, holeSince);信箱不可读时记日志、等下一轮,不动水位——这属于基础设施失败,不能当成「没有新消息」。 - 取
ID > W的升序前claim-batch行。 - 从
W+1起逐 1 数求连续上界;出现缺号时按「遇空洞即停 / 超期放行」处理,本批缺号之后的行本轮一律不入队(判据见 §5.1)。 - 在同一个 PG 事务内:对水位以内的每一行
insertIfAbsent(MSG_ID, RECEIVED_AT),并写回(W, holeSince);主键冲突表示已入队,不计入也不报错。 - 提交。本批中因空洞或批次上限未入队的行留待下一轮——每轮最多解决一个空洞。
水位不是已处理标记,其有效性以 Q2 的 ID 单调承诺为前提;空洞老化阈值取 msgx.pipeline.max-commit-delay。
单实例前提:信箱读取不加锁,水位是单行覆盖写。本设计只在单活动实例下成立(architecture.md §5、D2);多实例并发收报会让水位互相覆盖,必须先有实例级排他。
兼容 HTTP 入口执行“写入共享信箱 → PG 入队”。两步不在同一事务中:信箱成功而 PG 失败时,原文不能丢失,由轮询补建;客户端失败重试可能再次写信箱,业务身份去重仍然必需。
兼容入口只写 PROC_STATE、不参与水位,因此它登记的行会超出水位;主泵在水位追平前不领取(§3.2 步骤 2),顺序因此不受影响——代价是这类消息要等收报把 W 推到它的 ID 之后才开始处理(最长约一个空洞老化窗口)。运维含义:只使用兼容入口而不运行收报轮询时,这些行不会被处理,必须让 InboxPoller 运行(msgx.pipeline.autostart=true 或显式触发)。详见 message-lifecycle.md §5.1/§12。
3.2 主泵调度
每次 Pump.tick 只围绕最小未完成消息(PENDING 与 FAILED 都占队头):
- 无队头:按轮询间隔休眠。
- 队头超出水位(
msgId > W):不领取,休眠到下一轮。这类行只可能来自兼容入口的直接登记;允许领取会让它越过尚未入队的较小 ID(G2)。会打印一条限流 WARN(仅在水位值变化时打一次)。 - 队头为
FAILED且已毒丸(attempts ≥ max-attempts,或now − 计时起点 ≥ head-deadline):转DEAD(EXHAUSTED),并立即尝试一次回填。该分支只写PROC_STATE,不取PIPELINE_LOCK、不在处理器事务内,也不在MessageLifecycleGate内(见 §3.3 表 1 与 §6.1)。 - 队头为
FAILED且未到next_attempt_at:休眠到可重试时刻,不处理后续消息。 - 其余(新消息或退避到期的重试):记录
PROCESSING_STARTED_AT(仅首次)后调用MessageProcessor.processOne,失败迁移在该边界内完成。
维护作业由独立 job 线程调度(§6.1),不占用消息循环;作业有界且不使到期消息无限饥饿。所有取时统一经注入 Clock(收报空洞老化、主泵调度、处理器落库时间、回填重试、作业切日),不使用系统时钟;HOL deadline 以首次处理时写入的 PROCESSING_STARTED_AT 为稳定起点,该列为空时(V3 迁移之前的存量行)以 UPDATED_AT 兜底;人工重放会清空 PROCESSING_STARTED_AT,由新一轮首次处理重新记录。
3.3 单条处理
processOne(head):
1. 入口守卫:head 已是 FAILED 且 attempts 达上限 → DEAD(EXHAUSTED),结束
2. 读原文:缺失 → DEAD(MALFORMED, raw-missing);读取异常 → FAILED(INFRA)
3. 解码:
报文非法(MALFORMED) → DEAD(MALFORMED),不重试
可修复解码错(CODEC_ERROR) → FAILED(CODEC_ERROR) 退避
4. 身份绑定(仅当 IDENTITY_KEY 为空):
已被本消息占用 → 继续
已被别的消息占用 → SKIPPED(duplicate-of:<id>),结束
空闲 → 写入 IDENTITY_KEY(独立单语句,不参与业务事务)
5. 按 MsgKind 分派:
SCHD-DNLD / SCHD-RESP → ScheduleProcessor(快照事务)
SCHD-ADFT → AdftProcessor(单航班事务)
FLOP / FDEL → Flop / FdelProcessor(单航班事务)
Unsupported → FAILED(UNSUPPORTED) 退避
载荷缺失 → DEAD(MALFORMED)
整包协议拒绝 → DEAD(PROTOCOL),不落半包
6. 业务型成功:处理器在自己的事务内写航班变更 + 待发事件 + SUCCEEDED + 回填意图
7. 返回终态标记:只有终态才调用 backfill.attempt(msgId) 试写一次信箱标记
表 1 事务边界(哪些动作在一个事务里、哪些不是):
| 动作 | 显式事务 | 持 PIPELINE_LOCK |
触及航班表/事件 | 原子性来源 |
|---|---|---|---|---|
收报入队(insertIfAbsent + cursor.save) |
是 | 否 | 否 | 同库事务 |
| 身份首次绑定 | 否 | 否 | 否 | 单语句 + uk_proc_identity |
业务型终态(处理器产出 SUCCEEDED) |
是 | 是 | 是 | 同库事务:航班变更 + 事件 + 终态 + 回填意图 |
非业务型终态(MALFORMED / PROTOCOL / SKIPPED / EXHAUSTED) |
否 | 否 | 否 | 单语句(终态与回填意图同一条 UPDATE) |
毒丸升级 DEAD(EXHAUSTED)(§3.2 步骤 2) |
否 | 否 | 否 | 单语句 |
回填(信箱标记 + BACKFILL_AT) |
否 | 否 | 否 | 跨库两次单写;幂等可重跑 |
人工重放(批量改回 PENDING) |
否 | 否 | 否 | 单语句批量;MessageLifecycleGate 与回填互斥 |
结论:「航班变更与处理终态同事务」只对业务型终态成立;非业务型终态不涉及跨表一致性,因此不需要 PIPELINE_LOCK,但终态与回填意图仍由同一条 UPDATE 保证不分离。
- 原文缺失归为
MALFORMED;读取异常不能伪装成“缺失”,应进入基础设施重试。 - 忽略规则(
LDM / REGN / RSTA / EROR→SKIPPED)尚未实现(§10);不能因类型未覆盖就把合法忽略报文当非法报文处理。 - 主泵不回填:终态与回填意图由同一条 UPDATE 落库,回填一律由扫描驱动(跨库写不能占用 FIFO 关键路径)。失败按退避重试,抵达超期期限 R 时强制补写,确认行不存在或达尝试上限则停止自动重试(message-lifecycle.md §5.2,US-09/Q7)。回填只需消息 ID,因此缺 META 或解码失败的死信同样可补写。
PENDING / FAILED禁止回填;影子环境禁写。 - 回填有三种结果:写入成功;此前已被标记(视为成功,不覆盖已有值);信箱行不存在(视为失败,当前无终态,见 §10)。
4. 日计划快照与请求匹配
4.1 快照发布
SCHD-DNLD 与 SCHD-RESP 共用 ScheduleProcessor.applyScheduleRecords:
- 重放判定:
PROC_STATE已存在成功终态 → 幂等成功,仅追加留痕,不重复写入。 - 整包校验:声明记录数、航班标识与运营日推导等校验失败 → 整包
DEAD(PROTOCOL),不写半包,既有状态保持不变。 - 事务写入:锁内按
FLID点查归属日,发现同一航班跨运营日即整包回滚并DEAD(PROTOCOL);通过后合并写主表与资源明细。报文未携带的航班不因本次日计划报文被删除。 - 提交结果:同一事务保存
KAFKA:schd/KAFKA:msg事件、置消息SUCCEEDED并预登记回填意图;事务提交后执行信箱回填,留痕在事务外追加。
单事务保证未提交变更整体回滚。消息重放由 PROC_STATE 的消息 ID 与业务身份控制;版本号不能单独证明消息身份。
应答守卫仍是缺口:RESP 应匹配开放 RQFD 请求(无匹配、过期或报文早于发送时间则不更新快照);当前 RESP 与 DNLD 无差别进入快照写入(§4.2/§10),不能视为 RESP 匹配闭环。
4.2 上游请求与静态数据
REQ_TRACK 表与仓储已存在(状态 PENDING / SENT / DONE / EXPIRED),但没有运行时协调器:出站 COUTMSGS 适配、请求编码、超时与应答匹配均未实现(XmlCodec.encodeRqrd 只是占位)。本节是目标机制,不是现状。
请求生命周期目标:
REGISTERED → SENT → WAITING → DONE
└──→ EXPIRED
- 注册同类新请求前使旧开放请求过期;只有
COUTMSGS写入确认后才标记SENT并关联出站记录;写信箱成功但本地未确认的情况需要补偿与去重,不能无条件重新发送。 - 应答优先按已确认的回显字段精确匹配;回显契约未确认时的降级匹配(同类开放请求且
DTTM ≥ sentAt)存在跨代误配风险,必须明确接受并审计,不能宣称精确关联。比较前统一时区和时间单位。 - 参考应答写入自有
REF_MASTER(尚未建表),日计划应答走快照流程;请求完成必须在相应数据处理成功之后,超时和迟到应答不能修改已关闭请求对应的状态。
请求与参考数据的交付范围见 user-stories.md US-08/US-13/US-14 与本文 §10。
5. 事件投递
5.1 普通事件
Dispatcher 按 TARGET 读取最小未发送 EVENT_ID(当前逐条投递只处理 KAFKA:msg)。队头退避未到期时,该目标停止推进;发送确认后才标记 SENT,失败记录次数并按退避推后,达到上限转 DEAD(记录保留作 DLQ)。所有外部调用需要有界超时,避免阻塞整个投递线程。
投递是至少一次:Kafka Broker 或其他投递目标已接受、但本地未标记成功时可能重发;目标端接受不等于业务消费者已消费。Kafka 生产约束沿用架构决策 D3(architecture.md §7),但生产者幂等不替代应用层事件去重;跨重启的事件身份和消费方去重契约仍需落实。共享出站信箱也必须单独解决重复写入,不能假设 Kafka 的保证适用于 MySQL。真实 Kafka 适配器尚未实现(DeliveryPort 仅有 stub),投递闭环须先交付适配与配置强制校验。
5.2 schd 聚合
KAFKA:schd 不进入逐条投递循环,只由 flushSchd 发送:
- 到期领取批次:
mergePendingSchd按FLID合并未发事件,每个FLID只保留最新STATE_VERSION,批次大小受flush-limit约束。 - 逐条发送:UPSERT 发送该
FLID的最新整态(key =FLID);TOMBSTONE 发送 null 值删除通知。 - 成功后把本批被代表的事件(含被最新版本合并压掉的旧事件)一起标记完成,推进
lastFlush;失败的单条增加次数并退避,达到上限转DEAD。
默认聚合周期 3 秒、批上限 500。它提供最新状态通知,不保留每次中间变化;KAFKA:msg 与 KAFKA:schd 之间不承诺顺序。
6. 失败恢复与维护作业
6.1 失败、重试与重放
ProcFailure 与 FailureScheduler 统一处理侧失败落账,投递侧(Dispatcher)按同一套次数与退避规则迁移事件。默认最多 5 次(attempts ≥ 5 判耗尽);退避表配置为 1、2、4、8、16 秒、单档封顶 60 秒,但耗尽判定与退避取值同源(attempts ≥ max-attempts 即转 DEAD,不再计算下次重试),因此默认配置下实际只用 1、2、4、8 四档:16 秒档与 60 秒封顶不会被触发(要么调高 max-attempts,要么接受"5 次尝试 = 4 档退避")。时间经可注入 Clock 判定。
失败必须在持有具体消息、事件或批次的位置记录,外层循环只做兜底日志和等待,不重复增加次数。线程中断应恢复中断标记并向上传递;不把 JVM Error 当普通业务失败捕获。
ReplayService 只允许 CODEC_ERROR / UNSUPPORTED / INFRA / EXHAUSTED 从 FAILED / DEAD 回到 PENDING:重置 ATTEMPTS=0、NEXT_ATTEMPT_AT=NULL、PROCESSING_STARTED_AT=NULL,不重置 IDENTITY_KEY(保留身份,避免重放时把自己判成重复消息)。它按错误类全局批量重放,尚无按记录预检、操作审计与管理入口(US-10)。重放与回填通过 MessageLifecycleGate 在同一实例内互斥,避免「旧回填给已重新入队的消息写标记」;【缺口】毒丸升级路径不在该 gate 内(§3.2 步骤 2),该竞态窗口登记于 §10。旧消息进入终态后后续消息可能已执行,重新入队不等于恢复历史顺序;人工重放前必须评估状态覆盖和版本保护,不能直接批量重放到生产。
维护作业:JobRunner 用独立 daemon 线程每 30 秒触发 BackfillService.sweep(调度周期,不是回填完成时限:批次积压、单行调用超时与历史作业耗时都会延长实际延迟;回填扫描自身的退避为 30 秒起步、封顶 15 分钟),每天机场时区 03:30 后触发一次 HistorySweepJob(§6.2)。回填意图在写终态的同一条语句里登记在 PROC_STATE(BACKFILL_NEXT_AT/ATTEMPTS/ERROR),不再有独立待办表。扫描谓词见 message-lifecycle.md §5.2;超过超期期限 R 后超期分支恒成立并覆盖退避,但永久失败不会无限重试:确认行不存在立即放弃、暂时性故障到 backfill-max-attempts 后停止自动重试(两类都告警且可人工恢复)。扫描按 BACKFILL_ATTEMPTS, MSG_ID 公平轮转并排除已放弃行,最旧的一批永久失败行不会再占满批次饿死后续记录。作业不再经 PUMP_JOB 队列插队,不参与消息 FIFO,也不使到期消息饥饿。作业与回填通道的生命周期口径见 message-lifecycle.md §3/§4。
6.2 历史清理与归档
航班历史清理(HistorySweepJob,每天 03:30 触发):按 HistoryProps 的保留期与终态/静默判据选出候选(含 DELETED),先写历史存储,成功后物理删除主行与明细;历史存储未接通或 msgx.history.history-store-enabled=false 时删除 0 条。未经 FDEL、由生命周期直接清除的航班,清除前补发一次删除事件。语义与红线见 flight-state.md §6,不在此重复。
留痕清理:SCHD_SNAP_LOG 保留 90 天,在历史清理窗口内按 (SCOPE_END, RECV_AT) 删除。
处理终态归档:PROC_STATE_HST 仍是目标表(user-stories.md US-11),尚未建表;不得归档 PENDING / FAILED,也不能因移走身份记录而失去业务去重能力。ES 历史投影(阶段 B)不启用。自有记录归档与信箱原文保留的关系见 message-lifecycle.md §8/§9。
共享信箱保留策略由库所有方管理;历史写入与删除事件入队之间仍需恢复方案,顺序调用不构成原子提交。
7. 接口与运行配置
兼容入口为 POST /cminmsgs/send,请求体为原始 XML,当前成功响应为 HTTP 200、text/plain 格式的信箱记录 ID。这只表示接收结果,不表示业务处理成功。请求媒体类型、字符集和失败响应仍需与现役逐项对拍;查询与其他兼容端点不能因列入需求就视为已提供。
运行配置以 application.yml、application-dev.yml 和 .env.example 为准,设计上重点区分(下列 msgx.* 参数以 msgx. 为前缀,mailbox.* 不带前缀):
msgx.pipeline.autostart与msgx.stubs:分别控制管道启动与内存适配器;生产禁止 stub,默认不自动启动。msgx.pipeline.poll-interval / claim-batch / max-attempts / backoff-ms / backoff-cap-ms / head-deadline:控制轮询节奏、批次、重试上限、退避与队头滞留;这些参数不能改变 FIFO。msgx.pipeline.max-commit-delay / overdue-backfill / backfill-batch / backfill-max-attempts:空洞老化阈值(Q2 的「ID 分配 → 事务可见时延上界」,同时决定目标补偿扫描的窗口宽度)、超期补写期限 R(Q6)、回填扫描批量与回填自动重试上限(达上限停止自动重试并可人工恢复);R 与老化阈值都不能为提速而下调。默认 5 分钟只是缺少依据的占位假定值——库方尚未给出该可见性时延上界,且它不能由 SIS 报文的Expiry(480 分钟量级)推导(message-lifecycle.md §5.1),Q2 确认前属于上线门槛。overdue-backfill(R)的完整约束见 message-lifecycle.md §5.2。msgx.pipeline.delivery-batch / delivery-drain-rounds:普通事件(KAFKA:msg)批量投递的批大小与每轮最多连取批数;把投递从"每条一次 DB 往返 + 一轮一次 sleep"提升到由下游决定,同时保留"队头失败即停止本轮"的目标内保序。msgx.operation-day.zone / cutoff-hour:运营日时区与切日边界,决定OPERATION_DAY推导(flight-state.md §2.1)。mailbox.processed-value:写回共享信箱的处理标记值,仅限库方认可的 legacy 值集(Q7)。msgx.schd.flush-period / flush-limit:控制状态通知的聚合延迟与批量大小。msgx.identity.include-day-boundary:影响去重语义,不能作为普通调优项切换;序号重置周期见 Q11。mailbox.shared-mysql.enabled与msgx.history.history-store-enabled:分别门控真实信箱与历史存储接线,默认关闭。msgx.health.backlog-cache-ttl-ms:积压快照缓存窗口(默认 30 秒,/health与/metrics共用);设为 0 仅用于测试/排障,不作为实时性的替代。msgx.pipeline.cutover-watermark:一次性、显式的切流播种(默认不配置 = 不播种)。取值为min(读当前全部现存行,W=MIN(ID)−1)、zero(从 0 按空洞规则扫描)、max(跳过当前可见存量,W=MAX(ID))或具体 ID。代码不做默认选择、也不会自动退化成max;升级实例(已有水位或已有处理记录)拒绝重新播种,重新切流必须是显式操作;播种事实记在INBOX_CURSOR.SEEDED_AT(与水位在同一条语句落库),而该列为 NULL 不等于从未消费。非法取值由启动自检挡下。msgx.pipeline.late-detect-period / late-detect-batch:只读迟到检测(ACM2-41 阶段 0)的周期与单轮复查量;周期设 0 即关闭。检测只计数与告警,不补入队、不改变处理语义。
日志关联消息 ID、事件 ID 和批次;失败记录错误分类、次数、下次执行时间。健康检查反映依赖实际可用性;队头滞留、积压、死信和补偿失败需要指标及告警。指标经 Micrometer 暴露(PipelineMetrics,启动时急切注册):msgx.pipeline.backlog.unfinished、msgx.pipeline.backlog.oldest_unprocessed_seconds、msgx.pipeline.backfill.unmarked_terminal、msgx.pipeline.backfill.abandoned、msgx.pipeline.backfill.oldest_unmarked_seconds、msgx.pipeline.watermark.lag、msgx.pipeline.hole.aged_out.total、msgx.pipeline.late_arrival.detected.total(迟到检测命中数——> 0 表示上游提交确实晚于水位推进,需要与库方对契约)。取数统一走 BacklogSnapshotProvider(msgx.health.backlog-cache-ttl-ms,默认 30 秒),/health 与 /metrics 共用同一份快照——backlog() 是 PROC_STATE 的全表聚合,不能被高频抓取打穿;无法取数时上报 NaN,不伪造 0。日志出口故障不得阻塞业务线程。
8. 验证要求
单元测试使用内存仓储和可推进的 Clock,不依赖睡眠或在线中间件。以下不变量必须有回归测试,接口级单测不能替代主泵调度测试:
| 场景 | 必须验证的结果 |
|---|---|
| 重复扫描、入队中断 | 不重复入队、不丢记录、不让后续消息越序;终态而未回填的行不得阻断后续消息发现。 |
| 较小 ID 迟到(Q2 未确认) | 【缺口 · 已固定基线用例】快路径只读 ID > W;InboxPollerTest 已把"水位越过后到达的较小 ID 不被发现"钉成基线。补偿扫描(G1)交付前禁止任何"迟到不越序"的验收声明。 |
| 空洞老化与重置 | 阈值内不推进水位、不越过入队;超期后只放行空洞本身;旧空洞补齐后出现的新空洞获得完整等待窗口。 |
| 兼容入口与空洞并发 | 兼容入口登记的行超出水位,主泵不领取;水位追平后按序处理。端到端用例:PipelineSmokeTest「compat injected high id is not claimed until the watermark catches up」。 |
| 队头失败、退避及作业竞争 | 消息不越队;到期后恢复;作业不使消息无限饥饿。 |
| 同身份多条记录、失败后重试、归档后重复 | 只产生一次有效业务处理,不把自身重试判为重复。 |
| 非业务型终态 | 不触碰 FLIGHT_SCHD / MSG_EVENT,只写 PROC_STATE,且终态与回填意图同语句生效。 |
| PG 事务失败、快照重复或迟到 | 整体回滚重试、不重复推进版本、不回退状态、不误删增量航班。 |
| 整包协议拒绝(运营日冲突、声明数不符) | 整包不落地、整体回滚,既有状态与版本不变,消息终态为 DEAD(PROTOCOL)。 |
| PG 提交失败、信箱回填失败 | 事件、处理终态与回填意图一起回滚;已提交结果只补写标记,不重放业务;中间态永不补写。 |
| 回填四种结果 | 写入成功 / 早已标记(不覆盖、记成功)/ 信箱行不存在(立即放弃并告警,不得视为已标记)/ 暂时性故障达上限(停止自动重试,可人工恢复)。 |
| 回填公平性 | 最旧的一批记录永久失败时,后续待回填记录仍能被扫描到;已放弃行不再进入扫描。 |
| 「打标即清除」语义 | 【待确认】若库方清除由打标触发,必须验证存在约定的保留期或独立原文保留通道,且不依赖 R 的取值。 |
RECEIVED_AT 为 NULL |
超期兜底不生效的行为被显式验证,且不会导致标记被提前写入。 |
| 毒丸升级与人工重放并发 | 【缺口】毒丸路径不在 gate 内;窗口必须被复现,或用 gate 覆盖后验证互斥。 |
| 投递确认丢失、批次失败、次数耗尽 | 允许可识别的重发、保持目标顺序、整批退避并保留死信。 |
| 请求超时、无匹配 RESP、时间单位不一致 | 不误用迟到应答,不提前完成请求。 |
| stub 误配置、重复实例、停机中断 | 生产拒绝不安全启动,工作线程能正确退出。 |
真实适配器还需 PG 事务与补偿集成测试、日计划崩溃恢复测试、Kafka 故障投递验证;常规验证命令为 JDK 25 下执行 ./gradlew test。
9. 实现入口
生产代码根目录为 src/main/kotlin/com/gzzn/omms/msgexchange/:
| 关注点 | 主要入口 |
|---|---|
| 收报与兼容接口 | ingress/InboxPoller.kt、InboxService.kt、InboxController.kt |
| 解码 | codec/JacksonXmlCodec.kt、SisWireMapper.kt、SisMessageBody.kt |
| 调度与处理 | processing/Pump.kt(含 MessageProcessor)、DynamicProcessors.kt(FLOP/FDEL/ADFT)、Identity.kt |
| 日计划 | processing/ScheduleProcessor.kt;请求协调尚无实现(REQ_TRACK 见 infra/persistence/) |
| 回填与投递作业 | processing/BackfillService.kt、jobs/JobRunner.kt、HistorySweepJob.kt、delivery/Dispatcher.kt |
| 持久化与恢复 | infra/persistence/、infra/retry/(ProcFailure / ReplayService / FailureScheduler) |
| 启停与配置 | PipelineLifecycle.kt、config/PipelineProps.kt、config/HistoryProps.kt |
10. 当前实现差异
以下缺口直接影响上述设计是否成立,不能以类或接口已存在作为完成依据。收报与处理链路的逐条缺口另见 message-lifecycle.md §12。
- 事务与外部副作用:业务型终态(处理器产出的
SUCCEEDED)与航班变更、待发事件、回填意图在同一 PG 事务提交;非业务型终态(MALFORMED / PROTOCOL / SKIPPED / EXHAUSTED)与毒丸升级只写PROC_STATE,终态与回填意图同一条 UPDATE,不涉及跨表一致性(§3.3 表 1)。回填意图落在PROC_STATE(BACKFILL_AT/NEXT_AT/ATTEMPTS/ERROR),BACKFILL_TODO已随 V2 迁移下线,message-lifecycle.md §4 的两个崩溃窗口(提交后回填前崩溃、回填意图二次落账失败)不再存在。回填本身仍是跨库单写,失败按退避重试并由超期期限 R 兜底;R 的取值待 Q6 确认。 - 收报与调度:水位 W 与空洞计时
HOLE_SINCE落库并与入队同事务推进,遇空洞即停、空洞超过max-commit-delay判定为永久(Q2 未书面确认前该阈值只是缺少依据的占位假定值——库方尚未给出「ID 分配 → 事务可见」的时延上界,且不能由 SIS 报文Expiry推导);旧空洞补齐后出现的新空洞会重置老化起点。较小 ID 迟提交目前没有任何发现机制:message-lifecycle.md §5.1 描述的窗口补偿扫描尚未实现,水位越过空洞后到达的较小 ID 不会被快路径(ID > W)发现,端到端顺序保证仍待与库方联合验证。HOL deadline 已改用PROCESSING_STARTED_AT(为空时以UPDATED_AT兜底)和可注入Clock;Q6 仍需确认长期积压与人工重放的期限口径。阶段 0 只读迟到检测已实装(监视被放行的空洞 ID,命中即计入msgx.pipeline.late_arrival.detected.total并告警,不补入队);补偿扫描的阶段 1(按类型安全补入队)与 Q2 契约仍待推进。 - 兼容入口与水位(已修):
POST /cminmsgs/send直接写PROC_STATE、不参与水位,因此登记行会超出水位;主泵只领msgId ≤ W,在水位追平前不领取(Pump已实现,端到端用例守住)。代价是这类消息延迟到水位追平,且必须让收报轮询运行(message-lifecycle.md §5.1/§12)。 - 回填闭环(已修 · V4):三种结果已区分(写入成功 / 早已标记 / 信箱行不存在),并新增放弃语义:确认
MISSING立即放弃、暂时性故障达backfill-max-attempts后停止自动重试,两者都告警且可由人工恢复;放弃 ≠ 标记已确认(BACKFILL_AT仍为空,清除前提不成立)。扫描改为公平轮转,饥饿问题关闭。仍存在的相关风险是"打标即清除"语义下**R无法保护重放窗口**,必须另行约定保留期或引入独立原文保留通道(message-lifecycle.md §12 G10)。 - 重放互斥:
MessageLifecycleGate只覆盖回填与人工重放,毒丸升级路径不在其中,存在「先标 DEAD 并打标、再被重放拨回 PENDING」的窗口。 - 快照与业务能力:DNLD/RESP/ADFT 与 FLOP/FDEL 处理器、整包校验与跨运营日整包拒绝均已接入;但 RESP 应答守卫与出站请求未实现,忽略规则(US-04)未实现,29 类 FLOP 与参考应答的逐类矩阵未补全,ADFT 缺失字段与
FLID重用语义待上游确认。 - 航班读写:唯一写入口与权威读已落地;ROUT/ERUT 联合主键、空值/未知属性保真、事件在事务内只算一次、逐航班多次查询仍待修正(见 flight-state.md §6)。
/all/flights尚未实现。 - 请求、参考数据与归档:
REQ_TRACK表与仓储已建,但无运行时协调与COUTMSGS出站适配;REF_MASTER未建表;PROC_STATE_HST未建表。航班历史清理脚手架已实现,历史存储未接通时删 0 条。 - 生产与运维:已有事务行锁,但没有整个实例的排他/FIFO 协调;真实 Kafka 与信箱出站适配未交付(仅 stub),配置允许环境变量覆盖
acks/幂等/in-flight;启动校验、影子隔离、告警与指标未闭环。对拍比较工具存在不等于现场对拍已完成。Oracle 11g 方言与迁移未接入验证。
业务契约与待确认事项见 user-stories.md,进度由 Plane 跟踪;本文件不维护工单流水账、测试数量或历史方案全文。