fix(processing): 收紧消息生命周期与投递约束

This commit is contained in:
windyboy
2026-09-10 20:39:22 +08:00
parent dd839e6abb
commit eea11203a2
27 changed files with 502 additions and 79 deletions
@@ -6,18 +6,26 @@ import com.gzzn.omms.msgexchange.domain.ErrorClass
import com.gzzn.omms.msgexchange.domain.ProcStatus
import com.gzzn.omms.msgexchange.infra.persistence.CminmsgInboxRepository
import com.gzzn.omms.msgexchange.infra.persistence.MailboxRow
import com.gzzn.omms.msgexchange.infra.persistence.MailboxMarkResult
import com.gzzn.omms.msgexchange.infra.stub.StubInbox
import com.gzzn.omms.msgexchange.infra.stub.StubProcState
import com.gzzn.omms.msgexchange.infra.retry.ReplayService
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.Test
import java.time.Clock
import java.time.Duration
import java.time.Instant
import java.time.ZoneOffset
import java.util.concurrent.CountDownLatch
import java.util.concurrent.Callable
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
/**
* 回填环节的规矩:
@@ -37,11 +45,12 @@ class BackfillServiceTest {
val marked = linkedSetOf<Long>()
override fun insertRaw(rawXml: String): Long = 1L
override fun rawOf(msgId: Long): String? = null
override fun receivedAtOf(msgId: Long): Instant? = null
override fun readRange(fromExclusive: Long, limit: Int): List<MailboxRow> = emptyList()
override fun maxId(): Long? = null
override fun markProcessedIfUnmarked(msgId: Long, value: String): Boolean {
override fun markProcessedIfUnmarked(msgId: Long, value: String): MailboxMarkResult {
if (fail) throw IllegalStateException("mysql-down")
return marked.add(msgId)
return if (marked.add(msgId)) MailboxMarkResult.MARKED else MailboxMarkResult.ALREADY_MARKED
}
}
@@ -75,8 +84,8 @@ class BackfillServiceTest {
val inbox = StubInbox().apply { clear() }
val id = inbox.insertRaw("<MSG/>")
assertTrue(inbox.markProcessedIfUnmarked(id, "PROCESSED"))
assertFalse(inbox.markProcessedIfUnmarked(id, "OTHER")) // 已经有标记了,不再写第二次
assertEquals(MailboxMarkResult.MARKED, inbox.markProcessedIfUnmarked(id, "PROCESSED"))
assertEquals(MailboxMarkResult.ALREADY_MARKED, inbox.markProcessedIfUnmarked(id, "OTHER"))
assertEquals("PROCESSED", inbox.markOf(id))
}
@@ -89,10 +98,12 @@ class BackfillServiceTest {
val backfill = service(proc, inbox)
backfill.attempt(id)
val firstCompletion = proc.find(id)!!.backfillAt
backfill.attempt(id) // 重复补写没有副作用
assertNull(proc.find(id)!!.backfillError)
assertEquals(0, proc.find(id)!!.backfillAttempts)
assertEquals(firstCompletion, proc.find(id)!!.backfillAt)
}
@Test
@@ -111,6 +122,23 @@ class BackfillServiceTest {
assertNull(row.backfillAt)
}
@Test
fun `missing mailbox row remains an unconfirmed backfill failure`() {
val proc = StubProcState()
val inbox = StubInbox().apply { clear() }
val id = inbox.insertRaw("<MSG/>")
succeeded(proc, id)
inbox.removeRow(id)
service(proc, inbox).attempt(id)
val row = proc.find(id)!!
assertNull(row.backfillAt)
assertEquals(1, row.backfillAttempts)
assertEquals("mailbox-row-missing", row.backfillError)
assertNotNull(row.backfillNextAt)
}
@Test
fun `sweep retries due rows and completes once the mailbox recovers`() {
val proc = StubProcState()
@@ -163,6 +191,25 @@ class BackfillServiceTest {
assertFalse(inbox.isMarked(failed))
}
@Test
fun `immediate attempt also refuses pending and failed messages`() {
val proc = StubProcState()
val inbox = StubInbox().apply { clear() }
val pending = inbox.insertRaw("<MSG/>")
val failed = inbox.insertRaw("<MSG/>")
proc.insertIfAbsent(pending, t0)
proc.insertIfAbsent(failed, t0)
proc.update(failed, ProcStatus.FAILED, errorClass = ErrorClass.INFRA)
service(proc, inbox).attempt(pending)
service(proc, inbox).attempt(failed)
assertFalse(inbox.isMarked(pending))
assertFalse(inbox.isMarked(failed))
assertNull(proc.find(pending)!!.backfillAt)
assertNull(proc.find(failed)!!.backfillAt)
}
@Test
fun `backoff delay doubles per attempt and caps at fifteen minutes`() {
assertEquals(Duration.ofSeconds(30), BackfillService.backoffDelayFor(1))
@@ -170,4 +217,49 @@ class BackfillServiceTest {
assertEquals(Duration.ofMinutes(15), BackfillService.backoffDelayFor(10))
assertEquals(Duration.ofMinutes(15), BackfillService.backoffDelayFor(50))
}
@Test
fun `replay waits for an in-flight backfill before reopening the message`() {
val entered = CountDownLatch(1)
val release = CountDownLatch(1)
val replayStarted = CountDownLatch(1)
val mailbox = object : CminmsgInboxRepository {
override fun insertRaw(rawXml: String) = 1L
override fun rawOf(msgId: Long): String? = "<MSG/>"
override fun receivedAtOf(msgId: Long) = t0
override fun readRange(fromExclusive: Long, limit: Int) = emptyList<MailboxRow>()
override fun maxId(): Long? = 1L
override fun markProcessedIfUnmarked(msgId: Long, value: String): MailboxMarkResult {
entered.countDown()
release.await()
return MailboxMarkResult.MARKED
}
}
val proc = StubProcState().apply {
insertIfAbsent(1L, t0)
markTerminal(1L, ProcStatus.DEAD, errorClass = ErrorClass.EXHAUSTED, now = t0)
}
val gate = MessageLifecycleGate()
val backfill = BackfillService(proc, mailbox, MailboxProps(), props, Clock.fixed(t0, ZoneOffset.UTC), gate)
val replay = ReplayService(proc, gate)
val pool = Executors.newFixedThreadPool(2)
try {
val backfillTask = pool.submit { backfill.attempt(1L) }
assertTrue(entered.await(1, TimeUnit.SECONDS))
val replayTask = pool.submit(Callable {
replayStarted.countDown()
replay.replay(listOf(ErrorClass.EXHAUSTED))
})
assertTrue(replayStarted.await(1, TimeUnit.SECONDS))
assertThrows(TimeoutException::class.java) { replayTask.get(100, TimeUnit.MILLISECONDS) }
release.countDown()
backfillTask.get(1, TimeUnit.SECONDS)
assertEquals(1, replayTask.get(1, TimeUnit.SECONDS))
assertEquals(ProcStatus.PENDING, proc.find(1L)!!.state)
} finally {
release.countDown()
pool.shutdownNow()
}
}
}