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:
@@ -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>
|
||||
|
||||
+2
-2
@@ -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))
|
||||
|
||||
+6
-6
@@ -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 返回 PENDING(attempts 清零、立即重试);
|
||||
* 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")))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user