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
@@ -14,17 +14,21 @@ import java.time.LocalDate
import java.time.ZoneId
// =====================================================================
// 仓储契约(docs/flight-state.md §2 权威模型/§3 写入语义,design.md §2.1
// 决策层写路径约定:所有 FLIGHT_SCHD 及明细写操作必须发生在
// 「持有 PIPELINE_LOCK 的同一事务」内(§1/§4:锁只串行化 DB 事务)。
// 仓储接口定义
//
// 有一条约定贯穿航班相关的写操作:改航班状态(FLIGHT_SCHD 和明细表)必须发生在
// 同一个事务里,并且进事务后第一件事就是拿 PIPELINE_LOCK 这把单行锁。锁只负责让
// 写事务排队,不负责选主或故障切换。
//
// 航班模型与合并规则的完整说明见 docs/flight-state.md。
// =====================================================================
/** 自有 PG 单事务原子保障(flight-state.md §1/§4状态、事件、处理终态同事务提交)。 */
/** 把一段代码包进一个数据库事务:航班状态、待发事件、处理终态要么一起提交,要么一起回滚。 */
interface PipelineTransactionManager {
fun <T> inTransaction(block: () -> T): T
}
/** 单行锁 PIPELINE_LOCK:事务内第一步 SELECT ... FOR UPDATE,串行化状态写事务(§1/§4。 */
/** 单行锁:进事务后先 `SELECT ... FOR UPDATE`,让并发的状态写事务排队执行。 */
interface PipelineLockRepository {
fun lock()
}
@@ -117,7 +121,12 @@ data class BackfillDue(val msgId: Long, val attempts: Int)
*/
data class Backlog(val unfinished: Int, val oldestReceivedAt: Instant?, val unmarkedTerminal: Int)
/** MSG_EVENT outboxflight-state.md §5)。KAFKA_SCHD 合并同 FLID 未发事件按最新 STATE_VERSION 输出。 */
/**
* 待发事件(outbox)表:业务提交时把要发的事件一起写进来,投递线程再从这张表往外发,
* 这样发送失败不会影响业务事务。
*
* 其中 KAFKA_SCHD 是"最新状态"通知:同一条航班的多个未发事件只留最新的那个版本。
*/
interface MsgEventRepository {
fun insertAll(events: List<MsgEvent>): List<Long>
@@ -125,7 +134,7 @@ interface MsgEventRepository {
fun claimBatch(target: String, limit: Int): List<MsgEvent>
/** 同 FLID 未发 KAFKA_SCHD 事件合并:每 FLID 取最新 STATE_VERSION 一条(§5)。 */
/** 挑出待发的整态事件,同一条航班只取版本最高的那一条(中间的版本不用发)。 */
fun mergePendingSchd(limit: Int): List<MsgEvent>
fun markSent(eventId: Long)
@@ -137,63 +146,74 @@ interface MsgEventRepository {
fun markDead(eventId: Long, errorClass: ErrorClass, lastError: String, attempts: Int? = null)
}
/** 完整态落库结果:DAY_GUARD_VIOLATION = OPERATION_DAY 不可变条件更新未命中(§2.1)。 */
/**
* 落库结果。
* [DAY_GUARD_VIOLATION] 表示这次写入被"运营日不可变"的条件挡住:库里已有运营日,
* 和这次的报文对不上,说明这条报文可能串了日子。
*/
enum class PersistOutcome { INSERTED, UPDATED, DAY_GUARD_VIOLATION }
/**
* 航班当前态权威(§2 FLIGHT_SCHD + 9 张明细表
* 唯一写路径 = persistFullState:写入前在内存生成完整新状态(引擎产物)再落库,
* 明细集合按组先删后插(§2.2 完整合并结果为准)
* 航班当前态的读写入口:主表 FLIGHT_SCHD 9 张明细表。
*
* 写只有一个入口 [persistFullState]:先在内存里算出合并后的完整状态,再整体落库
* 明细数据按组先删后插,所以入库的永远是"合并后的完整结果",不依赖增量更新。
*/
interface FlightStateRepository {
/** 主行点查(身份/版本判定;批量用于日计划归属校验 §2.1)。 */
/** 查航班主行,用于判断它是否存在、属于哪个运营日、当前版本是多少。 */
fun findMainRow(flid: String): FlightMainRow?
fun findMainRows(flids: Collection<String>): Map<String, FlightMainRow>
/** 完整当前态主行 + 全部明细(一致性读边界由调用方事务保证,§5。 */
/** 读一条航班的完整当前态主行全部明细)。要读得一致,得由调用方放在事务里读。 */
fun loadFullSnapshot(flid: String): FlightSnapshot?
/**
* 完整当前态落库(§3:主行 upsert + 全部明细按组先删后插
* STATE_VERSION 以 snapshot.stateVersion 落库
* SCHD(§3.1)、FLOP/ADFT(§3.2/§3.3)合并后的全量结果共用此唯一写路径。
* 条件更新带 `WHERE operation_day IS NULL OR operation_day = :day`(§2.1 不可变强化)
* 把合并后的完整状态写库:主行 upsert明细按组先删后插
* 版本号用 [snapshot] 里算好的那个
*
* 日计划、动态事件、异常航班的合并结果都走这一个入口
* 写入带运营日条件(`WHERE operation_day IS NULL OR operation_day = :day`):
* 一旦航班已经归属某个运营日,别的日子的报文就写不进来。
*/
fun persistFullState(snapshot: FlightSnapshot, msgId: Long, now: Instant): PersistOutcome
/**
* FDEL(§3.3):ACTIVE → 置 DELETED、推进版本、明细保留、返回 true(发布删除事件);
* 已 DELETED 或不存在 → 返回 false(幂等成功,不推进版本不重复发布)。
* 删除航班:把在用的航班标成 DELETED、版本号加一,明细数据保留不删。
*
* 返回 true 表示这次真的删了(调用方据此发一次删除通知);返回 false 表示航班已经
* 是删除状态或根本不存在,按成功处理,不推进版本也不重复发通知。
*/
fun markDeleted(flid: String, msgId: Long, now: Instant): Boolean
/** ADFT 生命周期重激活(§3.3):DELETED → ACTIVE,推进版本;非 DELETED 返回 false。 */
/** 把已删除的航班重新激活(ADFT 报文触发):DELETED → ACTIVE、版本号加一;不是删除状态则返回 false。 */
fun revive(flid: String, msgId: Long, now: Instant): Boolean
/** §6按保留期与终态/静默判据选出历史候选(含 DELETED;窗口按机场时区折算)。 */
/** 按保留期挑出可以归档清理的航班(含已删除的)。时间窗口按机场时区计算。 */
fun findHistoryCandidates(rules: HistoryRules, zone: ZoneId, now: Instant): List<HistoryCandidate>
/**
* §6物理删除主行与明细(仅历史存储确认成功后调用;
* 历史存储接通时调用方必须传空集合——删 0 条)
* 物理删除主行与明细。**只能在历史存储确认归档成功后调用**
* 历史存储接通时调用方必须传空集合,也就是一条都不删
*/
fun purgeArchived(flids: Collection<String>): Int
/** §2.1 观测:OPERATION_DAY 仍为 NULL 的航班数(只增不删,终止规则未定 §6)。 */
/** 运营日还没定下来的航班有多少条(观测用;这些航班暂时不参与清理)。 */
fun countOperationDayNull(): Int
}
/** SCHD_SNAP_LOGdesign.md §6.2):事务外追加留痕,不参与决策;一行 = 一次尝试(重放也记)。 */
/** 日计划处理留痕:只追加、不参与业务判断,一次处理(含重放)记一行,写失败不影响业务。 */
interface SnapshotLogRepository {
fun append(entry: SnapshotLogEntry)
}
/**
* 请求状态机 REQ_TRACKdesign.md §4.2 目标机制,尚无运行时协调器):
* 只有 RESP 完成 RQFD 请求,按(运营日、发送方、请求类型)匹配最新一条 PENDING;
* 同类请求只留一条有效,新请求置旧为 EXPIRED
* 上游请求的跟踪表:记录我们发出去的请求、以及对方回来的应答。
*
* 目前只有表和读写方法,**还没有运行时协调器**(出站写信箱、超时、应答匹配都没实现)
* 设计意图是:RESP 报文按(运营日、发送方、请求类型)匹配最近一条待应答的请求;
* 同一类请求同时只保留一条有效,发新请求时把旧的置为已过期。
*/
interface ReqTrackRepository {
enum class ReqState { PENDING, SENT, DONE, EXPIRED }
@@ -38,9 +38,10 @@ import java.util.Locale
import javax.sql.DataSource
// =====================================================================
// 自有 PostgreSQL 仓储实现docs/flight-state.md §2 权威模型/§3 写入语义;schema 见
// db/migration/V1__flight_state_baseline.sql)。全部写路径约定在
// withTransaction + PIPELINE_LOCK 内调用(§1/§4)。
// 自有 PostgreSQL 仓储实现,表结构见 db/migration/V1__flight_state_baseline.sql。
//
// 航班相关的写操作都要求调用方先开事务、再拿 PIPELINE_LOCK 单行锁(见 Repositories.kt
// 顶部说明)。航班模型与合并规则见 docs/flight-state.md。
// =====================================================================
@Singleton
@@ -58,7 +59,7 @@ class JdbcPipelineTransactionManager(
class JdbcPipelineLockRepository(
private val ds: DataSource,
) : PipelineLockRepository {
/** 事务第一步:行 FOR UPDATE 串行化状态写事务(§1/§4 PIPELINE_LOCK。 */
/** 事务后的第一步:对锁`FOR UPDATE`,让并发的状态写事务排队。 */
override fun lock() {
ds.queryOne("SELECT lock_id FROM pipeline_lock WHERE lock_id = 1 FOR UPDATE", {}) { 1 } ?: error("PIPELINE_LOCK row missing")
}
@@ -340,7 +341,7 @@ class JdbcMsgEventRepository(
::mapEvent,
)
/** §5:同 FLID 未发事件按最新 STATE_VERSION 合并;PG DISTINCT ON 方言(注释明示)。 */
/** 同一条航班只取版本最高的待发整态事件;用了 PostgreSQL 的 DISTINCT ON 语法。 */
override fun mergePendingSchd(limit: Int): List<MsgEvent> =
ds.query(
"""
@@ -477,7 +478,7 @@ class JdbcFlightStateRepository(
},
)
if (updated == 0) {
// §2.1 不可变强化:条件更新未命中 = 归属日冲突
// 一行都没更新到,说明被运营日条件挡住了:这条航班已经属于别的运营日
return if (existed != null) PersistOutcome.DAY_GUARD_VIOLATION else PersistOutcome.UPDATED
}
replaceDetails(snapshot, now)
@@ -499,7 +500,7 @@ class JdbcFlightStateRepository(
) == 1
override fun findHistoryCandidates(rules: HistoryRules, zone: ZoneId, now: Instant): List<HistoryCandidate> {
// §6 四条判定在应用层执行(SIS 时间串解析无法下推 SQL
// 四条清理判据都在应用层算:里面的时间字段是 SIS 格式的字符串,没法交给 SQL 比较
// 不按 updated_at 粗筛——取消/终态时间可能早于最近一次更新,粗筛会漏删。
val rows = ds.query(
"SELECT flid, state, state_version, operation_day, cncl, naat, neat, updated_at FROM flight_schd",
@@ -528,7 +529,7 @@ class JdbcFlightStateRepository(
r.updatedAt < now.minus(Duration.ofHours(rules.idleHours)) -> true
else -> false
}
// NAAT/NEAT 业务含义待术语表确认(§6 开放项):解析失败视为不命中,不误
// NAAT/NEAT 业务含义还没确认,所以解析不出来时一律当成"不满足条件",宁可漏
if (hit) HistoryCandidate(r.flid, r.state, r.version, wasNeverFdel = r.state == FlightState.ACTIVE) else null
}
}
@@ -585,7 +586,7 @@ class JdbcFlightStateRepository(
items.forEachIndexed { ordinal, item ->
val cols = mutableListOf("flid", "ordinal", "source_seq")
val vals = mutableListOf<Any?>(flid)
vals.add(ordinal + 1) // ORDINAL 保留输入顺序(§2.2
vals.add(ordinal + 1) // 序号按报文里的先后顺序编,读回来顺序不变
vals.add(item[spec.seqAttr])
spec.columns.forEach { col ->
cols.add(col)
@@ -617,7 +618,7 @@ class JdbcFlightStateRepository(
private fun loadDetails(flid: String, key: String, spec: DetailSpec): List<Map<String, String>> {
val sql = if (spec.routeKind != null) {
// ROUT/ERUT 共表:按 ROUTE_KIND 过滤(§2.2
// ROUTERUT 存在同一张表里,靠 ROUTE_KIND 区分,读的时候要带上这个条件
"SELECT * FROM ${spec.table} WHERE flid = ? AND route_kind = ? ORDER BY ordinal ASC"
} else {
"SELECT * FROM ${spec.table} WHERE flid = ? ORDER BY ordinal ASC"
@@ -673,7 +674,7 @@ class JdbcFlightStateRepository(
"flight_delay", "flight_bridge_op", "flight_chock_op", "flight_route_point",
)
/** 10 类集合 ↔ 明细表/列映射(§2.2列名与基线一致)。 */
/** 报文里的 10 类集合分别存到哪张表、哪些列(列名与基线脚本一致)。 */
internal val COLLECTIONS: Map<String, DetailSpec> = mapOf(
"GTDT" to DetailSpec("flight_gate", listOf("gate", "pgot", "pgct", "gotm", "gctm", "gtyp"), "GTNO"),
"CKDT" to DetailSpec("flight_checkin", listOf("chkc", "ccls", "pcot", "pcct", "cotm", "cctm", "ctyp"), "CKNO"),
@@ -695,7 +696,7 @@ class JdbcFlightStateRepository(
class JdbcSnapshotLogRepository(
private val ds: DataSource,
) : SnapshotLogRepository {
/** design.md §6.2:只追加;写失败由调用方捕获记指标(append 自身不抛出)。 */
/** 只追加写;这里不抛异常,写失败由调用方捕获并记为指标。 */
override fun append(entry: SnapshotLogEntry) {
ds.update(
"""
@@ -757,7 +758,10 @@ class JdbcReqTrackRepository(
)
}
/** design.md §4.2 RESP 完成请求:匹配最新一条 PENDING/SENT;无匹配返回 false(迟到不报错)。 */
/**
* 应答报文到达时,把对应请求标记为已完成:找最近一条待应答的请求(PENDING 或 SENT)。
* 找不到就返回 false——迟到或多余的应答不算错误。
*/
override fun completeLatest(reqType: String, operationDay: LocalDate, sender: String): Boolean =
ds.update(
"""
@@ -199,7 +199,7 @@ class StubMsgEvents : MsgEventRepository {
override fun claimBatch(target: String, limit: Int): List<MsgEvent> =
rows.values.filter { it.target == target && it.state == EventStatus.PENDING }.sortedBy { it.eventId!! }.take(limit)
/** §5:同 FLID 取最新 STATE_VERSION,按事件输出。 */
/** 同一条航班只取版本最高的待发整态事件,按事件先后输出。 */
override fun mergePendingSchd(limit: Int): List<MsgEvent> =
rows.values
.filter { it.target == "KAFKA:schd" && it.state == EventStatus.PENDING }
@@ -245,7 +245,7 @@ class StubFlightState : FlightStateRepository {
override fun loadFullSnapshot(flid: String): FlightSnapshot? = snapshots[flid]
/** §2.1 不可变条件:已有非空 OPERATION_DAY 且与新值不同 DAY_GUARD_VIOLATION。 */
/** 运营日不可变:库里已有运营日且与新值不同时,返回 DAY_GUARD_VIOLATION。 */
override fun persistFullState(snapshot: FlightSnapshot, msgId: Long, now: Instant): PersistOutcome {
val existing = mains[snapshot.flid]
if (existing?.operationDay != null && existing.operationDay != snapshot.operationDay) {
@@ -327,7 +327,7 @@ class StubReqTrack : ReqTrackRepository {
fun clear() = rows.clear()
override fun insert(reqType: String, operationDay: LocalDate, sender: String): Long {
// design.md §4.2:同类(类型+运营日+发送方)旧有效请求先置 EXPIRED
// 同一类请求(类型 + 运营日 + 发送方)只保留一条有效,旧的先置为已过期
rows.values.filter {
it.reqType == reqType && it.operationDay == operationDay && it.sender == sender &&
(it.state == ReqTrackRepository.ReqState.PENDING || it.state == ReqTrackRepository.ReqState.SENT)
@@ -78,7 +78,7 @@ class FdelProcessor(
val deleted = flightState.markDeleted(payload.flid, msgId = head.msgId, now = Instant.now())
if (deleted) {
val current = flightState.loadFullSnapshot(payload.flid)
// tombstone 仅在 ACTIVE→DELETED 时登记(§3.3/§5),与删除同事务
// 只有"在用 → 删除"这一步才发删除通知,而且和状态变更写在同一个事务
msgEvents.insertAll(
listOf(
MsgEvent(
@@ -105,7 +105,7 @@ class FdelProcessor(
),
)
}
procState.markTerminal(head.msgId, ProcStatus.SUCCEEDED) // 未命中 = 迟到/重复,幂等成功(§3.3
procState.markTerminal(head.msgId, ProcStatus.SUCCEEDED) // 没删到东西说明是迟到重复报文,照样算成功
ApplyResult.Succeeded
}
}
@@ -137,7 +137,7 @@ class AdftProcessor(
lock.lock()
val main = flightState.findMainRow(record.flid)
if (main != null && main.state == FlightState.DELETED) {
// §3.3 重激活:DELETED → ACTIVE,推进版本,登记状态事件
// 已删除的航班重新激活:状态改回 ACTIVE、版本号加一,并登记状态事件
if (flightState.revive(record.flid, msgId = head.msgId, now = Instant.now())) {
val current = flightState.loadFullSnapshot(record.flid)
if (current != null) {
@@ -152,11 +152,11 @@ class AdftProcessor(
val current = flightState.loadFullSnapshot(record.flid)
val next: FlightSnapshot = if (current == null) {
// 新实例建立:ADFT 含 SODT 时直接计算运营日(§2.1),不可算则置 null 待日计划收录
// 新建航班:带了计划时间就算出运营日,算不出来先留空,等日计划报文来收录
val day = opDay.compute(record.scalars["SODT"])
FlightSnapshot(
flid = record.flid,
operationDay = day, // §2.1:不可算时不得默认写接收日
operationDay = day, // 算不出来就留空,不能默认拿收报当天顶上
state = FlightState.ACTIVE,
stateVersion = 1L,
scalars = record.scalars,
@@ -129,8 +129,8 @@ class MessageProcessor(
log.warn("processOne unexpected failure msgId={} ec=INFRA msg={}", head.msgId, e.message ?: e.javaClass.simpleName)
procFailure.fail(head, ErrorClass.INFRA, e.message ?: e.javaClass.simpleName)
}
// 终态已提交:立即尝试一次回填;失败留待回填扫描按退避重试(意图已在终态事务内登记)。
// 中间态(PENDING/FAILED)不适用(message-lifecycle §5.2
// 终态已经写好了,马上试一次回填;写不进去也没关系,回填扫描按退避继续重试
// (待办在写终态时就一起登记了)。还没处理完的消息不打标记
if (terminal) backfill.attempt(head.msgId)
}
}
@@ -178,7 +178,7 @@ class MessageProcessor(
}
}
// 处理器分派:SCHD 日计划主链路(§3.1)/ FLOP / FDEL / ADFT;缺载荷按 MALFORMED 终态
// 按报文类型分派:日计划走 SCHD,其余走 FLOP / FDEL / ADFT报文缺载荷直接判为非法报文的死信
val result: ApplyResult = when (val kind = decoded.kind) {
is MsgKind.Schd -> {
val body = decoded.body as? ScheduleBody
@@ -204,14 +204,14 @@ class MessageProcessor(
flopProcessor.apply(head, decoded, payload)
}
is MsgKind.Unsupported -> {
// design.md §2.3:未支持类型 → FAILED(UNSUPPORTED) 退避重试,达阈值转 DEAD;绝不写终态
// 还没有对应处理器的报文类型:先按可重试的失败处理,等能力补齐,不直接判死
log.warn("unsupported type -> FAILED(UNSUPPORTED) msgId={} tag={}", head.msgId, kind.tag)
return procFailure.fail(head, ErrorClass.UNSUPPORTED, "no-handler:${kind.tag}")
}
}
if (result is ApplyResult.DeadProtocol) {
// design.md §2.3整包拒绝 DEAD(PROTOCOL),立即释放队头,人工确认
// 整包拒绝:不重试、立刻放掉队头,人工确认
log.error("DEAD(PROTOCOL) msgId={} reason={} flags={}", head.msgId, result.reason, result.flags)
procState.markTerminal(
head.msgId, ProcStatus.DEAD,
@@ -76,7 +76,7 @@ class ScheduleProcessor(
val body = msg.body as? ScheduleBody ?: return ApplyResult.DeadProtocol("missing-schd-body")
val started = System.nanoTime()
// 重放判定:MSG_ID 已有成功终态 → 幂等成功(flight-state.md §4),重放也记留痕
// 这条消息以前处理成功过:直接算成功,不重复写数据(留痕照样记一条)
if (procState.findSuccessTerminal(head.msgId)) {
logSnapshot(head, body, SnapshotResult.REPLAY_SKIPPED, upserted = 0, flags = emptySet(), started)
return ApplyResult.ReplaySkipped
@@ -123,7 +123,7 @@ class ScheduleProcessor(
val record = body.records.first { it.flid == flid }
val existingMain = mains[flid]
val keepDeleted = existingMain?.state == FlightState.DELETED
if (keepDeleted) flags.add(SnapshotFlag.SCHD_REVIVE_CONFLICT) // §3.3:日计划不复活 DELETED
if (keepDeleted) flags.add(SnapshotFlag.SCHD_REVIVE_CONFLICT) // 日计划不会让已删除的航班复活
val current = if (existingMain != null) flightState.loadFullSnapshot(flid) else null
val next = FlightStateEngine.snapshotState(
current = current,
@@ -151,7 +151,7 @@ class ScheduleProcessor(
}
}
/** 状态事件:KAFKA_SCHD 整态 + KAFKA_MSG 变化通知(flight-state.md §3.1 登记、§5 语义)。 */
/** 每次航班状态变化登记两个事件:KAFKA_SCHD 发完整状态,KAFKA_MSG 发一条变更通知。 */
private fun snapshotEvents(next: FlightSnapshot): List<MsgEvent> {
val payload = linkedMapOf<String, Any>(
"flid" to next.flid,