docs(kdoc): 重写信箱生命周期相关 KDoc,改为直白说明并去掉内部编号

原有注释大量引用 §5.1/§5.2/Q2/US-01 这类文档编号和内部简称,跳过了"这段代码在做什么、
为什么这么做",没有读过设计文档的人基本读不懂。本次统一改成先讲清这件事本身、
再说为什么要这样做,编号只在末尾留一处指路。

覆盖本次改动涉及的 24 个 Kotlin 文件(生产 16 个 + 测试 8 个):

- 领域与端口:ProcState(补齐全量字段说明与状态/错误分类逐项注释)、
  ProcStateRepository / InboxCursorRepository / CminmsgInboxRepository 及
  MailboxRow / BackfillDue / Backlog;
- 收报:InboxPoller(把"水位连续、遇缺口停下、缺口老化"用大白话讲透)、
  InboxService、JdbcCminmsgInboxRepository;
- 处理:Pump / MessageProcessor、ScheduleProcessor、DynamicProcessors、
  ProcFailure、JdbcProcStateRepository 与游标实现;
- 回填与观测:BackfillService、InboxLifecycleHealthIndicator、JobRunner;
- 配置与 stub:PipelineProps(三个新增参数说清取值理由)、MailboxProps、
  StubRepositories;
- 测试:8 个测试类改为"这些用例在守哪几条规矩",并保留 H2 不覆盖
  ON CONFLICT 的说明。

术语统一按第一次出现就地解释:水位、处理标记、回填、死信、队头、终态。
纯注释改动;除拆分枚举时按仓库风格补的两个行尾逗号外无代码变更
(已用剥离注释后比对 HEAD 的方式逐文件核对)。测试仍为 78 passed / 1 skipped。
This commit is contained in:
windyboy
2026-09-10 11:05:55 +08:00
parent 8c6bb83b62
commit d475feb790
24 changed files with 332 additions and 186 deletions
@@ -10,15 +10,20 @@ import java.time.Duration
import java.time.Instant
/**
* 信箱处理标记回填(docs/message-lifecycle.md §3/§4/§5.2
* 把"已处理"标记写回共享信箱
*
* 回填意图(`BACKFILL_NEXT_AT`)由处理器在终态事务内登记,与业务写入同提交同回滚
* 因此不存在"业务已提交、待办未记"的窗口;本服务只做两件事:
* 1. [attempt]:终态提交后立即尝试一次(低延迟,失败静默留给扫描);
* 2. [sweep]:到期或已达超期期限 R 的记录批量补写(§5.2),指数退避 30s 起步、封顶 15 分钟。
* 一条消息处理完要做两件事:记下终态、把标记写回信箱。第一件在业务事务里完成
* 同时留下"还欠一次回填"的意图(PROC_STATE 上的 BACKFILL_NEXT_AT);
* 这个类负责第二件:
*
* 回填失败绝不重放业务变更,也绝不回改终态(§11);写入侧只把空标写为已处理
* 重复执行无副作用
* - [attempt]:处理刚结束时马上试一次,让标记尽快落到信箱。失败也不影响处理结果
* 留给扫描重试即可
* - [sweep]:定时把还欠回填的记录挑出来重试。失败就按 30 秒起步、最长 15 分钟的
* 退避往后推;如果一条消息从收到现在已经超过超期期限,则无视退避强制补写——
* 否则退避可能一直失败下去,这些行永远打不上标记,库方就没法清理信箱。
*
* 两条底线:回填失败不会把终态改回去,也不会重新执行业务逻辑;写标记只写还是空标记的
* 行,重复执行没有副作用。
*/
@Singleton
class BackfillService(
@@ -40,7 +45,10 @@ class BackfillService(
}
}
/** 单条最佳努力回填;失败只登记退避(异常不外抛,不阻塞提交后的处理路径)。 */
/**
* 处理完立刻试一次。失败只记一笔退避信息就返回,不抛异常——
* 调用方是主泵的处理路径,不能被回填问题拖住。
*/
fun attempt(msgId: Long, now: Instant = clock.instant()) {
record(msgId, attempts = 0, now = now)?.let {
log.warn("backfill failed msgId={} error={} (sweep will retry)", msgId, it)
@@ -48,8 +56,9 @@ class BackfillService(
}
/**
* 批量补写JobRunner 每 30s 触发;重启即继续,不依赖内存状态)。
* @return 本批检查条数
* 批量补写,由 JobRunner 每 30 秒调用一次。待办状态都在数据库里,
* 进程重启后接着跑,不需要额外恢复步骤。
* @return 本批处理的条数
*/
fun sweep(now: Instant = clock.instant()): Int {
val due = procState.findBackfillDue(now, now.minus(props.pipeline.overdueBackfill), props.pipeline.backfillBatch)
@@ -57,10 +66,10 @@ class BackfillService(
return due.size
}
/** @return 失败原因;null = 已确认标记(含"已被其他路径标记"的幂等成功) */
/** 回填一条。@return 失败原因;返回 null 表示标记已确认(包括"别的路径已经标过了")。 */
private fun record(msgId: Long, attempts: Int, now: Instant): String? =
try {
// 影响 0 行 = 已有标记;按幂等成功处理(§11 标记单调:不回撤、不覆盖
// 没有真正写进去说明库里已经有标记了,同样算成功(不覆盖已有值
mailbox.markProcessedIfUnmarked(msgId, mailboxProps.processedValue)
procState.markBackfilled(msgId, now)
null