Files
msgexchange-v2/src/test/kotlin/com/gzzn/omms/msgexchange/delivery/DispatcherTickTest.kt
T
windyboy 879d658159 refactor(flight-state): 按 flight-state.md 审计定稿全量重构脚手架与 SQL (ACM2-31)
- SQL 基线 V1__flight_state_baseline.sql 整体取代 V1.0.0–V1.4.0:
  PIPELINE_LOCK/PROC_STATE/MSG_EVENT/REQ_TRACK/BACKFILL_TODO/FLIGHT_SCHD
  + 8 张资源明细表 + FLIGHT_ROUTE_POINT + SCHD_SNAP_LOG 留痕层
- 废除 FDAY 日代/SCHD_GEN/名单差删:OPERATION_DAY 不可变(应用层校验 +
  条件更新强化 §7.4),STATE 仅 ACTIVE/DELETED,物理清除只在历史归档后
- 处理器化:applyScheduleRecords(§5.1 七步同一事务,重放判定/整包
  DEAD(PROTOCOL)/归属冲突不落地)+ FLOP/FDEL/ADFT(tombstone 仅
  ACTIVE→DELETED,重复 FDEL 幂等不推进版本)
- 投递:KAFKA_SCHD 同 FLID 按最新 STATE_VERSION 合并,被压掉事件关闭,
  TOMBSTONE 发 null 值消息(键缺失=删除旧值 §7.3)
- 回填待办改为业务事务内预登记,消除提交后写待办的崩溃窗口(§7.2/§10)
- XML 解码改为 jackson-dataformat-xml 数据类直接映射(SIS 信封强类型,
  FLTR 开放标签泛型承载)
- 历史归档/物理清除顺序不可颠倒:归档确认成功集才物理删除,未接通删 0 条
- 移除 PUMP_JOB 队列/ReferenceService/FlightStoreDiffTool 等旧机制与测试,
  新增运营日/引擎/快照/FDEL/归档顺序不变性回归测试
2026-09-09 17:53:08 +08:00

133 lines
5.1 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package com.gzzn.omms.msgexchange.delivery
import com.gzzn.omms.msgexchange.MutableClock
import com.gzzn.omms.msgexchange.config.PipelineProps
import com.gzzn.omms.msgexchange.domain.EventType
import com.gzzn.omms.msgexchange.domain.MsgEvent
import com.gzzn.omms.msgexchange.domain.Targets
import com.gzzn.omms.msgexchange.infra.persistence.MsgEventRepository
import com.gzzn.omms.msgexchange.infra.retry.FailureScheduler
import com.gzzn.omms.msgexchange.infra.stub.StubDeliveryPort
import com.gzzn.omms.msgexchange.infra.stub.StubMsgEvents
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import java.time.Instant
/**
* 投递调度(docs/flight-state.md §7.3):
* KAFKA_MSG 逐条 FIFOKAFKA_SCHD 唯一出口 flushSchd——同 FLID 未发事件按最新
* STATE_VERSION 合并;TOMBSTONE 发 null 值消息;失败退避重试、达上限 DEAD。
*/
class DispatcherTickTest {
private val clock = MutableClock(MutableClock.BASE)
private fun dispatcher(repo: MsgEventRepository, port: DeliveryPort, p: PipelineProps = PipelineProps()): Dispatcher =
Dispatcher(repo, port, p, FailureScheduler(p, clock))
private fun ev(id: Long, target: String, key: String, payload: String, version: Long = 0) =
MsgEvent(eventId = id, target = target, partitionKey = key, stateVersion = version, payloadJson = payload)
@Test
fun `per-target tick delivers KAFKA_MSG only and never consumes schd`() {
val repo = StubMsgEvents()
val port = StubDeliveryPort()
val props = PipelineProps().apply { schd.flushPeriod = java.time.Duration.ofHours(1) }
repo.insertAll(
listOf(
ev(1, Targets.KAFKA_MSG, "F1", """{"flid":"F1"}"""),
ev(2, Targets.KAFKA_SCHD, "F1", """{"flid":"F1"}""", 1),
),
)
val d = dispatcher(repo, port, props)
d.tick()
assertEquals(1, port.sent.count { it.topic == "msg" })
assertEquals(0, port.sent.count { it.topic == "schd" }) // N03schd 唯一出口 flushSchd
}
@Test
fun `flushSchd aggregates latest version per flight and marks sent`() {
val repo = StubMsgEvents()
val port = StubDeliveryPort()
repo.insertAll(
listOf(
ev(1, Targets.KAFKA_SCHD, "F1", """{"v":"old"}""", 1),
ev(2, Targets.KAFKA_SCHD, "F1", """{"v":"new"}""", 2),
ev(3, Targets.KAFKA_SCHD, "F2", """{"v":"f2"}""", 1),
),
)
val d = dispatcher(repo, port)
d.flushSchd()
val schd = port.sent.filter { it.topic == "schd" }
assertEquals(2, schd.size)
assertEquals(1, schd.count { it.key == "F1" && it.payload!!.contains("new") && !it.payload.contains("old") })
assertEquals(0, repo.rows.values.count { it.state.name == "PENDING" })
}
@Test
fun `tombstone is delivered as null value message`() {
val repo = StubMsgEvents()
val port = StubDeliveryPort()
repo.insertAll(
listOf(
MsgEvent(
eventId = 1, target = Targets.KAFKA_SCHD, partitionKey = "F1",
eventType = EventType.TOMBSTONE, stateVersion = 3, payloadJson = """{"flid":"F1","deleted":true}""",
),
),
)
val d = dispatcher(repo, port)
d.flushSchd()
assertEquals(1, port.tombstones.size)
assertEquals("F1", port.tombstones.single().key) // 整态键缺失表示删除旧值(§7.3)
assertNull(port.tombstones.single().payload)
}
@Test
fun `schd send failure schedules backoff and goes DEAD at limit`() {
val repo = StubMsgEvents()
val p = PipelineProps()
val d = dispatcher(repo, FailingSchdPort(), p)
repo.insertAll(listOf(ev(1, Targets.KAFKA_SCHD, "F1", """{"v":"1"}""", 1)))
d.flushSchd()
assertEquals(1, repo.rows[1]!!.attempts)
repeat(p.pipeline.maxAttempts - 1) {
clock.advance(p.pipeline.backoffFor(repo.rows[1]!!.attempts) + 1)
d.flushSchd()
}
val final = repo.rows[1]!!
assertTrue(final.attempts >= p.pipeline.maxAttempts || final.state.name == "DEAD")
}
@Test
fun `KAFKA msg delivery unaffected while schd batch is retrying`() {
val repo = StubMsgEvents()
val port = StubDeliveryPort()
val d = dispatcher(repo, port)
repo.insertAll(listOf(ev(1, Targets.KAFKA_MSG, "F9", """{"flid":"F9"}""")))
d.tick()
assertEquals(1, port.sent.count { it.topic == "msg" && it.key == "F9" })
}
}
/** schd 发送恒失败的端口(退避/终态闭环验证用)。 */
private class FailingSchdPort : DeliveryPort {
var calls = 0
override fun sendKafka(topic: String, key: String, payloadJson: String) = Unit
override fun sendKafkaSchd(topic: String, key: String, payloadJson: String) {
calls++
throw IllegalStateException("broker-down")
}
override fun sendKafkaNull(topic: String, key: String) {
throw IllegalStateException("broker-down")
}
}