fix(delivery): msg 队头失败或退避只暂停同 FLID,不阻塞其他航班(ACM2-95)

- drainKafkaMsg 按分区键维护本轮暂停集:队头失败/退避未到期仅停该 FLID(D2)
- 同一 FLID 内仍严格按 EVENT_ID 保序不越队
- 补三条用例:失败仅停同 FLID、退避未到期仅停同 FLID、同 FLID 跨轮保序(TestClocks 无 sleep)
This commit is contained in:
windyboy
2026-09-21 12:26:09 +08:00
parent 0e09365cf6
commit df087c7933
2 changed files with 95 additions and 7 deletions
@@ -214,6 +214,82 @@ class DispatcherTickTest {
d.tick()
assertEquals(1, port.sent.count { it.topic == "msg" && it.key == "F9" })
}
@Test
fun `msg head failure pauses only the same FLID while other flights deliver in the same tick`() {
val repo = StubMsgEvents()
val port = FailingMsgKeyPort("F1")
val d = dispatcher(repo, port)
repo.insertAll(
listOf(
ev(1, Targets.KAFKA_MSG, "F1", """{"flid":"F1","v":1}"""),
ev(2, Targets.KAFKA_MSG, "F2", """{"flid":"F2","v":1}"""),
ev(3, Targets.KAFKA_MSG, "F1", """{"flid":"F1","v":2}"""),
),
)
d.tick()
// D2F1 队头失败只暂停 F1(含 id=3 的后续),F2 照常发出
assertEquals(1, port.sent.count { it.topic == "msg" && it.key == "F2" })
assertEquals(0, port.sent.count { it.topic == "msg" && it.key == "F1" })
val f1Rows = repo.rows.values.filter { it.partitionKey == "F1" }
assertEquals(2, f1Rows.size)
assertTrue(f1Rows.all { it.state == EventStatus.PENDING })
}
@Test
fun `msg head backoff not yet due pauses only the same FLID`() {
val repo = StubMsgEvents()
val port = StubDeliveryPort()
val d = dispatcher(repo, port)
repo.insertAll(
listOf(
ev(1, Targets.KAFKA_MSG, "F1", """{"flid":"F1","v":1}"""),
ev(2, Targets.KAFKA_MSG, "F2", """{"flid":"F2","v":1}"""),
),
)
repo.scheduleRetry(1, clock.instant().plusSeconds(60), 1) // F1 队头退避未到期
d.tick()
assertEquals(1, port.sent.count { it.topic == "msg" && it.key == "F2" })
val f1 = repo.rows.values.single { it.partitionKey == "F1" }
assertEquals(EventStatus.PENDING, f1.state)
assertEquals(1, f1.attempts)
}
@Test
fun `same FLID msg events keep strict order across ticks`() {
val repo = StubMsgEvents()
val port = StubDeliveryPort()
val d = dispatcher(repo, port)
repo.insertAll(
listOf(
ev(1, Targets.KAFKA_MSG, "F1", """{"v":1}"""),
ev(2, Targets.KAFKA_MSG, "F1", """{"v":2}"""),
ev(3, Targets.KAFKA_MSG, "F1", """{"v":3}"""),
),
)
d.tick()
val f1Keys = port.sent.filter { it.topic == "msg" }.map { it.payload }
assertEquals(listOf("""{"v":1}""", """{"v":2}""", """{"v":3}"""), f1Keys) // 同 FLID 不越队
}
}
/** KAFKA:msg 对指定 FLID 的发送永远失败,其他 FLID 照常记录,用来验证按 FLID 暂停。 */
private class FailingMsgKeyPort(private val failingKey: String) : DeliveryPort {
val sent = mutableListOf<StubDeliveryPort.Sent>()
override fun sendKafka(topic: String, key: String, payloadJson: String) {
if (key == failingKey) throw IllegalStateException("broker-down:$key")
sent.add(StubDeliveryPort.Sent(topic, key, payloadJson))
}
override fun sendKafkaSchd(topic: String, key: String, payloadJson: String) = Unit
override fun sendKafkaNull(topic: String, key: String) = Unit
}
/** KAFKA_SCHD 发送永远失败的投递端口,用来验证退避重试和最终转死信的闭环。 */