From f0411da5fa4e25e0570aa263f01f84d9b38c635d Mon Sep 17 00:00:00 2001 From: windyboy Date: Sun, 6 Sep 2026 22:30:03 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E8=A1=A5=E5=85=85=E6=9E=B6=E6=9E=84?= =?UTF-8?q?=E6=96=87=E6=A1=A3=E4=B8=8E=E8=AE=BE=E8=AE=A1=E6=96=87=E6=A1=A3?= =?UTF-8?q?=EF=BC=88docs/architecture.md=20+=20docs/design.md=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - architecture.md:系统定位与双跑策略、技术栈、总体拓扑(两条单线程管道)、 模块职责、关键决策 D1–D8、数据边界(六辅助表 vs legacy 旧表)、两阶段权威 与就绪度(诚实口径:生产默认不可服务,前置 U05/U07/U09/U13/U15)、 部署与安全姿态、可观测性。 - design.md:状态机与错误分类(含重放白名单)、六表数据模型(含 U18 已知缺口)、 流程 1–4 与泵作业语义、失败/重试统一设计(U08)、不变量 I1–I5 落点、 参数表、测试策略、可观测性、缺口清单对照 ACM2-10 U01–U30。 - README:新增文档导航;关联节按 ACM2-10 T16 修正改为 ACM2 现行入口 (ACM2-3 架构权威 / ACM2-4 脚手架 / ACM2-10 评审),ACMA 仅归档。 --- README.md | 12 ++- docs/architecture.md | 143 +++++++++++++++++++++++++++++ docs/design.md | 208 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 361 insertions(+), 2 deletions(-) create mode 100644 docs/architecture.md create mode 100644 docs/design.md diff --git a/README.md b/README.md index 654c4d6..8486b68 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,15 @@ MICRONAUT_ENVIRONMENTS=dev ./gradlew run # dev stub 冒烟:内存仓储 + > dev/shadow 冒烟装配:`msgx.stubs=true`(内存仓储/适配层,见 infra/stub)+ > `msgx.pipeline.autostart=true`(PipelineLifecycle 拉起专用线程,U07);生产默认两者关闭。 +## 文档 + +- [docs/architecture.md](docs/architecture.md):架构速览——总体拓扑、模块职责、关键决策、 + 数据边界、部署与安全姿态、可观测性、就绪度(与代码同步维护)。 +- [docs/design.md](docs/design.md):设计细节——状态机与错误分类、数据模型、核心流程语义、 + 失败/重试/重放统一设计、不变量落点、参数表、测试策略、已知缺口(对照 ACM2-10)。 + ## 关联 -Plane `airport_chengdu_msgexchange_api`:ACMA-9(本阶段跟踪)、ACMA-8 v4(架构)、 -ACMA-6(技术选型)、ACMA-3(总计划)、ACMA-4(行为对拍基线)。 +Plane `airport_chengdu_msgexchange_api`(**ACM2 为现行入口**):ACM2-3(综合架构 v4, +架构权威)、ACM2-4(脚手架跟踪)、ACM2-10(评审与实施计划 U01–U30);ACMA 系列仅作 +归档历史/迁移来源(ACMA-8 v4 / ACMA-6 选型 / ACMA-9 JDK 口径在归档中可溯)。 diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..b866061 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,143 @@ +# msgexchange-v2 架构文档 + +> 现行架构权威为 Plane `airport_chengdu_msgexchange_api` 工作区的 **ACM2-3(综合架构 v4)**; +> 脚手架跟踪 **ACM2-4**,评审与实施计划(U01–U30)**ACM2-10**。本文是仓库内的架构速览, +> 与代码同步维护;两者冲突时以 ACM2-3 为准并回改本文。 +> 配套设计细节见 [design.md](design.md)。 + +## 1. 系统定位 + +新一代机场消息交换服务(AODB 报文接入 → 处理 → 对外投递),替换 legacy +`msgexchange-api`(Java 8 / Spring Boot 1.5 / Maven)。过渡策略为**双跑三步**: + +``` +影子对拍(同入口双收,比对输出)→ 切流(nextgen 权威)→ 旧仓库冻结 +``` + +- legacy 维护不受本仓库影响;本仓库不声明 legacy 旧表 schema(见 §6 数据边界)。 +- wire 契约冻结:消息结构唯一事实源为 `SIS_AODB_RMS-V0.1.md` + `doc/unisysaodbsis.xsd`; + HTTP 端点路径与响应语义沿用现役(如 `POST /cminmsgs/send` 返回记录 ID)。 + +## 2. 技术栈 + +| 层 | 选型 | 说明 | +|---|---|---| +| 语言/运行时 | Kotlin 2.3 + JDK 25 | JDK 21 不可行(Micronaut 5.1 系要求 JVM 25+,ACMA-9 实测) | +| 框架 | Micronaut 5.1.3 | 编译期 DI:KSP(`kotlin-ksp` + `micronaut-inject-kotlin`)生成 `*$Definition` | +| 持久化 | MySQL + Flyway | 仓储现为接口(Micronaut Data JDBC 实装属 U05,阶段 1 后续) | +| 权威存储 | Redis(阶段 A) | flightInfo hash;仅主泵线程写(I5);Lua 脚本原子覆盖 | +| 投递 | Kafka(acks=all + 幂等) | outbox 模式,经 MSG_EVENT 表中转 | +| 投影(阶段 B) | Elasticsearch + Redis 投影 + FLIGHT_STATE | 仅阶段 B 启用(`msgx.phase`) | +| 注册中心 | Eureka(Micronaut 原生键) | 服务名契约 `msgexchangeapi`(影子 `msgexchangeapi-shadow`) | +| 可观测 | logstash TCP(Async 包装)+ MDC traceId + 自定义健康指示器 | 见 [design.md §8](design.md) | + +## 3. 总体拓扑 + +``` + ┌──────────────────────────────────────────────────┐ + │ msgexchange-nextgen │ + │ (单实例 · 单写者) │ + AODB/上游 ──HTTP──▶│ ingress │ + │ InboxService ──事务1──▶ CMINMSGS(原文) │ + │ └▶ PROC_STATE(PENDING) │ + │ │ + │ processing(msgx-pump 线程,严格 FIFO 队头) │ + │ Pump ──tick──▶ MessageProcessor │ + │ │ │ decode(XmlCodec) │ + │ │ │ identity 绑定(I3) │ + │ │ │ Handler.decide(纯函数) │ + │ │─Schd DNLD──▶ SnapshotFlow(流程4) │ + │ │─PUMP_JOB───▶ JobExecutor(同队列,决策1) │ + │ │ │ + │ ├────Redis Lua──▶ Redis flightInfo(A权威) │ + │ └──事务2──▶ MSG_EVENT(outbox)+ 回填+SUCCEEDED│ + │ │ + │ delivery(msgx-dispatcher 线程,每 target FIFO) │ + │ Dispatcher ──逐条──▶ Kafka(msg) │ + │ └─flushSchd 聚合─▶ Kafka(schd) │ + │ (阶段 B 追加:ES flight_hts → Redis 投影删除) │ + └──────────────────────────────────────────────────┘ + │ │ + ▼ ▼ + 下游 Kafka topic Eureka / logstash +``` + +要点: + +- **两条专用 daemon 单线程**(`msgx-pump` / `msgx-dispatcher`)由 `PipelineLifecycle` + 在 `ServerStartupEvent` 后拉起,不占用 Netty event loop;停机 `requestStop` + + interrupt + join(U07)。仅当 `msgx.pipeline.autostart=true` 时装配——生产默认关, + 当前属**有意脚手架门禁**(生产可运行需先完成 U05 数据层实装,见 §7)。 +- **单写者约束(I5)**:阶段 A 全部 Redis 写集中在主泵线程;实例数必须为 1 + (运行期租约/选主保护属 U26,尚未实装,当前靠部署拓扑约束)。 +- **统一 FIFO(决策 1)**:定时作业(cron → PUMP_JOB 入队)与消息同队列, + 作业产物不绕过队头顺序;job 与队头的先后目前为入队时间近似, + 统一序号列属 U15(未实装,见 design.md §9 缺口清单)。 + +## 4. 模块职责 + +| 包 | 职责 | 对应 ACMA-8 | 主要类 | +|---|---|---|---| +| `ingress/` | 收报事务1:原文落库 + 伴生 PENDING 行;不解析报文 | 流程 1,I3 | `InboxController` `InboxService` | +| `processing/` | 主泵:FIFO 领取、解码、identity 绑定、纯函数决策、事务2 | 流程 2/4,I1/I2/I5 | `Pump` `MessageProcessor` `SnapshotFlow` `Identity` `Handler(Registry)` | +| `delivery/` | 投递:每 target 严格 FIFO、schd 聚合 | 流程 3 | `Dispatcher` `SchdAggregation` | +| `jobs/` | 泵作业:清场/归档/投影重建(经 PUMP_JOB 同队列) | 流程 4/5/7,I4 | `JobExecutor` `HistorySweepJob` `ArchiveJob` `ProjectionRebuildJob` | +| `codec/` | XML 解码 + 失败分类(MALFORMED vs CODEC_ERROR) | 决策 4 前置 | `XmlCodec` `DecodeResult` | +| `domain/` | 状态机枚举、事件/决策模型、Phase 开关 | I1–I5 | `ProcState` `MsgEvent` `Decision` `MsgKind` | +| `infra/` | 仓储接口、重试策略、Redis Lua、stub、健康、日志 | 数据模型节 | 见 design.md | +| `config/` | `PipelineProps` 参数表(ACMA-8 参数初值) | — | `PipelineProps` | + +## 5. 关键架构决策 + +| # | 决策 | 落点 | +|---|---|---| +| D1 | 作业与消息同队列(cron 只经 PUMP_JOB 入队,产物不绕过队头) | `Pump.tick` / `JobExecutor` | +| D2 | 阶段 B:ES 投递成功后同线程**同步** enqueue 删除事件(不轮询 ack) | `Dispatcher.tick`(定案 2) | +| D3 | schd 唯一出口是 flushSchd 批量聚合(逐条循环显式排除 KAFKA_SCHD) | `Dispatcher.tick`(U06/N03) | +| D4 | 未实装 ≠ 非法:无 handler / staging 未实装 → FAILED(UNSUPPORTED) 可重放,绝不写终态 | `MessageProcessor` `SnapshotFlow`(U10/N21) | +| D5 | 失败迁移在持有具体 head/batch 的边界完成;loop 只作最后防线,不吞 InterruptedException/Error | `MessageProcessor` `Dispatcher`(U08) | +| D6 | 接口驱动 + 假仓储单测;时间一律经可注入 `Clock` | `infra/persistence` `FailureScheduler` | +| D7 | stub 装配门禁:`msgx.stubs=true` 才装配内存实装;与 `autostart` 组合支撑 dev 冒烟 | `infra/stub`(U07/U01) | +| D8 | 编译期 DI(KSP)+ 启动期冒烟测试锁定 BeanDefinition 生成 | `build.gradle.kts`(U01) | + +## 6. 数据边界 + +- **本仓库 Flyway 只建六张辅助表**:`PROC_STATE` / `MSG_EVENT` / `REF_DATA` / `REQ_TRACK` / + `PUMP_JOB` / `FLIGHT_STATE`(`V2.0.0__aux_tables.sql`)。 +- `CMINMSGS` / `CMINMSGS_HST` / `COUTMSGS` 等 legacy 旧表归 legacy 仓库维护(冻结期), + 本仓库不重复声明;**全新空库需先建 legacy schema**,否则收报首句 SQL 报表不存在 + (README「数据库初始化」节)。 +- 回滚兼容关键:SUCCEEDED 时回填 `CMINMSGS.SUBSYSTEM_*` + `DATE_PROCESSED/STATUS`, + 旧系统可按自身语义无缝接管(Runbook 第 7 步)。 + +## 7. 两阶段权威与当前就绪度 + +| 阶段 | 权威 | 投递目标 | 状态 | +|---|---|---|---| +| A(`msgx.phase=A`) | Redis flightInfo | KAFKA:msg、KAFKA:schd | 管道骨架+重试闭环已实装;Redis Lua/实仓储属 U05/U09 | +| B(`msgx.phase=B`) | FLIGHT_STATE + 投影 | + ES:flight_hts、REDIS:flightInfo | 未实施(阶段 2 后) | + +**就绪度(诚实口径,2026-09-06)**:可编译、可测试(37 测试全绿)、dev stub 冒烟可端到端; +生产默认配置**不可对外服务**——`autostart=false` 且生产(stubs=false)下仓储无实装、DI 装配 +即失败。生产就绪前置:U05(数据层+事务)、U07 fail-fast 定案、U09(快照恢复协议)、 +U13(投递毒丸补全)、U15(统一序号)。逐项状态见 ACM2-10「定稿实施计划」。 + +## 8. 部署与安全姿态 + +- **实例数 = 1**(主泵单写者前提);双实例误配当前无运行期防护(U26:租约/DB 锁 + 拒启,未实装)。 +- **影子隔离**:服务名(`msgexchangeapi-shadow`)+ 独立 schema + Redis key 前缀 + 独立 topic + 三层隔离;`msgx.register-eureka=false` 可脱离注册中心对拍。 +- **网络信任模型**:`/cminmsgs/send` 无鉴权(沿用现役内网信任姿态);eureka default-zone + 回退 `127.0.0.1:8761`;口令/端点全部环境变量外置(零入库)。安全节细化属 U28。 +- **管理端点**:`/env`、`/beans` 默认 sensitive,仅 dev/影子环境放开(`application-dev.yml`)。 +- **同名单风险**:影子与生产同名同路径会互相收报——切流前必须核对服务名三隔离。 + +## 9. 可观测性 + +- **日志**:logstash TCP JSON 通道(Async + neverBlock 降级,logstash 不可达不阻塞业务线程); + 结构化生命周期日志(收报/SUCCEEDED/SKIPPED/FAILED/DEAD/毒丸/flush 批次);MDC `traceId` + (当前 = cminmsgsId/eventId,处理片段;贯穿收报→投递属 U12 遗留)。 +- **健康**:`/health` 聚合 `redis-flight-store` / `kafka-delivery` 自定义指示器——真实 ping + 判定(false/异常→DOWN,缺 bean→DOWN),非仅 bean 存在。 +- **指标缺口**:micrometer 队列深度/投递延迟 gauge 未引入(版本对齐待 U05 批次); + DEAD/DLQ 告警出口与一致性哨兵实装(U25)未落地——告警当前以 ERROR 日志为落点。 diff --git a/docs/design.md b/docs/design.md new file mode 100644 index 0000000..459f76f --- /dev/null +++ b/docs/design.md @@ -0,0 +1,208 @@ +# msgexchange-v2 设计文档 + +> 本文对应仓库当前实现,给出模块级设计语义与依据;架构总览见 +> [architecture.md](architecture.md),权威架构为 Plane **ACM2-3**,实施计划与逐项验收为 +> **ACM2-10(U01–U30)**。文中标注「TODO/未实装」的条目均为已知开放项,不属文档遗漏。 + +## 1. 领域模型 + +### 1.1 状态机与错误分类 + +``` +ProcStatus(PROC_STATE.STATE,消息处理侧): + PENDING ──处理成功──▶ SUCCEEDED(终态,回填 CMINMSGS) + │ ──同 identity 已绑定──▶ SKIPPED(终态,lastError=duplicate-of:) + └──失败──▶ FAILED(非终态,attempts+1 + nextAttemptAt 退避) + │ attempts ≥ maxAttempts 或 队头滞留超 head-deadline + ▼ + DEAD(终态/DLQ,ERROR_CLASS=EXHAUSTED 规范化) + +EventStatus(MSG_EVENT.STATE,投递侧): + PENDING ──▶ SENT;失败退避回 PENDING;attempts 耗尽整批/单条 → DEAD(DLQ) + +ErrorClass(两侧共用): + MALFORMED 报文非法 → 直接 DEAD,永不重放 + CODEC_ERROR 可随 codec 修复 → FAILED 可重放(白名单内) + UNSUPPORTED 能力未实装 → FAILED 可重放(白名单内) + INFRA 基础设施抖动 → FAILED 可重放(白名单内) + EXHAUSTED 重试耗尽(终态规范化类)→ 人工复核后可重放(白名单内) +``` + +- 「未实装 ≠ 非法」是通用规则(D4):无 handler、快照 staging 未实装都写 + `FAILED(UNSUPPORTED)`,绝不写终态——阶段 2 前接入流量不会把报文变砖。 +- 显式重放入口 `ReplayService`:仅白名单 + `{CODEC_ERROR, UNSUPPORTED, INFRA, EXHAUSTED}` 可从 FAILED/DEAD 回 PENDING + (ATTEMPTS=0、NEXT_ATTEMPT_AT=NULL,errorClass/lastError 保留审计); + 含 MALFORMED 的请求对该类静默忽略。运维接口(controller/runbook)属 U11 遗留。 + +### 1.2 报文模型(sealed 分派) + +- `MetaFields(sndr, type, styp, seqn, dttm)`——实名沿用 legacy META.java。 +- `MsgKind` sealed:`Schd(RESP|DNLD|ADFT)` + `Flop(29 类 STYP)`;`typeTag` 产出 + `SCHD-XXX` / `FLOP-xxx`,与 `HandlerRegistry.keyOf` 同源(查表键=日志类型,禁止分叉)。 +- 解码失败二分:`DecodeResult.Err(MALFORMED)` → DEAD;`Err(CODEC_ERROR)` → FAILED 退避 + (T06/U11)。 +- Handler 为**纯函数**:`decide(flightView, msg) → Decision`(flightChanges / msgNotifies / + schdPush / outboundIntents / refUpserts),不触碰 Redis/Kafka——副作用全部由泵边界执行。 +- Handler 实装:0/32(骨架),翻译属阶段 2/3,逐条对照 ACM2-4 行为基线与 KEEP/FIX 矩阵。 + +## 2. 数据模型(六辅助表) + +`db/migration/V2.0.0__aux_tables.sql`(legacy 旧表不在本仓库声明,见 architecture.md §6): + +| 表 | 角色 | 关键列/约束 | +|---|---|---| +| PROC_STATE | 消息处理伴生状态(不动 CMINMSGS 旧列) | `UK_PROC_IDENTITY(IDENTITY_KEY)` 唯一约束=I3 依据;`IDX_PROC_HEAD(STATE, CMINMSGS_ID)`=队头查询 | +| MSG_EVENT | 统一投递 outbox | `EVENT_ID` 自增=全序;`IDX_EVT_HEAD(TARGET, STATE, EVENT_ID)`=每 target 队头 | +| REF_DATA | 21 类参考数据 + SCHD_GEN | `VERSION` 列支撑流程 4 CAS;`SOURCE` 区分 ADMINAPI/AODB/PIPELINE | +| REQ_TRACK | 15 类请求状态机 | REGISTERED/SENT/WAITING/DONE/EXPIRED | +| PUMP_JOB | 泵作业队列 | kind:ARCHIVE/HISTORY_SWEEP/PROJECTION_REBUILD | +| FLIGHT_STATE | 阶段 B 权威 | `replaceDay` 单事务删差集+写新代+版本提升 | + +已知 DDL 缺口(U18,未修):`REQ_TRACK.COUTMSGS_ID` 应 INT→BIGINT;`FLIGHT_STATE` +主键应含 FDAY;时间列 TIMESTAMP(秒级+会话时区)应 DATETIME(6)/显式 UTC,否则退避/毒丸 +判定存在系统性偏移风险。 + +## 3. 核心流程设计 + +### 3.1 流程 1:收报(`InboxService.accept`) + +事务 1 = `insertRaw`(CMINMSGS 原文)+ `procState.insert`(伴生 PENDING 行); +不解析报文、接收层无唯一约束(I3)。响应 = 记录 ID(「已持久化」语义,与现役逐字对拍 +后固化,U16)。`wakePump()` 目前为 TODO 空操作——泵 1s 轮询兜底,唤醒仅为加速。 +事务边界随 U05(@Transactional + allopen)补齐。 + +### 3.2 流程 2:主泵 tick(`Pump.tick`) + +每 tick 按序判定: + +1. `headQueued()` 取作业、`headUnfinished()` 取最小未完成 CMINMSGS_ID(**含 FAILED**, + 消息间严格保序:队头退避未到期即 sleep 至到期点,后方消息永不越队)。 +2. `jobBefore(job, head)`:head 为空或队头 FAILED 时作业先行——**当前为入队时间近似, + 与「统一 FIFO」注释存在已知偏离(U15 未实装)**;统一序号列定案后消除。 +3. 队头 FAILED 且退避未到期:`poisoned()` 判定(attempts≥maxAttempts 或滞留超 + head-deadline 10m)→ DEAD(EXHAUSTED) 毒丸升级(Pump 侧;投递侧同语义属 U13,未实装); + 否则 sleep 至 nextAttemptAt。 +4. 正常队头 → `MessageProcessor.processOne`: + 入口守卫(FAILED 且已 exhausted → DEAD)→ `rawOf` 缺失 → DEAD(MALFORMED) → + decode(MALFORMED→DEAD / CODEC_ERROR→FAILED)→ identity 首绑 + (`tryBindIdentity` 失败 → SKIPPED,I3)→ Schd DNLD → `SnapshotFlow` → + 其余 → `Handler.decide(redis.hgetAllFlightInfo(), msg)` → + 阶段 A:Redis 先写(I2 happens-before,TODO redisApply)→ 事务 2: + MSG_EVENT 插入 + CMINMSGS 回填 + SUCCEEDED。 +5. 异常边界(U08):`processOne` 内 try/catch → `ProcFailure.fail(INFRA)`(attempts+1、 + 退避、达上限 DEAD);`InterruptedException` 恢复中断位后**上抛**;loop 仅 catch + `Exception` 作最后防线,`Error` 任其终止进程(异常必可见)。 + +幂等键(I3):`SNDR|TYPE|STYP|SEQN`(`Identity.of` 唯一入口);「含日边界」可配置且 +**默认关闭**(SEQN 重置作用域 CONFIRM 前,上线后不改幂等键)。 + +### 3.3 流程 3:投递(`Dispatcher`) + +- 每 target 严格 FIFO(I1 双层同策略):`headUnsent` 取队头;队头退避未到期 → 等待不跳过。 +- 逐条循环显式排除 `KAFKA_SCHD`(D3/U06):schd 唯一出口 `flushSchd`—— + `claimBatch`(ORDER BY EVENT_ID)→ 按 FLID 分组取 max(EVENT_ID)(FIX:现役 buffer + 无去重会重发旧值)→ FLTR JSON 数组一次发出;失败整批 attempts+1 退避、队首未到期不 + claim、`lastFlush` 仅成功后推进;达上限整批 DEAD(DLQ)。周期/批上限取参数表 + (3s / 500)。 +- 轮询间隔取参数表(下限 50ms,N18);无 200ms 硬编码。 +- 阶段 B(定案 2/D2):ES 投递成功 → 同线程同步 `insertSync` 删除事件 + (`deleteOf`:Jackson 结构化序列化,refs 可空恒合法 JSON——U14)。 + +### 3.4 流程 4:日计划快照(`SnapshotFlow`) + +``` +staging(流式解析+整包校验,TODO 阶段2;未实装→FAILED(UNSUPPORTED)) + → Redis Lua SNAPSHOT_REPLACE(同一 hash 原子「覆盖新代+按代差删」,删除集=旧代flids−新代) + → putGenIfVersion CAS(version 未变才写;CAS 后重放=version 已达标→no-op 成功) + → SUCCEEDED +CAS 冲突 → FAILED(INFRA)+退避(串行泵下不应发生→告警语义) +``` + +**已知缺口(U09,未定案)**:Lua 与 CAS 分属两存储,非同一事务;崩溃窗口 +(Lua 后/CAS 前、CAS 后/SUCCEEDED 前)与幂等重放判据(版本不二次自增)的显式恢复协议 +待定案并补测试。 + +### 3.5 泵作业(`JobExecutor`,经 PUMP_JOB 同队列) + +- HISTORY_SWEEP(3:30 清场,I4 同步链):判史 → 同步写 ES → 仅删成功集。 + **占位门禁(U10/T07 修订)**:ES saveSync 接线前 `pickHistory` 恒空集、删除量恒 0, + 禁止「全量可删」fail-open 默认;现役五条判史规则 golden 通过后才允许接线。 +- ARCHIVE(3:00):1 天前且仅终态(SUCCEEDED/SKIPPED/DEAD)可迁 CMINMSGS_HST(TODO)。 +- PROJECTION_REBUILD(阶段 B 切入时全量重建,TODO activeDays)。 + +## 4. 失败与重试统一设计(U08) + +| 侧 | 组件 | 迁移语义 | +|---|---|---| +| ProcState(处理/快照) | `ProcFailure` + `FailureScheduler` | attempts+1 → exhausted ? DEAD(EXHAUSTED) : FAILED+nextAttemptAt | +| MsgEvent 逐条(投递) | `Dispatcher.retryOrDead` | 同上(DLQ 保留行,attempts 审计) | +| MsgEvent 批量(schd) | `flushSchd` 整批 | 队首未到期不 claim;整批退避;达上限整批 DEAD | + +- 退避表 `[1s,2s,4s,8s,16s]`,单档封顶 `backoff-cap-ms=60s`;`attempt≤0` 兜底首档(N28)。 +- 时间一律经可注入 `java.time.Clock`(`TimeFactory`;测试用 MutableClock,无真实睡眠)。 +- loop 兜底 catch 不做状态迁移(迁移已在边界完成),仅防线程静默死亡。 + +## 5. 不变量与实现落点 + +| 不变量 | 语义 | 落点 | 状态 | +|---|---|---|---| +| I1 | 单写者严格 FIFO + HOL 阻塞 + 毒丸升级 | `headUnfinished`/`headUnsent` 队头语义、`poisoned()` | 实装(job 相对队头的全序属 U15) | +| I2 | Redis 先写、后于事件创建(happens-before) | `processOne` 阶段 A 分支 | TODO redisApply(流程占位已留) | +| I3 | identity 首绑幂等;接收层无唯一约束;SUCCEEDED 回填 | `Identity`/`tryBindIdentity`/`backfillOnSuccess` | 实装 | +| I4 | 清场仅删 ES 成功集;按代差删 | `HistorySweepJob`/SNAPSHOT_REPLACE delFields | 门禁实装,ES 接线 TODO | +| I5 | 阶段 A Redis 写仅主泵线程;Delivery 不写 Redis | 单线程拓扑 + Targets.phaseA | 实装(拓扑约束,U26 运行期保护未做) | + +## 6. 配置参数(`msgx.*`,ACMA-8 参数表初值) + +| 键 | 默认 | 说明 | +|---|---|---| +| `phase` | A | 阶段总开关(A/B 权威切换) | +| `service-name` | msgexchangeapi | 契约冻结;影子= msgexchangeapi-shadow | +| `pipeline.poll-interval` | 1s | 泵轮询节律(KEEP 现役) | +| `pipeline.max-attempts` | 5 | 处理/投递同值 | +| `pipeline.backoff-ms` / `backoff-cap-ms` | 1s..16s / 60s | 指数退避表与封顶 | +| `pipeline.head-deadline` | 10m | 队头滞留上界(毒丸升级) | +| `pipeline.autostart` | **false** | 生命周期门禁:true 才装配 Pump/Dispatcher 线程(dev+stubs 开) | +| `schd.flush-period` / `flush-limit` | 3s / 500 | schd 聚合节律与批上限 | +| `identity.include-day-boundary` | false | 幂等键日边界(CONFIRM 前禁开) | +| `consistency-check.on-startup` / `daily-sample-ratio` | true / 0.01 | 一致性哨兵(实装属 U25) | + +基础设施键位口径(Micronaut 5.1,U03):`datasources.default.*`、 +`flyway.datasources.default.*`、`kafka.producers.default.*`、`eureka.client.*`; +logback 独立于本文件,环境变量前缀 `MSGX_LOGSTASH_*`。 + +## 7. 测试策略 + +- **接口驱动 + 假仓储**:管道语义全部离线单测(无 DB/Redis/Kafka),时间用 `MutableClock`。 +- **不变量测试**:FIFO/HOL、schd 批退避与 DLQ、毒丸升级、重试上限、重放白名单、 + 聚合最新态、配置绑定、DI 装配冒烟(PipelineSmokeTest 端到端:收报→FAILED(UNSUPPORTED) + →重放→schd 聚合发出)。 +- 现状 37 测试全绿(`./gradlew test`;wrapper 钉 9.6.1 + JDK 25;内网构建设 + `GRADLE_USER_HOME`/`TMPDIR` 指向可写目录)。 +- **U29 门禁(规划)**:不变量清单化入 CI 红即阻塞;「新增逻辑必伴生不变量测试」入贡献约定。 + +## 8. 可观测性设计(U12 已落地部分) + +- logstash TCP 经 AsyncAppender(queueSize 4096 / neverBlock / discardingThreshold 0): + logstash 不可达丢弃日志而非阻塞业务线程(N31b 顺序约束:先降级再补日志)。 +- MDC `traceId` = cminmsgsId/eventId(`TraceLog.withTrace`),覆盖处理/投递日志片段; + 收报→投递全链贯穿与 micrometer gauge(队列深度/投递延迟)未实装。 +- 生命周期结构化日志:收报 INFO、SUCCEEDED/SKIPPED INFO、FAILED WARN、DEAD/毒丸 ERROR、 + flush 批次 INFO、整批 DLQ ERROR——告警暂以 ERROR 日志为落点(DEAD 告警出口属 U13/U25)。 + +## 9. 已知缺口与定案待办(对照 ACM2-10) + +| 项 | 缺口 | 计划 | +|---|---|---| +| U05 | 仓储接口无实装;`@Transactional`/allopen 未引入 → 生产 DI 装配失败、事务1/2 未原子 | 阶段 1(Micronaut Data JDBC + allopen + 事务生效回归) | +| U07/U26 | `autostart` 默认关=有意门禁,但生产无 fail-fast;双实例无运行期防护 | fail-fast 定案 + 租约/DB 锁拒启 | +| U09 | 快照跨存储恢复协议未定案 | 崩溃窗口清单 + 幂等判据 + 测试 | +| U13 | 投递侧无 createdAt/headDeadline 超时升级、无 DEAD 告警出口 | WP2 | +| U15 | job 与队头消息无统一全序(入队时间近似) | 统一序号列定案 | +| U16 | `/cminmsgs/send` 无 @Consumes/字符集、无错误路径契约(@ControllerAdvice) | WP2(legacy 逐字对拍固化) | +| U18 | §2 所列 DDL 缺口 | WP2 | +| U19–U21 | 请求状态机量纲/死分支、identity 绑定静默跳过、REF_DATA 接口/DDL 对齐 | WP2 | +| U22–U24 | 载荷类型收敛、eventSeq 未接线、每报文全量读语义定案 | WP3 | +| U25/U28/U30 | 一致性哨兵实装、README 安全节/入口、索引与杂项 | WP3/4 | +| U27 | 专有材料(SIS md 703KB / XSD 版权头)治理决策 | WP4(ACL 核验先行) |