fix(processing): 收紧消息生命周期与投递约束
This commit is contained in:
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user