docs(kdoc): V2 迁移与处理/持久化层剩余注释改为直白说明

接上一提交,把本次工作范围内还没改到的注释补齐:V2 迁移脚本的注释原文几乎全是
段落编号引用,处理层与持久化层还留着一批 `(§3.3/§5)` 形式的行内注释。

V2__inbox_lifecycle.sql:文件头改为先讲清这次迁移解决的三个问题(记住收报读到哪里、
回填记录并进 PROC_STATE 而不是单开一张表、记下收信时间做什么用),再落到每个字段;
字段级注释补上"这个值非空代表什么"。SQL 语句一字未动(已用去注释后比对确认)。

processing:Pump 的分派与终态注释、ScheduleProcessor 的重放判定与运营日冲突、
DynamicProcessors 的删除通知与重新激活,都改成说明白"这一步在做什么、为什么这么做"。
infra/persistence:Repositories.kt 的仓储约定、航班状态读写、待发事件与请求跟踪接口,
JdbcPgRepositories 的锁、整态合并、历史清理判据、明细表映射,StubRepositories 的
对应实现,一律先说清用途再谈规则。
测试:8 个测试类里的行内引用改为说明这条断言在守什么。

注释里保留的文档指向只在需要延伸阅读时出现,不再作为解释本身。
全部为注释改动,测试仍为 78 passed / 1 skipped。
This commit is contained in:
windyboy
2026-09-10 11:09:19 +08:00
parent d475feb790
commit 6e7d819034
15 changed files with 154 additions and 114 deletions
@@ -60,7 +60,7 @@ class PipelineSmokeTest {
}
companion object {
/** 合法 META,但 TYPE 未在支持范围 → FAILED(UNSUPPORTED)design.md §2.3 */
/** 报文头合法,但类型没有对应处理器,会走"暂不支持、先重试"这条路 */
val UNSUPPORTED_XML = """
<MSG>
<META><SNDR>AODB</SNDR><SEQN>1</SEQN><DTTM>20260908120000</DTTM><TYPE>XYZQ</TYPE><STYP>FOO</STYP></META>
@@ -77,7 +77,7 @@ class FlywayMigrationTest {
assertEquals(setOf("flid", "operation_day", "state", "state_version", "last_msg_id"), cols)
}
// 回填事实并入 PROC_STATE(§5.2 判据 + 回填待办
// 回填相关的列都落在 PROC_STATE 上(收信时间用来判断超期,其余记录回填进度
stmt.executeQuery(
"SELECT column_name FROM information_schema.columns WHERE table_name = 'proc_state' " +
"AND column_name IN ('received_at', 'backfill_at', 'backfill_next_at', " +
@@ -91,7 +91,7 @@ class FlywayMigrationTest {
)
}
// PIPELINE_LOCK 与 INBOX_CURSOR 单行种子(§3.2 / §5.1
// 两张单行表(管道锁、收报水位)的种子数据都要在
stmt.executeQuery("SELECT count(*) FROM pipeline_lock WHERE lock_id = 1").use { rs ->
assertTrue(rs.next())
assertEquals(1, rs.getInt(1))
@@ -70,7 +70,7 @@ class InboxLifecycleJdbcSqlTest {
val row = proc.find(11L)!!
assertEquals(ProcStatus.DEAD, row.state)
assertEquals(ErrorClass.MALFORMED, row.errorClass)
assertEquals(t0, row.backfillNextAt) // 终态与回填意图同一条 UPDATE(§2/§4
assertEquals(t0, row.backfillNextAt) // 终态的同时就登记了回填待办(同一条 UPDATE
assertNull(row.backfillAt)
assertEquals(t0, row.receivedAt)
}
@@ -83,7 +83,7 @@ class InboxLifecycleJdbcSqlTest {
proc.recordBackfillFailure(11L, "mysql-down", attempts = 2, nextAttemptAt = t0.plusSeconds(120), now = t0)
val row = proc.find(11L)!!
assertEquals(ProcStatus.SUCCEEDED, row.state) // §11 终态不可逆
assertEquals(ProcStatus.SUCCEEDED, row.state) // 回填失败不改处理结果
assertEquals(2, row.backfillAttempts)
assertEquals(t0.plusSeconds(120), row.backfillNextAt)
assertEquals("mysql-down", row.backfillError)
@@ -98,11 +98,11 @@ class InboxLifecycleJdbcSqlTest {
seed(2L, t0)
proc.markTerminal(2L, ProcStatus.SUCCEEDED, now = t0)
proc.recordBackfillFailure(2L, "mysql-down", 1, t0.plus(Duration.ofMinutes(15)), t0)
// ③ 未到期但接收时间已达超期期限 R(§5.2 覆盖退避)
// ③ 退避还没到,但收信时间已经很久了:应当无视退避直接补写
seed(3L, overdue)
proc.markTerminal(3L, ProcStatus.DEAD, errorClass = ErrorClass.MALFORMED, now = t0)
proc.recordBackfillFailure(3L, "mysql-down", 1, t0.plus(Duration.ofMinutes(15)), t0)
// ④ 中间态:永不补写(§5.2
// ④ 还没处理完:不补写
seed(4L, overdue)
// ⑤ 已确认标记
seed(5L, t0)
@@ -124,7 +124,7 @@ class InboxLifecycleJdbcSqlTest {
val backlog = proc.backlog()
assertEquals(1, backlog.unfinished)
assertEquals(overdue, backlog.oldestReceivedAt) // OPS-2 最老未处理信龄锚点
assertEquals(overdue, backlog.oldestReceivedAt) // 最老一条未处理消息的接收时间
assertEquals(1, backlog.unmarkedTerminal)
}
@@ -150,7 +150,7 @@ class InboxLifecycleJdbcSqlTest {
assertEquals(third, mailbox.maxId())
assertTrue(mailbox.markProcessedIfUnmarked(second, "PROCESSED"))
assertFalse(mailbox.markProcessedIfUnmarked(second, "OTHER")) // §11 只把空标写为已处理
assertFalse(mailbox.markProcessedIfUnmarked(second, "OTHER")) // 已有标记,不覆盖
assertEquals("PROCESSED", statusOf(second))
// 已标记行仍出现在区间读结果中(发现与标记彻底解耦)
assertEquals(listOf(first, second, third), mailbox.readRange(0L, 50).map { it.msgId })
@@ -10,8 +10,8 @@ import org.junit.jupiter.api.Test
import java.time.Instant
/**
* U11:显式重放入口只允许可恢复错误从 FAILED/DEAD 返回 PENDINGattempts 清零、立即重试);
* MALFORMED(报文非法)永不被重放。
* 人工重放的规矩:只有"可以重放"的失败原因才能把消息从 FAILED / DEAD 拉回队列
* (重试次数清零,马上再试一次);报文本身非法的记录永远不允许重放。
*/
class ReplayServiceTest {
@@ -52,14 +52,14 @@ class InboxPollerTest {
assertEquals(ProcStatus.PENDING, proc.find(second)!!.state)
assertEquals(second, cursor.cursor.committedUpTo)
assertNull(cursor.cursor.holeSince)
// §5.3:消化阶段只写自有 PG,不碰信箱标记
// 收报只写自有,不碰信箱的处理标记
assertFalse(inbox.isMarked(first))
assertEquals(0, poller.pollOnce(t0)) // 重复扫描幂等
}
/**
* 回归US-01 条目 3 / §5.3「积压挡批」):终态且永不回填的行(解码失败死信)曾占满
* 有限批次使收报整体停摆——发现必须与处理标记彻底解耦
* 回归用例:处理完却永远不会回填的行(典型是解码失败死信)曾占满每一批的名额,
* 导致收报整体停摆。取新消息这件事必须和"有没有处理标记"彻底分开
*/
@Test
fun `terminal rows without a mailbox mark do not block discovery of later messages`() {
@@ -32,7 +32,7 @@ class BackfillServiceTest {
private val t0: Instant = Instant.parse("2026-09-08T03:00:00Z")
private val props = PipelineProps()
/** 可注入故障的信箱验证回填失败路径(§3 失败处理。 */
/** 可以人为制造故障的信箱,用来验证回填失败时怎么处理。 */
private class FakeMailbox(var fail: Boolean = false) : CminmsgInboxRepository {
val marked = linkedSetOf<Long>()
override fun insertRaw(rawXml: String): Long = 1L
@@ -76,7 +76,7 @@ class BackfillServiceTest {
val id = inbox.insertRaw("<MSG/>")
assertTrue(inbox.markProcessedIfUnmarked(id, "PROCESSED"))
assertFalse(inbox.markProcessedIfUnmarked(id, "OTHER")) // §11 只把空标写为已处理
assertFalse(inbox.markProcessedIfUnmarked(id, "OTHER")) // 已经有标记了,不再写第二次
assertEquals("PROCESSED", inbox.markOf(id))
}
@@ -89,7 +89,7 @@ class BackfillServiceTest {
val backfill = service(proc, inbox)
backfill.attempt(id)
backfill.attempt(id) // 重复执行无副作用(§5.2 补写只针对空标记)
backfill.attempt(id) // 重复补写没有副作用
assertNull(proc.find(id)!!.backfillError)
assertEquals(0, proc.find(id)!!.backfillAttempts)
@@ -104,7 +104,7 @@ class BackfillServiceTest {
service(proc, mailbox).attempt(901L)
val row = proc.find(901L)!!
assertEquals(ProcStatus.SUCCEEDED, row.state) // §11 终态不可逆
assertEquals(ProcStatus.SUCCEEDED, row.state) // 回填失败不会把处理结果改回去
assertEquals(1, row.backfillAttempts)
assertEquals("mysql-down", row.backfillError)
assertEquals(t0.plus(Duration.ofSeconds(30)), row.backfillNextAt)
@@ -129,7 +129,7 @@ class BackfillServiceTest {
assertNotNull(proc.find(901L)!!.backfillAt)
}
/** §5.2:超期期限 R 覆盖退避,保证库方清除前提「边界内无未标记行」在有限时间内成立。 */
/** 等得太久的消息不再等退避、直接补写:保证标记最终一定会写上。 */
@Test
fun `overdue rows bypass the retry backoff`() {
val proc = StubProcState()
@@ -145,7 +145,7 @@ class BackfillServiceTest {
assertNotNull(proc.find(id)!!.backfillAt)
}
/** §5.2:中间态PENDING / FAILED不适用超期补写——处理未完成时不打标。 */
/** 还没处理完的消息PENDING / FAILED永远不打标,等再久也不行。 */
@Test
fun `mid states are never marked even when far past the deadline`() {
val proc = StubProcState()
@@ -73,7 +73,7 @@ class FdelAndAdftProcessorTest {
assertEquals(1, tombstones.size)
assertEquals(Targets.KAFKA_SCHD, tombstones.single().target)
assertTrue(tombstones.single().payloadJson.contains("\"deleted\":true"))
// 终态 + 回填意图由处理器在自己的事务内落库(message-lifecycle §2/§4
// 终态和回填待办由处理器在自己的事务里写好
val row = proc.find(msgId)!!
assertEquals(ProcStatus.SUCCEEDED, row.state)
assertNotNull(row.backfillNextAt)
@@ -89,7 +89,7 @@ class FdelAndAdftProcessorTest {
val version = f.findMainRow("121")!!.stateVersion
val result = proc.apply(head(), msg(), FlopPayload("121"))
assertEquals(ApplyResult.Succeeded, result) // §3.3:重复 FDEL 幂等成功
assertEquals(ApplyResult.Succeeded, result) // 重复的删除报文算成功,但不再动数据
assertEquals(version, f.findMainRow("121")!!.stateVersion)
assertEquals(1, events.rows.values.count { it.eventType == EventType.TOMBSTONE })
}
@@ -102,7 +102,7 @@ class FdelAndAdftProcessorTest {
val result = FdelProcessor(StubPipelineTx(), StubPipelineLock(), f, events, StubProcState(), ObjectMapper())
.apply(head(), msg(), FlopPayload("999"))
assertEquals(ApplyResult.Succeeded, result) // §3.3:迟到/不存在幂等成功
assertEquals(ApplyResult.Succeeded, result) // 航班不存在(迟到或多余)也算成功
assertEquals(0, events.rows.size)
}
@@ -123,7 +123,7 @@ class FdelAndAdftProcessorTest {
assertEquals(ApplyResult.Succeeded, result)
val main = f.findMainRow("121")!!
assertEquals(FlightState.ACTIVE, main.state) // §3.3 生命周期重激活
assertEquals(FlightState.ACTIVE, main.state) // 已删除的航班被重新激活
assertEquals(LocalDate.of(2026, 12, 15), main.operationDay)
assertEquals("CA002", f.loadFullSnapshot("121")!!.scalars["FLNO"])
}
@@ -145,7 +145,7 @@ class FdelAndAdftProcessorTest {
assertEquals(ApplyResult.Succeeded, result)
val main = f.findMainRow("555")!!
assertEquals(FlightState.ACTIVE, main.state)
assertEquals(LocalDate.of(2026, 12, 20), main.operationDay) // §2.1:含 SODT 直接计算
assertEquals(LocalDate.of(2026, 12, 20), main.operationDay) // 报文带了计划时间,运营日直接算出来
}
@Test
@@ -167,7 +167,7 @@ class FdelAndAdftProcessorTest {
val scalars = f.loadFullSnapshot("555")!!.scalars
assertEquals("XX200", scalars["FLNO"])
assertEquals("keep-me", scalars["REMC"]) // §2.1 保守 Set-only:缺失不 Clear
assertEquals("keep-me", scalars["REMC"]) // 报文没带的字段保持原值,不清空
}
@Suppress("unused")
@@ -106,7 +106,7 @@ class ScheduleProcessorTest {
assertEquals(1, log.entries.size)
assertEquals(SnapshotResult.COMMITTED, log.entries.single().result)
assertEquals(1, log.entries.single().upserted)
// message-lifecycle §2/§4终态回填意图随业务写入在同一事务提交(不依赖主泵补写)
// 终态回填待办跟业务数据一起提交,不靠主泵事后再补
val row = proc.find(msgId)!!
assertEquals(ProcStatus.SUCCEEDED, row.state)
assertNotNull(row.backfillNextAt)
@@ -123,7 +123,7 @@ class ScheduleProcessorTest {
val result = processor(proc = proc, flights = flights, log = log)
.applyScheduleRecords(head(), message(makeBody("121" to "15DEC261723")))
assertEquals(ApplyResult.ReplaySkipped, result) // §4 重放判定:幂等成功
assertEquals(ApplyResult.ReplaySkipped, result) // 已经成功处理过,重放不重复写
assertNull(flights.findMainRow("121"))
assertEquals(SnapshotResult.REPLAY_SKIPPED, log.entries.single().result)
}
@@ -135,7 +135,7 @@ class ScheduleProcessorTest {
val schdBody = ScheduleBody(recsDeclared = 2, records = makeBody("121" to "15DEC261723").records)
val result = processor(flights = flights, log = log).applyScheduleRecords(head(), message(schdBody)) as ApplyResult.DeadProtocol
assertTrue(result.flags.contains(SnapshotFlag.RECS_DROP)) // §4:声明数量不符整包拒绝
assertTrue(result.flags.contains(SnapshotFlag.RECS_DROP)) // 声明条数和实收条数不符整包拒绝
assertNull(flights.findMainRow("121"))
assertEquals(SnapshotResult.ROLLED_BACK, log.entries.single().result)
}
@@ -143,7 +143,7 @@ class ScheduleProcessorTest {
@Test
fun `same flid across operation days is rejected whole-batch without writes`() {
val flights = StubFlightState()
// 预置 121 已归属 12-15报文声称 12-16 → §2.1 归属冲突
// 库里 121 已经属于 12-15报文却说是 12-16:运营日对不上
flights.persistFullState(
com.gzzn.omms.msgexchange.domain.flight.FlightSnapshot(
"121", LocalDate.of(2026, 12, 15), FlightState.ACTIVE, 1, mapOf("SODT" to "15DEC261723"), emptyMap(),
@@ -181,7 +181,7 @@ class ScheduleProcessorTest {
)
assertEquals(ApplyResult.Succeeded, result)
assertEquals(FlightState.DELETED, flights.findMainRow("121")!!.state) // §3.3:日计划不恢复
assertEquals(FlightState.DELETED, flights.findMainRow("121")!!.state) // 日计划不会让已删除的航班复活
assertEquals(8L, flights.findMainRow("121")!!.stateVersion)
assertEquals(SnapshotResult.COMMITTED, log.entries.single().result)
assertTrue(log.entries.single().flags.contains(SnapshotFlag.SCHD_REVIVE_CONFLICT))
@@ -195,7 +195,7 @@ class ScheduleProcessorTest {
proc.insertIfAbsent(msgId, null)
val p = processor(proc = proc, flights = flights, log = log, tx = TxRunner { true })
// design.md §2.3:数据库/内部故障 → 异常上抛,MessageProcessor 记 FAILED(INFRA) 退避;不写任何终态
// 数据库故障时异常直接上抛,MessageProcessor 记成可重试的失败;这里不留下任何终态
org.junit.jupiter.api.Assertions.assertThrows(IllegalStateException::class.java) {
p.applyScheduleRecords(head(), message(makeBody("121" to "15DEC261723")))
}