feat(runtime): U07 生命周期装配 + stub 装配冒烟(ACM2-10 U07/U01 遗留)

- PipelineLifecycle:ServerStartupEvent 后在专用单线程(msgx-pump/msgx-dispatcher,daemon)
  拉起 Pump/Dispatcher 循环;停机 requestStop+interrupt+join;@Requires(msgx.pipeline.autostart=true)
- PipelineProps.pipeline.autostart(默认 false,生产/测试默认不自动起循环,防泄漏线程)
- stub 装配(infra/stub,@Requires(msgx.stubs=true)):7 仓储内存实装 + StubXmlCodec(未实装
  → CODEC_ERROR 走 FAILED 可重放路径)+ StubRedis + StubDeliveryPort + Holder Factory
- 配置:application-dev.yml(stubs+autostart,/env、/beans 放开,无需 DB 即可 dev 跑通);
  application-test.yml(stubs=true、autostart=false);README 补 dev stub 运行说明
- PipelineSmokeTest(4):等价 /beans 装配核验 + 收报→主泵领取→FAILED(CODEC_ERROR) + U11 重放
  回 PENDING + Dispatcher flush 聚合(stub 用例前清内存状态防上下文复用串扰)
- 全量测试 34 个通过
This commit is contained in:
windyboy
2026-09-06 20:53:14 +08:00
parent 73113779d2
commit f6da0cb782
11 changed files with 518 additions and 7 deletions
@@ -0,0 +1,108 @@
package com.gzzn.omms.msgexchange.nextgen
import com.gzzn.omms.msgexchange.nextgen.delivery.Dispatcher
import com.gzzn.omms.msgexchange.nextgen.domain.ErrorClass
import com.gzzn.omms.msgexchange.nextgen.domain.MsgEvent
import com.gzzn.omms.msgexchange.nextgen.domain.ProcStatus
import com.gzzn.omms.msgexchange.nextgen.domain.Targets
import com.gzzn.omms.msgexchange.nextgen.infra.stub.StubDeliveryPort
import com.gzzn.omms.msgexchange.nextgen.infra.stub.StubMsgEvents
import com.gzzn.omms.msgexchange.nextgen.infra.stub.StubProcState
import com.gzzn.omms.msgexchange.nextgen.ingress.InboxController
import com.gzzn.omms.msgexchange.nextgen.processing.Pump
import io.micronaut.context.ApplicationContext
import io.micronaut.test.extensions.junit5.annotation.MicronautTest
import jakarta.inject.Inject
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
/**
* U07 端到端(stub 装配,等价 /beans 核验):无 MySQL/Redis/Kafka 下,
* ①核心 bean 装配齐全;②收报→落库→主泵领取→解码未实装→FAILED(CODEC_ERROR)+退避;
* ③U11 重放把 FAILED 拉回 PENDING;④Dispatcher flushSchd 聚合发出 schd。
* 后台循环关闭(autostart=false),按需手动 tick,避免测试泄漏线程。
*/
@MicronautTest
class PipelineSmokeTest {
@Inject
lateinit var ctx: ApplicationContext
@Inject
lateinit var controller: InboxController
@Inject
lateinit var pump: Pump
@Inject
lateinit var dispatcher: Dispatcher
// @MicronautTest 复用同一上下文:用例前清空内存 stub,避免全局状态跨用例串扰
@org.junit.jupiter.api.BeforeEach
fun cleanStubs() {
ctx.getBean(StubProcState::class.java).clear()
ctx.getBean(com.gzzn.omms.msgexchange.nextgen.infra.stub.StubInbox::class.java).clear()
ctx.getBean(StubMsgEvents::class.java).clear()
ctx.getBean(StubDeliveryPort::class.java).clear()
}
@Test
fun `core pipeline beans are wired under stubs`() {
assertNotNull(ctx.getBean(com.gzzn.omms.msgexchange.nextgen.config.PipelineProps::class.java))
assertTrue(ctx.getBeansOfType(Pump::class.java).isNotEmpty())
assertTrue(ctx.getBeansOfType(Dispatcher::class.java).isNotEmpty())
assertNotNull(controller)
}
@Test
fun `accept then pump tick transitions message to FAILED CODEC_ERROR with backoff`() {
val receipt = controller.send("<MSG/>")
assertNotNull(receipt.body()) // 受理 ID
val id = receipt.body()!!.toLong()
pump.tick() // 主泵领取 → stub codec 未实装 → FAILED(CODEC_ERROR)
val stub = ctx.getBean(StubProcState::class.java)
val s = stub.snapshotOf(id)
assertNotNull(s)
assertEquals(ProcStatus.FAILED, s!!.state)
assertEquals(ErrorClass.CODEC_ERROR, s.errorClass)
assertEquals(1, s.attempts)
assertNotNull(s.nextAttemptAt)
}
@Test
fun `replay reopens failed CODEC_ERROR row to PENDING`() {
val receipt = controller.send("<MSG/>")
val id = receipt.body()!!.toLong()
pump.tick()
val stub = ctx.getBean(StubProcState::class.java)
assertEquals(ProcStatus.FAILED, stub.snapshotOf(id)!!.state)
val n = ctx.getBean(com.gzzn.omms.msgexchange.nextgen.infra.retry.ReplayService::class.java)
.replay(listOf(ErrorClass.CODEC_ERROR))
assertEquals(1, n)
val reopened = stub.snapshotOf(id)!!
assertEquals(ProcStatus.PENDING, reopened.state)
assertEquals(0, reopened.attempts)
}
@Test
fun `dispatcher flushes schd batch through stub port`() {
val events = ctx.getBean(StubMsgEvents::class.java)
val port = ctx.getBean(StubDeliveryPort::class.java)
events.insertAll(listOf(
MsgEvent(target = Targets.KAFKA_SCHD, partitionKey = "F1", payloadJson = """{"FLID":"F1","v":"old"}"""),
MsgEvent(target = Targets.KAFKA_SCHD, partitionKey = "F1", payloadJson = """{"FLID":"F1","v":"new"}"""),
))
dispatcher.flushSchd()
assertEquals(1, port.sent.count { it.first == "schd" })
val payload = port.sent.first { it.first == "schd" }.second
assertTrue(payload.contains("new") && !payload.contains("old"))
}
}
+9 -4
View File
@@ -1,6 +1,14 @@
# U03 配置绑定回归测试专用环境(@MicronautTest 默认 environment=test
# 目的:让 ApplicationContext 可无外部依赖启动(内存 datasource / 关闭迁移 / 关闭注册),
# 以便以「注入 PipelineProps 断言」离线验证 msgx.* 嵌套键绑定(N02 验收),不依赖 /env 端点。
# 以便以「注入 PipelineProps/DataSource 断言」离线验证绑定(N02/R09/R01a),不依赖 /env 端点。
# U07stubs=true 提供内存仓储使全链路(Controller→Inbox→Pump→Dispatcher)可装配;
# autostart=false:测试内不自动拉起后台循环(避免泄漏线程),按需手动 tick。
msgx:
register-eureka: false
stubs: true
pipeline:
autostart: false
datasources:
default:
url: jdbc:h2:mem:cfgtest;DB_CLOSE_DELAY=-1
@@ -20,9 +28,6 @@ kafka:
bootstrap:
servers: 127.0.0.1:9092
msgx:
register-eureka: false
micronaut:
server:
port: 0