2026-09-07 15:11:33 +08:00
|
|
|
|
package com.gzzn.omms.msgexchange.ingress
|
|
|
|
|
|
|
2026-09-10 10:59:58 +08:00
|
|
|
|
import com.gzzn.omms.msgexchange.config.PipelineProps
|
2026-09-08 11:26:21 +08:00
|
|
|
|
import com.gzzn.omms.msgexchange.domain.ErrorClass
|
2026-09-07 15:11:33 +08:00
|
|
|
|
import com.gzzn.omms.msgexchange.domain.ProcStatus
|
|
|
|
|
|
import com.gzzn.omms.msgexchange.infra.stub.StubInbox
|
2026-09-10 10:59:58 +08:00
|
|
|
|
import com.gzzn.omms.msgexchange.infra.stub.StubInboxCursor
|
|
|
|
|
|
import com.gzzn.omms.msgexchange.infra.stub.StubPipelineTx
|
2026-09-07 15:11:33 +08:00
|
|
|
|
import com.gzzn.omms.msgexchange.infra.stub.StubProcState
|
|
|
|
|
|
import org.junit.jupiter.api.Assertions.assertEquals
|
2026-09-10 10:59:58 +08:00
|
|
|
|
import org.junit.jupiter.api.Assertions.assertFalse
|
2026-09-07 15:11:33 +08:00
|
|
|
|
import org.junit.jupiter.api.Assertions.assertNotNull
|
2026-09-10 10:59:58 +08:00
|
|
|
|
import org.junit.jupiter.api.Assertions.assertNull
|
2026-09-07 15:11:33 +08:00
|
|
|
|
import org.junit.jupiter.api.BeforeEach
|
|
|
|
|
|
import org.junit.jupiter.api.Test
|
2026-09-10 10:59:58 +08:00
|
|
|
|
import java.time.Instant
|
2026-09-07 15:11:33 +08:00
|
|
|
|
|
2026-09-10 10:59:58 +08:00
|
|
|
|
/**
|
2026-09-10 11:05:55 +08:00
|
|
|
|
* 收报环节最要紧的几条规矩:
|
|
|
|
|
|
* - 取新消息只看 ID,不看处理标记。处理完却没能回填的行(尤其是永远不回填的死信)
|
|
|
|
|
|
* 不允许占住批次,也不允许挡住后面的新消息——这是曾经的线上隐患;
|
|
|
|
|
|
* - 水位只在成功登记后才推进,而且和登记写在同一个事务里,中断后重扫就能补齐;
|
|
|
|
|
|
* - 遇到 ID 缺口先停下来(可能有更小的消息还没到),缺口等太久则跳过(否则水位永远卡住);
|
|
|
|
|
|
* - 这一层不碰信箱的处理标记,标记留给回填环节写。
|
2026-09-10 10:59:58 +08:00
|
|
|
|
*/
|
2026-09-07 15:11:33 +08:00
|
|
|
|
class InboxPollerTest {
|
|
|
|
|
|
|
2026-09-10 10:59:58 +08:00
|
|
|
|
private val t0: Instant = Instant.parse("2026-09-08T03:00:00Z")
|
|
|
|
|
|
private val props = PipelineProps()
|
|
|
|
|
|
|
|
|
|
|
|
private lateinit var inbox: StubInbox
|
|
|
|
|
|
private lateinit var proc: StubProcState
|
|
|
|
|
|
private lateinit var cursor: StubInboxCursor
|
|
|
|
|
|
private lateinit var poller: InboxPoller
|
2026-09-07 15:11:33 +08:00
|
|
|
|
|
|
|
|
|
|
@BeforeEach
|
2026-09-10 10:59:58 +08:00
|
|
|
|
fun setUp() {
|
|
|
|
|
|
inbox = StubInbox().apply { clear() }
|
|
|
|
|
|
proc = StubProcState().apply { clear() }
|
|
|
|
|
|
cursor = StubInboxCursor().apply { clear() }
|
|
|
|
|
|
poller = InboxPoller(inbox, proc, cursor, StubPipelineTx(), props)
|
2026-09-07 15:11:33 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Test
|
2026-09-10 10:59:58 +08:00
|
|
|
|
fun `external rows are enqueued in id order and advance the watermark without marking the mailbox`() {
|
|
|
|
|
|
val first = inbox.simulateExternalWrite("<MSG/>")
|
|
|
|
|
|
val second = inbox.simulateExternalWrite("<MSG/>")
|
|
|
|
|
|
|
|
|
|
|
|
assertEquals(2, poller.pollOnce(t0))
|
|
|
|
|
|
|
|
|
|
|
|
assertEquals(ProcStatus.PENDING, proc.find(first)!!.state)
|
|
|
|
|
|
assertEquals(ProcStatus.PENDING, proc.find(second)!!.state)
|
|
|
|
|
|
assertEquals(second, cursor.cursor.committedUpTo)
|
|
|
|
|
|
assertNull(cursor.cursor.holeSince)
|
|
|
|
|
|
// §5.3:消化阶段只写自有 PG,不触碰信箱标记
|
|
|
|
|
|
assertFalse(inbox.isMarked(first))
|
|
|
|
|
|
assertEquals(0, poller.pollOnce(t0)) // 重复扫描幂等
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 回归(US-01 条目 3 / §5.3「积压挡批」):终态且永不回填的行(解码失败死信等)曾占满
|
|
|
|
|
|
* 有限批次使收报整体停摆——发现必须与处理标记彻底解耦。
|
|
|
|
|
|
*/
|
|
|
|
|
|
@Test
|
|
|
|
|
|
fun `terminal rows without a mailbox mark do not block discovery of later messages`() {
|
|
|
|
|
|
props.pipeline.claimBatch = 3
|
|
|
|
|
|
val dead = (1..3).map { inbox.simulateExternalWrite("<MSG/>") }
|
|
|
|
|
|
assertEquals(3, poller.pollOnce(t0))
|
|
|
|
|
|
dead.forEach {
|
|
|
|
|
|
proc.markTerminal(it, ProcStatus.DEAD, errorClass = ErrorClass.MALFORMED, lastError = "raw-missing")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
val fresh = inbox.simulateExternalWrite("<MSG/>")
|
|
|
|
|
|
|
|
|
|
|
|
assertEquals(1, poller.pollOnce(t0))
|
|
|
|
|
|
assertEquals(ProcStatus.PENDING, proc.find(fresh)!!.state)
|
|
|
|
|
|
assertEquals(fresh, cursor.cursor.committedUpTo)
|
2026-09-07 15:11:33 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Test
|
2026-09-10 10:59:58 +08:00
|
|
|
|
fun `watermark stops at a hole so later ids cannot overtake a missing smaller id`() {
|
|
|
|
|
|
val first = inbox.simulateExternalWrite("<MSG/>")
|
|
|
|
|
|
val hole = inbox.simulateExternalWrite("<MSG/>")
|
|
|
|
|
|
val afterHole = inbox.simulateExternalWrite("<MSG/>")
|
|
|
|
|
|
inbox.removeRow(hole)
|
|
|
|
|
|
|
|
|
|
|
|
assertEquals(1, poller.pollOnce(t0))
|
|
|
|
|
|
|
|
|
|
|
|
assertEquals(first, cursor.cursor.committedUpTo)
|
|
|
|
|
|
assertNotNull(cursor.cursor.holeSince)
|
|
|
|
|
|
assertNull(proc.find(afterHole)) // 不得越过空洞入队(FIFO)
|
|
|
|
|
|
assertEquals(0, poller.pollOnce(t0.plusSeconds(60))) // 宽限期内水位不推进
|
|
|
|
|
|
assertEquals(first, cursor.cursor.committedUpTo)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Test
|
|
|
|
|
|
fun `an aged hole is released and later ids resume enqueuing`() {
|
|
|
|
|
|
val hole = inbox.simulateExternalWrite("<MSG/>")
|
|
|
|
|
|
val afterHole = inbox.simulateExternalWrite("<MSG/>")
|
|
|
|
|
|
inbox.removeRow(hole)
|
|
|
|
|
|
poller.pollOnce(t0) // 记录空洞观测时刻
|
|
|
|
|
|
|
|
|
|
|
|
val agedOut = t0.plus(props.pipeline.maxCommitDelay)
|
|
|
|
|
|
assertEquals(0, poller.pollOnce(agedOut)) // 空洞判永久:推进水位但不越过入队
|
|
|
|
|
|
|
|
|
|
|
|
assertNull(cursor.cursor.holeSince)
|
|
|
|
|
|
assertEquals(afterHole - 1, cursor.cursor.committedUpTo)
|
|
|
|
|
|
assertEquals(1, poller.pollOnce(agedOut)) // 下一轮恢复发现
|
|
|
|
|
|
assertEquals(afterHole, cursor.cursor.committedUpTo)
|
|
|
|
|
|
assertNotNull(proc.find(afterHole))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Test
|
|
|
|
|
|
fun `compat http path and poller do not double enqueue the same message`() {
|
|
|
|
|
|
val receipt = InboxService(inbox, proc).accept("<MSG/>", t0)
|
|
|
|
|
|
|
|
|
|
|
|
assertEquals(0, poller.pollOnce(t0))
|
|
|
|
|
|
assertEquals(receipt.msgId, cursor.cursor.committedUpTo) // 已在 PG:读取进度照常推进
|
|
|
|
|
|
assertNotNull(proc.find(receipt.msgId))
|
2026-09-07 15:11:33 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|