feat(processing): 快照流程锁内复核收敛 + 异常显式清除语义 + 语义矩阵定稿 (ACM2-29 P2-5/M3)

- SnapshotFlow 收敛到 v2 §5 事务顺序:锁外仅解析校验;取得 PIPELINE_LOCK 后
  锁内复核快照身份(last_message_id 重放短路)→ 锁内读代与当前航班态 →
  计算 nextState → 写入 → CAS → 事件 → SUCCEEDED。锁内 CAS 失败属数据异常,
  一律回滚 FAILED(INFRA),删除「版本号相同即视为重放」的推断路径(评审 F7/F8 收口)
- M3/F4:异常对象(FDIV/FRET/FLAB)显式清除语义——null 字面量/空对象/空串
  翻译为 Clear 命令;FlightNextState.clearedKeys 携带本次清除键;增量路径按
  映射把前缀标量列置 NULL;库内清除后读视图无该键(线格式保持键缺失,显式
  清除表达留待阶段 2 按下游契约定)
- StubFlightSchd 增量分支改为 nextState 权威整体替换(引擎已合并当前态),
  天然承载 Clear;flattenScalarsOnly 去除冗余循环(L2)
- docs/flight-state-semantics.md:16 类结构语义矩阵定稿(P0-A 交付物落库)
- 新测试:引擎 0 标记/异常清除/混排 fail-fast 单测;JDBC 异常清除回环用例

验证:MSGX_PG_PORT=5433 真实 PG ./gradlew test --rerun-tasks
105 用例 0 失败 0 跳过
This commit is contained in:
windyboy
2026-09-08 15:44:48 +08:00
parent 48a27b26dd
commit 20aafb9ab0
8 changed files with 223 additions and 46 deletions
@@ -57,35 +57,39 @@ class SnapshotFlow(
return
}
val gen = flightSchd.getGen(day)
if (gen?.lastMessageId == messageId) {
txManager.inTransaction {
procState.update(head.cminmsgsId, ProcStatus.SUCCEEDED)
}
safeBackfill(head, msg)
log.info("snapshot replay no-op id={} day={}", head.cminmsgsId, day)
return
}
val expected = gen?.version ?: 0L
val newFlids = normalized.map { it.first }.toSet()
val delFields = gen?.flids?.minus(newFlids) ?: emptySet()
val newVersion = expected + 1L
val nextStates = normalized.map { (flid, fields) ->
val current = flightSchd.findNextStateByFlid(flid)
val commands = FlightStateEngine.commandsFromFields(flid, fields, snapshotReplace = true)
FlightStateEngine.apply(current, commands, messageId, bumpVersion = true)
}
// v2 §5 事务流程:锁外只做解析与整包校验;取得 PIPELINE_LOCK 后在锁内
// 复核快照身份 → 读取当前代与当前航班态 → 计算 nextState → 写入 → 提交。
var replayNoOp = false
try {
txManager.inTransaction {
// 锁内复核快照身份:同一消息已成功提交 → 重放短路(不加版本、不重复发事件)
val gen = flightSchd.getGen(day)
if (gen?.lastMessageId == messageId) {
procState.update(head.cminmsgsId, ProcStatus.SUCCEEDED)
replayNoOp = true
return@inTransaction
}
val expected = gen?.version ?: 0L
val newFlids = normalized.map { it.first }.toSet()
val delFields = gen?.flids?.minus(newFlids) ?: emptySet()
val newVersion = expected + 1L
// 锁内读取当前航班态,计算 nextState(单写者互斥下读到的一定是已提交最新态)
val nextStates = normalized.map { (flid, fields) ->
val current = flightSchd.findNextStateByFlid(flid)
val commands = FlightStateEngine.commandsFromFields(flid, fields, snapshotReplace = true)
FlightStateEngine.apply(current, commands, messageId, bumpVersion = true)
}
flightSchd.persistNextStates(day, nextStates, snapshotReplace = true)
if (delFields.isNotEmpty()) {
flightSchd.deleteDiffByDay(day, delFields)
}
// 锁内 CAS:expected 即锁内刚读到的最新版本,仍失败属数据异常——
// 一律回滚进入可重试 FAILED(INFRA),绝不凭版本号推断“已经是我的提交”(v2 §5)
val casSuccess = flightSchd.putGenIfVersion(
day = day,
expected = expected,
@@ -97,14 +101,9 @@ class SnapshotFlow(
),
)
if (!casSuccess) {
val again = flightSchd.getGen(day)
val replayIdentity = again?.lastMessageId == messageId && again.version == newVersion
if (!replayIdentity) {
throw CasConflictException(
"gen-cas-conflict: expected=$expected current=${again?.version} msg=${again?.lastMessageId}",
)
}
log.info("gen idempotent replay within tx id={} day={}", head.cminmsgsId, day)
throw CasConflictException(
"gen-cas-conflict: expected=$expected msg=$messageId (lock-held CAS must not fail)",
)
}
val schdKind = msg.kind as? MsgKind.Schd
@@ -128,8 +127,13 @@ class SnapshotFlow(
procState.update(head.cminmsgsId, ProcStatus.SUCCEEDED)
}
// 提交后补偿:重放短路同样补做回填(其待办可能仍在重试中)
safeBackfill(head, msg)
log.info("snapshot SUCCEEDED id={} day={} flights={}", head.cminmsgsId, day, normalized.size)
if (replayNoOp) {
log.info("snapshot replay no-op id={} day={}", head.cminmsgsId, day)
} else {
log.info("snapshot SUCCEEDED id={} day={} flights={}", head.cminmsgsId, day, normalized.size)
}
} catch (e: CasConflictException) {
log.warn("gen CAS conflict -> FAILED(INFRA) id={} msg={}", head.cminmsgsId, e.message)
procFailure.fail(head, ErrorClass.INFRA, e.message ?: "gen-cas-conflict")