feat(jobs): PROC_STATE 终态行到期清理(ACM2-82)

- 新增 deleteExpiredTerminal:仅删终态 + BACKFILL_AT 非空 + UPDATED_AT 超期的行(US-11)
- ProcStateCleanupJob 每轮 tick 执行,与人工重放共用 MessageLifecycleGate,不持 PIPELINE_LOCK
- 登记参数 msgx.pipeline.terminal-retention(默认 90d,暂定)进 reference.md
- Stub 与 JDBC 实现同口径;补四类候选条件回归
This commit is contained in:
windyboy
2026-09-21 12:52:26 +08:00
parent 477f038cc1
commit 1d95926e06
10 changed files with 181 additions and 1 deletions
@@ -126,6 +126,13 @@ interface ProcStateRepository {
/** 人工重放:把指定错误类别的 FAILED / DEAD 记录改回 PENDING,重试次数清零。 */
fun requeueByErrorClasses(errorClasses: List<ErrorClass>): Int
/**
* 到期清理(US-11):删除终态(SUCCEEDED/SKIPPED/DEAD)、已回填(BACKFILL_AT 非空)
* 且 `UPDATED_AT < cutoff` 的 PROC_STATE 行;未完成或未回填的行一律不删。
* @return 实际删除的行数
*/
fun deleteExpiredTerminal(cutoff: Instant, limit: Int): Int
/** 积压观测:还没处理完的条数、最老一条的接收时间、处理完但还没回填的条数。 */
fun backlog(): Backlog
@@ -288,6 +288,24 @@ class JdbcProcStateRepository(
)
}
/** 到期清理(US-11):只删终态且已回填、UPDATED_AT 超期的行;条件带不进子查询的行不受影响。 */
override fun deleteExpiredTerminal(cutoff: Instant, limit: Int): Int =
ds.update(
"""
DELETE FROM proc_state WHERE msg_id IN (
SELECT msg_id FROM proc_state
WHERE state IN ('SUCCEEDED', 'SKIPPED', 'DEAD')
AND backfill_at IS NOT NULL
AND updated_at < ?
ORDER BY msg_id LIMIT ?
)
""".trimIndent(),
{ ps ->
ps.setTimestamp(1, cutoff.toSqlTimestamp())
ps.setInt(2, limit)
},
)
/** 积压观测:未处理条数、最老一条的接收时间、处理完但未回填的条数。 */
override fun hasAny(): Boolean =
ds.queryOne("SELECT 1 FROM proc_state LIMIT 1", {}) { 1 } != null
@@ -225,6 +225,19 @@ class StubProcState(private val clock: Clock = Clock.systemUTC()) : ProcStateRep
return n
}
/** 到期清理(US-11):仅删终态且已回填、UPDATED_AT 超期的行。 */
override fun deleteExpiredTerminal(cutoff: Instant, limit: Int): Int {
val due = rows.values
.filter {
it.state in TERMINAL_STATES && it.backfillAt != null && it.updatedAt.isBefore(cutoff)
}
.sortedBy { it.msgId }
.take(limit)
.map { it.msgId }
due.forEach { rows.remove(it) }
return due.size
}
fun snapshotOf(msgId: Long): ProcState? = rows[msgId]
}
@@ -549,3 +562,7 @@ class StubInboxCursor : InboxCursorRepository {
/** 判断是不是终态:处理已经结束、不会再重试的状态。 */
private fun ProcStatus.isTerminal(): Boolean =
this == ProcStatus.SUCCEEDED || this == ProcStatus.SKIPPED || this == ProcStatus.DEAD
/** 终态集合:US-11 到期清理与回填扫描共用的口径。 */
private val TERMINAL_STATES: Set<ProcStatus> =
setOf(ProcStatus.SUCCEEDED, ProcStatus.SKIPPED, ProcStatus.DEAD)