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:
@@ -77,6 +77,8 @@ class ReplayServiceTest {
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
override fun deleteExpiredTerminal(cutoff: java.time.Instant, limit: Int): Int = 0
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -38,7 +38,9 @@ class JobRunnerTest {
|
||||
|
||||
private fun runner(proc: ProcStateRepository, activity: JobActivity) = JobRunner(
|
||||
BackfillService(proc, StubInbox(), MailboxProps(), props, clock, MessageLifecycleGate()),
|
||||
historySweep(), EventCleanupJob(StubMsgEvents(), props), clock, activity, props, OperationDayProps(),
|
||||
historySweep(), EventCleanupJob(StubMsgEvents(), props),
|
||||
ProcStateCleanupJob(proc, MessageLifecycleGate(), props),
|
||||
clock, activity, props, OperationDayProps(),
|
||||
)
|
||||
|
||||
@Test
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
package com.gzzn.omms.msgexchange.jobs
|
||||
|
||||
import com.gzzn.omms.msgexchange.config.PipelineProps
|
||||
import com.gzzn.omms.msgexchange.domain.ProcStatus
|
||||
import com.gzzn.omms.msgexchange.infra.stub.StubProcState
|
||||
import com.gzzn.omms.msgexchange.processing.MessageLifecycleGate
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertNotNull
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
* US-11 处理记录清理的几条规矩:
|
||||
* - 只删「终态 + 已回填 + 超过保留期」三关都过的行;
|
||||
* - 未完成(PENDING/FAILED)或没回填的行一条不删;
|
||||
* - 清理与人工重放共用同一把生命周期门,不持 PIPELINE_LOCK。
|
||||
*/
|
||||
class ProcStateCleanupJobTest {
|
||||
|
||||
private val now: Instant = Instant.parse("2026-09-21T04:00:00Z")
|
||||
private val retention: Duration = Duration.ofDays(90)
|
||||
|
||||
@Test
|
||||
fun `expired terminal and backfilled rows are deleted`() {
|
||||
val proc = StubProcState()
|
||||
val id = seed(proc, 1L, ProcStatus.SUCCEEDED, backfilled = true, updatedAt = now.minus(retention).minusSeconds(1))
|
||||
val job = ProcStateCleanupJob(proc, MessageLifecycleGate(), PipelineProps().apply { pipeline.terminalRetention = retention })
|
||||
|
||||
val outcome = job.run(now)
|
||||
|
||||
assertEquals(1, outcome.deleted)
|
||||
assertNull(proc.find(id))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `terminal rows without backfill are never deleted`() {
|
||||
val proc = StubProcState()
|
||||
val id = seed(proc, 2L, ProcStatus.DEAD, backfilled = false, updatedAt = now.minus(retention).minusSeconds(1))
|
||||
val job = ProcStateCleanupJob(proc, MessageLifecycleGate(), PipelineProps().apply { pipeline.terminalRetention = retention })
|
||||
|
||||
val outcome = job.run(now)
|
||||
|
||||
assertEquals(0, outcome.deleted)
|
||||
assertNotNull(proc.find(id))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unfinished rows are never deleted even when expired`() {
|
||||
val proc = StubProcState()
|
||||
val failed = seed(proc, 3L, ProcStatus.FAILED, backfilled = true, updatedAt = now.minus(retention).minusSeconds(1))
|
||||
val pending = seed(proc, 4L, ProcStatus.PENDING, backfilled = true, updatedAt = now.minus(retention).minusSeconds(1))
|
||||
val job = ProcStateCleanupJob(proc, MessageLifecycleGate(), PipelineProps().apply { pipeline.terminalRetention = retention })
|
||||
|
||||
val outcome = job.run(now)
|
||||
|
||||
assertEquals(0, outcome.deleted)
|
||||
assertNotNull(proc.find(failed))
|
||||
assertNotNull(proc.find(pending))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `rows within retention stay`() {
|
||||
val proc = StubProcState()
|
||||
val id = seed(proc, 5L, ProcStatus.SKIPPED, backfilled = true, updatedAt = now.minus(retention).plusSeconds(1))
|
||||
val job = ProcStateCleanupJob(proc, MessageLifecycleGate(), PipelineProps().apply { pipeline.terminalRetention = retention })
|
||||
|
||||
val outcome = job.run(now)
|
||||
|
||||
assertEquals(0, outcome.deleted)
|
||||
assertNotNull(proc.find(id))
|
||||
}
|
||||
|
||||
/** 手工插一行并按指定 UPDATED_AT 写终态,模拟「很久以前就了结并回填」的记录。 */
|
||||
private fun seed(proc: StubProcState, msgId: Long, state: ProcStatus, backfilled: Boolean, updatedAt: Instant): Long {
|
||||
proc.insertIfAbsent(msgId, Instant.EPOCH, Instant.EPOCH)
|
||||
if (backfilled) {
|
||||
proc.markTerminal(msgId, state, errorClass = null, lastError = null, attempts = 1, now = Instant.EPOCH)
|
||||
proc.markBackfilled(msgId, now = updatedAt)
|
||||
}
|
||||
// 直接改 UPDATED_AT 到目标时刻(stub 只在写路径刷新它,这里用回填时刻代表终态时刻)
|
||||
proc.rows[msgId] = proc.rows.getValue(msgId).copy(updatedAt = updatedAt)
|
||||
return msgId
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user