diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..79973cb --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,31 @@ +# Repository Guidelines + +## Project Structure & Module Organization + +Application code lives under `src/main/kotlin/com/gzzn/omms/msgexchange/`. Keep changes within the established modules: `ingress` receives mailbox records, `processing` owns FIFO decisions, `delivery` handles outbound events, `jobs` runs maintenance work, and `infra` contains persistence, Redis, health, and retry adapters. Runtime configuration, Flyway migrations, and Lua scripts are in `src/main/resources/`. Tests mirror production packages under `src/test/kotlin/`. Architecture and requirements live in `docs/`; treat `docs/legacy/` as reference material, not current design. Local middleware definitions are in `compose.yaml` and `deploy/dev/`. + +## Build, Test, and Development Commands + +- `./gradlew build` — compile, test, and package the application. +- `./gradlew test` — run the JUnit 5 test suite. +- `MICRONAUT_ENVIRONMENTS=dev ./gradlew run` — start the stub-backed development profile without external services. +- `cp .env.example .env && docker compose up -d` — start local PostgreSQL, MySQL, Valkey, and Kafka. +- `docker compose ps` — verify middleware health. + +Use JDK 25. In restricted environments, point `GRADLE_USER_HOME` and `TMPDIR` to writable directories. + +## Coding Style & Naming Conventions + +Use Kotlin conventions with four-space indentation, trailing commas in multiline declarations, and immutable values by default. Types use `PascalCase`; functions and properties use `camelCase`; constants use `UPPER_SNAKE_CASE`. Name tests after behavior, for example `MessageProcessorTest` and `InboxPollerTest`. Keep handlers pure: return domain decisions rather than performing Redis, Kafka, or database writes directly. Preserve the single-writer and strict message FIFO invariants documented in `docs/architecture.md`. + +## Testing Guidelines + +Tests use JUnit 5, Micronaut Test, and Kotlin Test. Add focused tests beside the affected package. Changes to ordering, retry, identity, snapshot, or delivery behavior must include invariant-level regression tests. Prefer injected clocks and in-memory adapters over sleeps or live infrastructure. Run `./gradlew test` before submitting. + +## Commit & Pull Request Guidelines + +History follows Conventional Commit-style subjects such as `feat(ref): ...`, `fix(processing): ...`, and `docs: ...`; include the relevant Plane identifier when applicable. Keep commits scoped and avoid mixing unrelated refactors. Pull requests should explain behavior changes, affected invariants, configuration or migration impact, linked Plane work items, and verification performed. Include request/response examples for API changes; screenshots are only needed for visual documentation changes. + +## Security & Configuration + +Never commit credentials or production endpoints. Use environment variables documented in `.env.example`. Shared MySQL is an external mailbox boundary: do not add schema migrations or unapproved tables there. diff --git a/build.gradle.kts b/build.gradle.kts index b562270..474484d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -48,6 +48,7 @@ dependencies { implementation(libs.logstash.logback.encoder) runtimeOnly(libs.mysql.connector.j) + runtimeOnly(libs.postgresql) // ACM2-11:datasources.reference(21 类静态主数据独立 PG 库)驱动 org.postgresql:postgresql // 随阶段 6 实装加入(当前 reference enabled=false 不加载;版本由 micronaut-platform BOM 钉 ~42.7) runtimeOnly(libs.snakeyaml) // U02:显式版本入 catalog(原无版本号依赖 BOM 覆盖,snakeyaml 不在 micronaut BOM 内) @@ -61,7 +62,7 @@ dependencies { application { // Kotlin 顶层 main(Application.kt)→ JVM 主类为 ApplicationKt; // 不声明则 ./gradlew run 报 "No main class specified"、installDist 产出的启动脚本主类为空。 - mainClass.set("com.gzzn.omms.msgexchange.nextgen.ApplicationKt") + mainClass.set("com.gzzn.omms.msgexchange.ApplicationKt") } tasks.test { diff --git a/src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/Application.kt b/src/main/kotlin/com/gzzn/omms/msgexchange/Application.kt similarity index 70% rename from src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/Application.kt rename to src/main/kotlin/com/gzzn/omms/msgexchange/Application.kt index 64f5884..5fccfe5 100644 --- a/src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/Application.kt +++ b/src/main/kotlin/com/gzzn/omms/msgexchange/Application.kt @@ -1,4 +1,4 @@ -package com.gzzn.omms.msgexchange.nextgen +package com.gzzn.omms.msgexchange import io.micronaut.runtime.Micronaut diff --git a/src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/PipelineLifecycle.kt b/src/main/kotlin/com/gzzn/omms/msgexchange/PipelineLifecycle.kt similarity index 77% rename from src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/PipelineLifecycle.kt rename to src/main/kotlin/com/gzzn/omms/msgexchange/PipelineLifecycle.kt index 1cea278..396ef50 100644 --- a/src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/PipelineLifecycle.kt +++ b/src/main/kotlin/com/gzzn/omms/msgexchange/PipelineLifecycle.kt @@ -1,7 +1,8 @@ -package com.gzzn.omms.msgexchange.nextgen +package com.gzzn.omms.msgexchange -import com.gzzn.omms.msgexchange.nextgen.delivery.Dispatcher -import com.gzzn.omms.msgexchange.nextgen.processing.Pump +import com.gzzn.omms.msgexchange.delivery.Dispatcher +import com.gzzn.omms.msgexchange.ingress.InboxPoller +import com.gzzn.omms.msgexchange.processing.Pump import io.micronaut.context.annotation.Requires import io.micronaut.runtime.event.annotation.EventListener import io.micronaut.runtime.server.event.ServerStartupEvent @@ -9,14 +10,14 @@ import jakarta.annotation.PreDestroy import jakarta.inject.Singleton /** - * U07(T03+N29):管道生命周期装配——服务启动(ServerStartupEvent)后,在各自专用单线程 - * (msgx-pump / msgx-dispatcher,不占用 Netty event loop)上拉起 Pump 与 Dispatcher 循环; + * U07(T03+N29):管道生命周期装配——服务启动后拉起 inbox 轮询、主泵、投递三条专用单线程 * 停机时 requestStop + interrupt + join。仅当 `msgx.pipeline.autostart=true` 时装配 * (默认关:需要真实仓储或 msgx.stubs=true 才安全开启)。 */ @Requires(property = "msgx.pipeline.autostart", value = "true") @Singleton class PipelineLifecycle( + private val poller: InboxPoller, private val pump: Pump, private val dispatcher: Dispatcher, ) { @@ -34,9 +35,10 @@ class PipelineLifecycle( private fun startIfNeeded() { if (started) return started = true + threads += spawn("msgx-inbox-poller", poller::loop) threads += spawn("msgx-pump", pump::loop) threads += spawn("msgx-dispatcher", dispatcher::loop) - log.info("pipeline loops started (pump, dispatcher)") + log.info("pipeline loops started (inbox-poller, pump, dispatcher)") } private fun spawn(name: String, body: () -> Unit): Thread = @@ -44,6 +46,7 @@ class PipelineLifecycle( @PreDestroy fun stop() { + poller.stop() pump.stop() dispatcher.stop() threads.forEach { it.interrupt() } // 解除 Thread.sleep 阻塞,加速退出 diff --git a/src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/codec/XmlCodec.kt b/src/main/kotlin/com/gzzn/omms/msgexchange/codec/XmlCodec.kt similarity index 82% rename from src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/codec/XmlCodec.kt rename to src/main/kotlin/com/gzzn/omms/msgexchange/codec/XmlCodec.kt index c383c46..9465a8d 100644 --- a/src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/codec/XmlCodec.kt +++ b/src/main/kotlin/com/gzzn/omms/msgexchange/codec/XmlCodec.kt @@ -1,7 +1,7 @@ -package com.gzzn.omms.msgexchange.nextgen.codec +package com.gzzn.omms.msgexchange.codec -import com.gzzn.omms.msgexchange.nextgen.domain.DecodedMessage -import com.gzzn.omms.msgexchange.nextgen.domain.ErrorClass +import com.gzzn.omms.msgexchange.domain.DecodedMessage +import com.gzzn.omms.msgexchange.domain.ErrorClass /** * ACMA-8 流程 2:解码失败分类(矩阵/状态机:MALFORMED 不重试;CODEC_ERROR 可一键重放)。 diff --git a/src/main/kotlin/com/gzzn/omms/msgexchange/config/MailboxProps.kt b/src/main/kotlin/com/gzzn/omms/msgexchange/config/MailboxProps.kt new file mode 100644 index 0000000..63f2041 --- /dev/null +++ b/src/main/kotlin/com/gzzn/omms/msgexchange/config/MailboxProps.kt @@ -0,0 +1,17 @@ +package com.gzzn.omms.msgexchange.config + +import io.micronaut.context.annotation.ConfigurationProperties + +@ConfigurationProperties("mailbox") +class MailboxProps { + var sharedMysql: SharedMysql = SharedMysql() + + @ConfigurationProperties("shared-mysql") + class SharedMysql { + var enabled: Boolean = false + var url: String = "" + var username: String = "" + var password: String = "" + var driverClassName: String = "com.mysql.cj.jdbc.Driver" + } +} diff --git a/src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/config/PipelineProps.kt b/src/main/kotlin/com/gzzn/omms/msgexchange/config/PipelineProps.kt similarity index 98% rename from src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/config/PipelineProps.kt rename to src/main/kotlin/com/gzzn/omms/msgexchange/config/PipelineProps.kt index 35840c4..5815a88 100644 --- a/src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/config/PipelineProps.kt +++ b/src/main/kotlin/com/gzzn/omms/msgexchange/config/PipelineProps.kt @@ -1,4 +1,4 @@ -package com.gzzn.omms.msgexchange.nextgen.config +package com.gzzn.omms.msgexchange.config import io.micronaut.context.annotation.ConfigurationProperties import java.time.Duration diff --git a/src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/delivery/Dispatcher.kt b/src/main/kotlin/com/gzzn/omms/msgexchange/delivery/Dispatcher.kt similarity index 93% rename from src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/delivery/Dispatcher.kt rename to src/main/kotlin/com/gzzn/omms/msgexchange/delivery/Dispatcher.kt index b1ac518..bd327c6 100644 --- a/src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/delivery/Dispatcher.kt +++ b/src/main/kotlin/com/gzzn/omms/msgexchange/delivery/Dispatcher.kt @@ -1,13 +1,13 @@ -package com.gzzn.omms.msgexchange.nextgen.delivery +package com.gzzn.omms.msgexchange.delivery import com.fasterxml.jackson.databind.ObjectMapper -import com.gzzn.omms.msgexchange.nextgen.config.PipelineProps -import com.gzzn.omms.msgexchange.nextgen.domain.ErrorClass -import com.gzzn.omms.msgexchange.nextgen.domain.EventStatus -import com.gzzn.omms.msgexchange.nextgen.domain.MsgEvent -import com.gzzn.omms.msgexchange.nextgen.domain.Targets -import com.gzzn.omms.msgexchange.nextgen.infra.persistence.MsgEventRepository -import com.gzzn.omms.msgexchange.nextgen.infra.retry.FailureScheduler +import com.gzzn.omms.msgexchange.config.PipelineProps +import com.gzzn.omms.msgexchange.domain.ErrorClass +import com.gzzn.omms.msgexchange.domain.EventStatus +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 jakarta.inject.Singleton import java.time.Duration import java.time.Instant diff --git a/src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/delivery/SchdAggregation.kt b/src/main/kotlin/com/gzzn/omms/msgexchange/delivery/SchdAggregation.kt similarity index 82% rename from src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/delivery/SchdAggregation.kt rename to src/main/kotlin/com/gzzn/omms/msgexchange/delivery/SchdAggregation.kt index e329650..28fb62c 100644 --- a/src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/delivery/SchdAggregation.kt +++ b/src/main/kotlin/com/gzzn/omms/msgexchange/delivery/SchdAggregation.kt @@ -1,7 +1,7 @@ -package com.gzzn.omms.msgexchange.nextgen.delivery +package com.gzzn.omms.msgexchange.delivery -import com.gzzn.omms.msgexchange.nextgen.domain.MsgEvent -import com.gzzn.omms.msgexchange.nextgen.domain.SchdPush +import com.gzzn.omms.msgexchange.domain.MsgEvent +import com.gzzn.omms.msgexchange.domain.SchdPush /** * ACMA-8 流程 3 flushSchd 聚合(纯函数,可单测): diff --git a/src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/domain/Decision.kt b/src/main/kotlin/com/gzzn/omms/msgexchange/domain/Decision.kt similarity index 96% rename from src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/domain/Decision.kt rename to src/main/kotlin/com/gzzn/omms/msgexchange/domain/Decision.kt index a8e051e..65a2f8c 100644 --- a/src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/domain/Decision.kt +++ b/src/main/kotlin/com/gzzn/omms/msgexchange/domain/Decision.kt @@ -1,4 +1,4 @@ -package com.gzzn.omms.msgexchange.nextgen.domain +package com.gzzn.omms.msgexchange.domain /** * ACMA-8 流程 2:Handler 决策(纯函数)产物——状态与报文进,变更与事件出, diff --git a/src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/domain/DecodedMessage.kt b/src/main/kotlin/com/gzzn/omms/msgexchange/domain/DecodedMessage.kt similarity index 95% rename from src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/domain/DecodedMessage.kt rename to src/main/kotlin/com/gzzn/omms/msgexchange/domain/DecodedMessage.kt index 222d25b..b39b686 100644 --- a/src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/domain/DecodedMessage.kt +++ b/src/main/kotlin/com/gzzn/omms/msgexchange/domain/DecodedMessage.kt @@ -1,4 +1,4 @@ -package com.gzzn.omms.msgexchange.nextgen.domain +package com.gzzn.omms.msgexchange.domain /** * 解码后的入站报文(统一内部模型)。META 字段实名 SNDR/SEQN/DTTM(legacy META.java,I3 幂等键来源)。 diff --git a/src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/domain/MsgEvent.kt b/src/main/kotlin/com/gzzn/omms/msgexchange/domain/MsgEvent.kt similarity index 95% rename from src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/domain/MsgEvent.kt rename to src/main/kotlin/com/gzzn/omms/msgexchange/domain/MsgEvent.kt index a4ac0f2..54bcb83 100644 --- a/src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/domain/MsgEvent.kt +++ b/src/main/kotlin/com/gzzn/omms/msgexchange/domain/MsgEvent.kt @@ -1,4 +1,4 @@ -package com.gzzn.omms.msgexchange.nextgen.domain +package com.gzzn.omms.msgexchange.domain /** * ACMA-8 MSG_EVENT(统一投递事件 / outbox)。 diff --git a/src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/domain/ProcState.kt b/src/main/kotlin/com/gzzn/omms/msgexchange/domain/ProcState.kt similarity index 94% rename from src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/domain/ProcState.kt rename to src/main/kotlin/com/gzzn/omms/msgexchange/domain/ProcState.kt index c27deac..503d663 100644 --- a/src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/domain/ProcState.kt +++ b/src/main/kotlin/com/gzzn/omms/msgexchange/domain/ProcState.kt @@ -1,4 +1,4 @@ -package com.gzzn.omms.msgexchange.nextgen.domain +package com.gzzn.omms.msgexchange.domain /** * ACMA-8 数据模型 / PROC_STATE 状态机(六迁移表)。 diff --git a/src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/infra/health/HealthIndicators.kt b/src/main/kotlin/com/gzzn/omms/msgexchange/infra/health/HealthIndicators.kt similarity index 91% rename from src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/infra/health/HealthIndicators.kt rename to src/main/kotlin/com/gzzn/omms/msgexchange/infra/health/HealthIndicators.kt index ce160c2..af3657c 100644 --- a/src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/infra/health/HealthIndicators.kt +++ b/src/main/kotlin/com/gzzn/omms/msgexchange/infra/health/HealthIndicators.kt @@ -1,7 +1,7 @@ -package com.gzzn.omms.msgexchange.nextgen.infra.health +package com.gzzn.omms.msgexchange.infra.health -import com.gzzn.omms.msgexchange.nextgen.delivery.DeliveryPort -import com.gzzn.omms.msgexchange.nextgen.infra.redis.FlightRedisClient +import com.gzzn.omms.msgexchange.delivery.DeliveryPort +import com.gzzn.omms.msgexchange.infra.redis.FlightRedisClient import io.micronaut.context.BeanProvider import io.micronaut.core.async.publisher.Publishers import io.micronaut.health.HealthStatus diff --git a/src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/infra/log/TraceLog.kt b/src/main/kotlin/com/gzzn/omms/msgexchange/infra/log/TraceLog.kt similarity index 89% rename from src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/infra/log/TraceLog.kt rename to src/main/kotlin/com/gzzn/omms/msgexchange/infra/log/TraceLog.kt index 6d99e99..9c61b25 100644 --- a/src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/infra/log/TraceLog.kt +++ b/src/main/kotlin/com/gzzn/omms/msgexchange/infra/log/TraceLog.kt @@ -1,4 +1,4 @@ -package com.gzzn.omms.msgexchange.nextgen.infra.log +package com.gzzn.omms.msgexchange.infra.log import org.slf4j.MDC diff --git a/src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/infra/persistence/Repositories.kt b/src/main/kotlin/com/gzzn/omms/msgexchange/infra/persistence/Repositories.kt similarity index 89% rename from src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/infra/persistence/Repositories.kt rename to src/main/kotlin/com/gzzn/omms/msgexchange/infra/persistence/Repositories.kt index f0cb513..f5fc54a 100644 --- a/src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/infra/persistence/Repositories.kt +++ b/src/main/kotlin/com/gzzn/omms/msgexchange/infra/persistence/Repositories.kt @@ -1,11 +1,11 @@ -package com.gzzn.omms.msgexchange.nextgen.infra.persistence +package com.gzzn.omms.msgexchange.infra.persistence -import com.gzzn.omms.msgexchange.nextgen.domain.ErrorClass -import com.gzzn.omms.msgexchange.nextgen.domain.EventStatus -import com.gzzn.omms.msgexchange.nextgen.domain.MsgEvent -import com.gzzn.omms.msgexchange.nextgen.domain.ProcState -import com.gzzn.omms.msgexchange.nextgen.domain.ProcStatus -import com.gzzn.omms.msgexchange.nextgen.domain.RefUpsert +import com.gzzn.omms.msgexchange.domain.ErrorClass +import com.gzzn.omms.msgexchange.domain.EventStatus +import com.gzzn.omms.msgexchange.domain.MsgEvent +import com.gzzn.omms.msgexchange.domain.ProcState +import com.gzzn.omms.msgexchange.domain.ProcStatus +import com.gzzn.omms.msgexchange.domain.RefUpsert import java.time.Instant /** @@ -19,6 +19,9 @@ import java.time.Instant interface ProcStateRepository { fun insert(cminmsgsId: Long, state: ProcStatus = ProcStatus.PENDING) + /** 主路径轮询/compat 入队前判重(PG 已有行则跳过)。 */ + fun exists(cminmsgsId: Long): Boolean + /** I1:严格 FIFO 队头(最小未完成 CMINMSGS_ID)。 */ fun headUnfinished(): ProcState? @@ -165,5 +168,11 @@ interface CminmsgInboxRepository { /** 主路径/处理:按 CMINMSGS_ID 读取上游已落信的原文 XML。 */ fun rawOf(cminmsgsId: Long): String? + /** + * 主路径 JDBC 轮询:共享库未处理报文 ID 列表(legacy `getNewMsgsAfterId` 同语义)。 + * @param afterId 下界(legacy 现役传 0);仅返回 `DATE_PROCESSED IS NULL` 行。 + */ + fun pollUnprocessed(afterId: Long, limit: Int): List + fun backfillOnSuccess(cminmsgsId: Long, sndr: String, type: String, styp: String, seqn: Long) } diff --git a/src/main/kotlin/com/gzzn/omms/msgexchange/infra/persistence/jdbc/JdbcCminmsgInboxRepository.kt b/src/main/kotlin/com/gzzn/omms/msgexchange/infra/persistence/jdbc/JdbcCminmsgInboxRepository.kt new file mode 100644 index 0000000..7fac118 --- /dev/null +++ b/src/main/kotlin/com/gzzn/omms/msgexchange/infra/persistence/jdbc/JdbcCminmsgInboxRepository.kt @@ -0,0 +1,63 @@ +package com.gzzn.omms.msgexchange.infra.persistence.jdbc + +import com.gzzn.omms.msgexchange.infra.persistence.CminmsgInboxRepository +import io.micronaut.context.annotation.Requires +import jakarta.inject.Named +import jakarta.inject.Singleton +import javax.sql.DataSource + +/** + * 共享 MySQL CMINMSGS 信箱适配(ACM2-12:仅 DML,不建表)。 + * 列名与 legacy `entity/Cminmsg.java` 一致。 + */ +@Singleton +@Requires(property = "msgx.stubs", notEquals = "true") +@Requires(property = "mailbox.shared-mysql.enabled", value = "true") +class JdbcCminmsgInboxRepository( + @Named("mailbox") private val ds: DataSource, +) : CminmsgInboxRepository { + + override fun insertRaw(rawXml: String): Long = + ds.updateReturningLong( + """ + INSERT INTO cminmsgs (CMINMSGS_CLOB_MSG, CMINMSGS_DATE_RECEIVED, CMINMSGS_DATE_PROCESSED) + VALUES (?, CURRENT_TIMESTAMP, NULL) + """.trimIndent(), + ) { ps -> + ps.setString(1, rawXml) + } + + override fun rawOf(cminmsgsId: Long): String? = + ds.queryOne( + "SELECT CMINMSGS_CLOB_MSG FROM cminmsgs WHERE CMINMSGS_ID = ?", + { ps -> ps.setLong(1, cminmsgsId) }, + ) { rs -> rs.getString("CMINMSGS_CLOB_MSG") } + + override fun pollUnprocessed(afterId: Long, limit: Int): List = + ds.query( + """ + SELECT CMINMSGS_ID FROM cminmsgs + WHERE CMINMSGS_ID > ? AND CMINMSGS_DATE_PROCESSED IS NULL + ORDER BY CMINMSGS_ID ASC + LIMIT ? + """.trimIndent(), + { ps -> + ps.setLong(1, afterId) + ps.setInt(2, limit) + }, + ) { rs -> rs.getLong("CMINMSGS_ID") } + + override fun backfillOnSuccess(cminmsgsId: Long, sndr: String, type: String, styp: String, seqn: Long) { + ds.update( + """ + UPDATE cminmsgs + SET CMINMSGS_DATE_PROCESSED = CURRENT_TIMESTAMP, + CMINMSGS_STATUS = ? + WHERE CMINMSGS_ID = ? + """.trimIndent(), + ) { ps -> + ps.setString(1, "PROCESSED") + ps.setLong(2, cminmsgsId) + } + } +} diff --git a/src/main/kotlin/com/gzzn/omms/msgexchange/infra/persistence/jdbc/JdbcOps.kt b/src/main/kotlin/com/gzzn/omms/msgexchange/infra/persistence/jdbc/JdbcOps.kt new file mode 100644 index 0000000..af811f0 --- /dev/null +++ b/src/main/kotlin/com/gzzn/omms/msgexchange/infra/persistence/jdbc/JdbcOps.kt @@ -0,0 +1,46 @@ +package com.gzzn.omms.msgexchange.infra.persistence.jdbc + +import java.sql.ResultSet +import java.sql.Timestamp +import java.time.Instant +import javax.sql.DataSource + +internal fun Instant.toSqlTimestamp(): Timestamp = Timestamp.from(this) + +internal fun ResultSet.getInstant(column: String): Instant? = + getTimestamp(column)?.toInstant() + +internal fun DataSource.query(sql: String, bind: (java.sql.PreparedStatement) -> Unit, map: (ResultSet) -> T): List = + connection.use { conn -> + conn.prepareStatement(sql).use { ps -> + bind(ps) + ps.executeQuery().use { rs -> + buildList { + while (rs.next()) add(map(rs)) + } + } + } + } + +internal fun DataSource.queryOne(sql: String, bind: (java.sql.PreparedStatement) -> Unit, map: (ResultSet) -> T): T? = + query(sql, bind, map).firstOrNull() + +internal fun DataSource.update(sql: String, bind: (java.sql.PreparedStatement) -> Unit): Int = + connection.use { conn -> + conn.prepareStatement(sql).use { ps -> + bind(ps) + ps.executeUpdate() + } + } + +internal fun DataSource.updateReturningLong(sql: String, bind: (java.sql.PreparedStatement) -> Unit): Long = + connection.use { conn -> + conn.prepareStatement(sql, java.sql.Statement.RETURN_GENERATED_KEYS).use { ps -> + bind(ps) + ps.executeUpdate() + ps.generatedKeys.use { keys -> + check(keys.next()) { "no generated key" } + keys.getLong(1) + } + } + } diff --git a/src/main/kotlin/com/gzzn/omms/msgexchange/infra/persistence/jdbc/JdbcPgRepositories.kt b/src/main/kotlin/com/gzzn/omms/msgexchange/infra/persistence/jdbc/JdbcPgRepositories.kt new file mode 100644 index 0000000..3491606 --- /dev/null +++ b/src/main/kotlin/com/gzzn/omms/msgexchange/infra/persistence/jdbc/JdbcPgRepositories.kt @@ -0,0 +1,426 @@ +package com.gzzn.omms.msgexchange.infra.persistence.jdbc + +import com.gzzn.omms.msgexchange.domain.ErrorClass +import com.gzzn.omms.msgexchange.domain.EventStatus +import com.gzzn.omms.msgexchange.domain.MsgEvent +import com.gzzn.omms.msgexchange.domain.ProcState +import com.gzzn.omms.msgexchange.domain.ProcStatus +import com.gzzn.omms.msgexchange.domain.RefUpsert +import com.gzzn.omms.msgexchange.infra.persistence.FlightStateRepository +import com.gzzn.omms.msgexchange.infra.persistence.MsgEventRepository +import com.gzzn.omms.msgexchange.infra.persistence.ProcStateRepository +import com.gzzn.omms.msgexchange.infra.persistence.PumpJobRepository +import com.gzzn.omms.msgexchange.infra.persistence.RefDataRepository +import com.gzzn.omms.msgexchange.infra.persistence.ReqTrackRepository +import com.gzzn.omms.msgexchange.infra.persistence.StaticRefRepository +import io.micronaut.context.annotation.Requires +import jakarta.inject.Singleton +import java.time.Instant +import javax.sql.DataSource + +@Singleton +@Requires(property = "msgx.stubs", notEquals = "true") +@Requires(property = "datasources.default.enabled", value = "true") +class JdbcProcStateRepository( + private val ds: DataSource, +) : ProcStateRepository { + + override fun insert(cminmsgsId: Long, state: ProcStatus) { + val now = Instant.now() + ds.update( + """ + INSERT INTO proc_state (cminmsgs_id, state, attempts, updated_at) + VALUES (?, ?, 0, ?) + ON CONFLICT (cminmsgs_id) DO NOTHING + """.trimIndent(), + ) { ps -> + ps.setLong(1, cminmsgsId) + ps.setString(2, state.name) + ps.setTimestamp(3, now.toSqlTimestamp()) + } + } + + override fun exists(cminmsgsId: Long): Boolean = + ds.queryOne( + "SELECT 1 FROM proc_state WHERE cminmsgs_id = ?", + { ps -> ps.setLong(1, cminmsgsId) }, + ) { 1 } != null + + override fun headUnfinished(): ProcState? = + ds.queryOne( + """ + SELECT cminmsgs_id, state, identity_key, attempts, next_attempt_at, error_class, last_error, updated_at + FROM proc_state + WHERE state IN ('PENDING', 'FAILED') + ORDER BY cminmsgs_id ASC + LIMIT 1 + """.trimIndent(), + {}, + ::mapProcState, + ) + + override fun tryBindIdentity(cminmsgsId: Long, identityKey: String): Boolean { + ownerOfIdentity(identityKey)?.let { owner -> + if (owner != cminmsgsId) return false + return true + } + val updated = ds.update( + """ + UPDATE proc_state SET identity_key = ?, updated_at = ? + WHERE cminmsgs_id = ? AND identity_key IS NULL + """.trimIndent(), + ) { ps -> + ps.setString(1, identityKey) + ps.setTimestamp(2, Instant.now().toSqlTimestamp()) + ps.setLong(3, cminmsgsId) + } + return updated == 1 + } + + override fun ownerOfIdentity(identityKey: String): Long? = + ds.queryOne( + "SELECT cminmsgs_id FROM proc_state WHERE identity_key = ?", + { ps -> ps.setString(1, identityKey) }, + ) { rs -> rs.getLong("cminmsgs_id") } + + override fun update( + cminmsgsId: Long, + state: ProcStatus, + nextAttemptAt: Instant?, + attempts: Int?, + errorClass: ErrorClass?, + lastError: String?, + ) { + ds.update( + """ + UPDATE proc_state SET + state = ?, + next_attempt_at = ?, + attempts = COALESCE(?, attempts), + error_class = COALESCE(?, error_class), + last_error = COALESCE(?, last_error), + updated_at = ? + WHERE cminmsgs_id = ? + """.trimIndent(), + ) { ps -> + ps.setString(1, state.name) + ps.setTimestamp(2, nextAttemptAt?.toSqlTimestamp()) + if (attempts != null) ps.setInt(3, attempts) else ps.setNull(3, java.sql.Types.INTEGER) + ps.setString(4, errorClass?.name) + ps.setString(5, lastError) + ps.setTimestamp(6, Instant.now().toSqlTimestamp()) + ps.setLong(7, cminmsgsId) + } + } + + override fun requeueByErrorClasses(errorClasses: List): Int { + if (errorClasses.isEmpty()) return 0 + val placeholders = errorClasses.joinToString(",") { "?" } + return ds.update( + """ + UPDATE proc_state SET state = 'PENDING', attempts = 0, next_attempt_at = NULL, updated_at = ? + WHERE state IN ('FAILED', 'DEAD') AND error_class IN ($placeholders) + """.trimIndent(), + ) { ps -> + ps.setTimestamp(1, Instant.now().toSqlTimestamp()) + errorClasses.forEachIndexed { i, ec -> ps.setString(i + 2, ec.name) } + } + } + + private fun mapProcState(rs: java.sql.ResultSet) = ProcState( + cminmsgsId = rs.getLong("cminmsgs_id"), + state = ProcStatus.valueOf(rs.getString("state")), + identityKey = rs.getString("identity_key"), + attempts = rs.getInt("attempts"), + nextAttemptAt = rs.getInstant("next_attempt_at"), + errorClass = rs.getString("error_class")?.let(ErrorClass::valueOf), + lastError = rs.getString("last_error"), + updatedAt = rs.getInstant("updated_at") ?: Instant.now(), + ) +} + +@Singleton +@Requires(property = "msgx.stubs", notEquals = "true") +@Requires(property = "datasources.default.enabled", value = "true") +class JdbcMsgEventRepository( + private val ds: DataSource, +) : MsgEventRepository { + + override fun insertAll(events: List): List = + events.map { e -> + ds.updateReturningLong( + """ + INSERT INTO msg_event (target, partition_key, payload_json, state, attempts, next_attempt_at, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?) + """.trimIndent(), + ) { ps -> + ps.setString(1, e.target) + ps.setString(2, e.partitionKey) + ps.setString(3, e.payloadJson) + ps.setString(4, e.state.name) + ps.setInt(5, e.attempts) + ps.setTimestamp(6, e.nextAttemptAt?.toSqlTimestamp()) + ps.setTimestamp(7, e.createdAt.toSqlTimestamp()) + } + } + + override fun headUnsent(target: String): MsgEvent? = + ds.queryOne( + """ + SELECT event_id, target, partition_key, payload_json, state, attempts, next_attempt_at, error_class, last_error, created_at + FROM msg_event + WHERE target = ? AND state = 'PENDING' + ORDER BY event_id ASC + LIMIT 1 + """.trimIndent(), + { ps -> ps.setString(1, target) }, + ::mapEvent, + ) + + override fun claimBatch(target: String, limit: Int): List = + ds.query( + """ + SELECT event_id, target, partition_key, payload_json, state, attempts, next_attempt_at, error_class, last_error, created_at + FROM msg_event + WHERE target = ? AND state = 'PENDING' + ORDER BY event_id ASC + LIMIT ? + """.trimIndent(), + { ps -> + ps.setString(1, target) + ps.setInt(2, limit) + }, + ::mapEvent, + ) + + override fun markSent(eventId: Long) { + ds.update("UPDATE msg_event SET state = 'SENT' WHERE event_id = ?") { ps -> ps.setLong(1, eventId) } + } + + override fun markAllSent(eventIds: List) { + eventIds.forEach(::markSent) + } + + override fun scheduleRetry(eventId: Long, nextAttemptAt: Instant, attempts: Int) { + ds.update( + "UPDATE msg_event SET state = 'PENDING', next_attempt_at = ?, attempts = ? WHERE event_id = ?", + ) { ps -> + ps.setTimestamp(1, nextAttemptAt.toSqlTimestamp()) + ps.setInt(2, attempts) + ps.setLong(3, eventId) + } + } + + override fun markDead(eventId: Long, errorClass: ErrorClass, lastError: String, attempts: Int?) { + ds.update( + """ + UPDATE msg_event SET state = 'DEAD', error_class = ?, last_error = ?, + attempts = COALESCE(?, attempts) + WHERE event_id = ? + """.trimIndent(), + ) { ps -> + ps.setString(1, errorClass.name) + ps.setString(2, lastError) + if (attempts != null) ps.setInt(3, attempts) else ps.setNull(3, java.sql.Types.INTEGER) + ps.setLong(4, eventId) + } + } + + override fun insertSync(events: List) { + insertAll(events) + } + + private fun mapEvent(rs: java.sql.ResultSet) = MsgEvent( + eventId = rs.getLong("event_id"), + target = rs.getString("target"), + partitionKey = rs.getString("partition_key"), + payloadJson = rs.getString("payload_json"), + state = EventStatus.valueOf(rs.getString("state")), + attempts = rs.getInt("attempts"), + nextAttemptAt = rs.getInstant("next_attempt_at"), + errorClass = rs.getString("error_class")?.let(ErrorClass::valueOf), + lastError = rs.getString("last_error"), + createdAt = rs.getInstant("created_at") ?: Instant.now(), + ) +} + +@Singleton +@Requires(property = "msgx.stubs", notEquals = "true") +@Requires(property = "datasources.default.enabled", value = "true") +class JdbcPumpJobRepository( + private val ds: DataSource, +) : PumpJobRepository { + + override fun enqueue(kind: String) { + val now = Instant.now() + ds.update( + "INSERT INTO pump_job (kind, state, created_at, updated_at) VALUES (?, 'QUEUED', ?, ?)", + ) { ps -> + ps.setString(1, kind) + ps.setTimestamp(2, now.toSqlTimestamp()) + ps.setTimestamp(3, now.toSqlTimestamp()) + } + } + + override fun headQueued(): PumpJobRepository.Job? = + ds.queryOne( + "SELECT job_id, kind FROM pump_job WHERE state = 'QUEUED' ORDER BY job_id ASC LIMIT 1", + {}, + ) { rs -> PumpJobRepository.Job(rs.getLong("job_id"), rs.getString("kind")) } + + override fun markRunning(jobId: Long) = markState(jobId, "RUNNING", null) + + override fun markDone(jobId: Long) = markState(jobId, "DONE", null) + + override fun markFailed(jobId: Long, lastError: String) = markState(jobId, "FAILED", lastError) + + private fun markState(jobId: Long, state: String, lastError: String?) { + ds.update( + "UPDATE pump_job SET state = ?, updated_at = ?, last_error = ? WHERE job_id = ?", + ) { ps -> + ps.setString(1, state) + ps.setTimestamp(2, Instant.now().toSqlTimestamp()) + ps.setString(3, lastError) + ps.setLong(4, jobId) + } + } +} + +/** gen→Redis 过渡占位:U09 前进程内 CAS(与 StubRefData 同语义)。 */ +@Singleton +@Requires(property = "msgx.stubs", notEquals = "true") +@Requires(property = "datasources.default.enabled", value = "true") +class JdbcRefDataRepository : RefDataRepository { + private val gens = mutableMapOf() + + override fun getGen(day: String): RefDataRepository.GenMeta? = gens[day] + + override fun putGenIfVersion(day: String, expected: Long, new: RefDataRepository.GenMeta): Boolean { + val cur = gens[day]?.version ?: 0L + if (cur != expected) return false + gens[day] = new + return true + } +} + +@Singleton +@Requires(property = "msgx.stubs", notEquals = "true") +@Requires(property = "datasources.default.enabled", value = "true") +class JdbcStaticRefRepository( + private val ds: DataSource, +) : StaticRefRepository { + + override fun upsertAll(refs: List) { + val now = Instant.now() + refs.forEach { r -> + ds.update( + """ + INSERT INTO ref_master (rtype, rkey, payload_json, source, refreshed_at) + VALUES (?, ?, ?, ?, ?) + ON CONFLICT (rtype, rkey) DO UPDATE SET + payload_json = EXCLUDED.payload_json, + source = EXCLUDED.source, + refreshed_at = EXCLUDED.refreshed_at + """.trimIndent(), + ) { ps -> + ps.setString(1, r.rtype) + ps.setString(2, r.rkey) + ps.setString(3, r.payloadJson) + ps.setString(4, r.source) + ps.setTimestamp(5, now.toSqlTimestamp()) + } + } + } + + override fun findByType(type: String): List = + ds.query( + "SELECT rtype, rkey, payload_json, source FROM ref_master WHERE rtype = ?", + { ps -> ps.setString(1, type) }, + ) { rs -> + RefUpsert( + rtype = rs.getString("rtype"), + rkey = rs.getString("rkey"), + payloadJson = rs.getString("payload_json"), + source = rs.getString("source"), + ) + } +} + +@Singleton +@Requires(property = "msgx.stubs", notEquals = "true") +@Requires(property = "datasources.default.enabled", value = "true") +class JdbcReqTrackRepository( + private val ds: DataSource, +) : ReqTrackRepository { + + override fun findOpenByKind(kind: String): ReqTrackRepository.Req? = + ds.queryOne( + """ + SELECT req_id, req_type, state, sent_at FROM req_track + WHERE req_type = ? AND state IN ('REGISTERED', 'SENT', 'WAITING') + ORDER BY req_id ASC LIMIT 1 + """.trimIndent(), + { ps -> ps.setString(1, kind) }, + ) { rs -> + ReqTrackRepository.Req( + reqId = rs.getLong("req_id"), + reqType = rs.getString("req_type"), + state = rs.getString("state"), + sentAt = rs.getInstant("sent_at"), + ) + } + + override fun forceExpireOpenOf(kind: String) { + ds.update( + """ + UPDATE req_track SET state = 'EXPIRED' + WHERE req_type = ? AND state IN ('REGISTERED', 'SENT', 'WAITING') + """.trimIndent(), + ) { ps -> ps.setString(1, kind) } + } + + override fun insert(kind: String, paramsJson: String): Long = + ds.updateReturningLong( + "INSERT INTO req_track (req_type, params_json, state) VALUES (?, ?, 'REGISTERED')", + ) { ps -> + ps.setString(1, kind) + ps.setString(2, paramsJson) + } + + override fun linkCoutmsgs(reqId: Long, coutmsgsId: Long) { + ds.update("UPDATE req_track SET coutmsgs_id = ? WHERE req_id = ?") { ps -> + ps.setLong(1, coutmsgsId) + ps.setLong(2, reqId) + } + } + + override fun markSent(reqId: Long, sentAt: Instant) { + ds.update("UPDATE req_track SET state = 'SENT', sent_at = ? WHERE req_id = ?") { ps -> + ps.setTimestamp(1, sentAt.toSqlTimestamp()) + ps.setLong(2, reqId) + } + } + + override fun expireIfWaiting(reqId: Long) { + ds.update("UPDATE req_track SET state = 'EXPIRED' WHERE req_id = ? AND state = 'WAITING'") { ps -> + ps.setLong(1, reqId) + } + } + + override fun markDone(reqId: Long) { + ds.update( + "UPDATE req_track SET state = 'DONE', completed_at = ? WHERE req_id = ?", + ) { ps -> + ps.setTimestamp(1, Instant.now().toSqlTimestamp()) + ps.setLong(2, reqId) + } + } +} + +@Singleton +@Requires(property = "msgx.stubs", notEquals = "true") +@Requires(property = "datasources.default.enabled", value = "true") +class JdbcFlightStateRepository : FlightStateRepository { + override fun replaceDay(day: String, flights: List>) = Unit + override fun findByDay(day: String): List> = emptyList() +} diff --git a/src/main/kotlin/com/gzzn/omms/msgexchange/infra/persistence/jdbc/MailboxDataSourceFactory.kt b/src/main/kotlin/com/gzzn/omms/msgexchange/infra/persistence/jdbc/MailboxDataSourceFactory.kt new file mode 100644 index 0000000..f10e217 --- /dev/null +++ b/src/main/kotlin/com/gzzn/omms/msgexchange/infra/persistence/jdbc/MailboxDataSourceFactory.kt @@ -0,0 +1,30 @@ +package com.gzzn.omms.msgexchange.infra.persistence.jdbc + +import com.zaxxer.hikari.HikariConfig +import com.zaxxer.hikari.HikariDataSource +import io.micronaut.context.annotation.Factory +import io.micronaut.context.annotation.Requires +import jakarta.inject.Named +import jakarta.inject.Singleton +import javax.sql.DataSource + +@Factory +class MailboxDataSourceFactory { + + @Singleton + @Named("mailbox") + @Requires(property = "msgx.stubs", notEquals = "true") + @Requires(property = "mailbox.shared-mysql.enabled", value = "true") + fun mailboxDataSource(props: com.gzzn.omms.msgexchange.config.MailboxProps): DataSource { + val cfg = props.sharedMysql + val hikari = HikariConfig().apply { + jdbcUrl = cfg.url + username = cfg.username + password = cfg.password + driverClassName = cfg.driverClassName + maximumPoolSize = 5 + poolName = "mailbox" + } + return HikariDataSource(hikari) + } +} diff --git a/src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/infra/redis/RedisScripts.kt b/src/main/kotlin/com/gzzn/omms/msgexchange/infra/redis/RedisScripts.kt similarity index 95% rename from src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/infra/redis/RedisScripts.kt rename to src/main/kotlin/com/gzzn/omms/msgexchange/infra/redis/RedisScripts.kt index 17c0e77..9fa1f9f 100644 --- a/src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/infra/redis/RedisScripts.kt +++ b/src/main/kotlin/com/gzzn/omms/msgexchange/infra/redis/RedisScripts.kt @@ -1,4 +1,4 @@ -package com.gzzn.omms.msgexchange.nextgen.infra.redis +package com.gzzn.omms.msgexchange.infra.redis /** * ACMA-8 流程 4 / I4 / I5:Redis Lua 脚本装载与执行入口。 diff --git a/src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/infra/retry/FailureScheduler.kt b/src/main/kotlin/com/gzzn/omms/msgexchange/infra/retry/FailureScheduler.kt similarity index 91% rename from src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/infra/retry/FailureScheduler.kt rename to src/main/kotlin/com/gzzn/omms/msgexchange/infra/retry/FailureScheduler.kt index 742345e..60d706e 100644 --- a/src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/infra/retry/FailureScheduler.kt +++ b/src/main/kotlin/com/gzzn/omms/msgexchange/infra/retry/FailureScheduler.kt @@ -1,6 +1,6 @@ -package com.gzzn.omms.msgexchange.nextgen.infra.retry +package com.gzzn.omms.msgexchange.infra.retry -import com.gzzn.omms.msgexchange.nextgen.config.PipelineProps +import com.gzzn.omms.msgexchange.config.PipelineProps import io.micronaut.context.annotation.Factory import jakarta.inject.Singleton import java.time.Clock diff --git a/src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/infra/retry/ProcFailure.kt b/src/main/kotlin/com/gzzn/omms/msgexchange/infra/retry/ProcFailure.kt similarity index 79% rename from src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/infra/retry/ProcFailure.kt rename to src/main/kotlin/com/gzzn/omms/msgexchange/infra/retry/ProcFailure.kt index 456af55..3c5208c 100644 --- a/src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/infra/retry/ProcFailure.kt +++ b/src/main/kotlin/com/gzzn/omms/msgexchange/infra/retry/ProcFailure.kt @@ -1,9 +1,9 @@ -package com.gzzn.omms.msgexchange.nextgen.infra.retry +package com.gzzn.omms.msgexchange.infra.retry -import com.gzzn.omms.msgexchange.nextgen.domain.ErrorClass -import com.gzzn.omms.msgexchange.nextgen.domain.ProcState -import com.gzzn.omms.msgexchange.nextgen.domain.ProcStatus -import com.gzzn.omms.msgexchange.nextgen.infra.persistence.ProcStateRepository +import com.gzzn.omms.msgexchange.domain.ErrorClass +import com.gzzn.omms.msgexchange.domain.ProcState +import com.gzzn.omms.msgexchange.domain.ProcStatus +import com.gzzn.omms.msgexchange.infra.persistence.ProcStateRepository import jakarta.inject.Singleton /** diff --git a/src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/infra/retry/ReplayService.kt b/src/main/kotlin/com/gzzn/omms/msgexchange/infra/retry/ReplayService.kt similarity index 87% rename from src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/infra/retry/ReplayService.kt rename to src/main/kotlin/com/gzzn/omms/msgexchange/infra/retry/ReplayService.kt index 6cca1a4..5a1a080 100644 --- a/src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/infra/retry/ReplayService.kt +++ b/src/main/kotlin/com/gzzn/omms/msgexchange/infra/retry/ReplayService.kt @@ -1,7 +1,7 @@ -package com.gzzn.omms.msgexchange.nextgen.infra.retry +package com.gzzn.omms.msgexchange.infra.retry -import com.gzzn.omms.msgexchange.nextgen.domain.ErrorClass -import com.gzzn.omms.msgexchange.nextgen.infra.persistence.ProcStateRepository +import com.gzzn.omms.msgexchange.domain.ErrorClass +import com.gzzn.omms.msgexchange.infra.persistence.ProcStateRepository import jakarta.inject.Singleton /** diff --git a/src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/infra/stub/StubAdapters.kt b/src/main/kotlin/com/gzzn/omms/msgexchange/infra/stub/StubAdapters.kt similarity index 77% rename from src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/infra/stub/StubAdapters.kt rename to src/main/kotlin/com/gzzn/omms/msgexchange/infra/stub/StubAdapters.kt index 98e872b..1814190 100644 --- a/src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/infra/stub/StubAdapters.kt +++ b/src/main/kotlin/com/gzzn/omms/msgexchange/infra/stub/StubAdapters.kt @@ -1,15 +1,15 @@ -package com.gzzn.omms.msgexchange.nextgen.infra.stub +package com.gzzn.omms.msgexchange.infra.stub -import com.gzzn.omms.msgexchange.nextgen.codec.DecodeFailure -import com.gzzn.omms.msgexchange.nextgen.codec.DecodeResult -import com.gzzn.omms.msgexchange.nextgen.codec.XmlCodec -import com.gzzn.omms.msgexchange.nextgen.delivery.DeliveryPort -import com.gzzn.omms.msgexchange.nextgen.domain.ErrorClass -import com.gzzn.omms.msgexchange.nextgen.infra.redis.FlightRedisClient -import com.gzzn.omms.msgexchange.nextgen.infra.redis.RedisScript -import com.gzzn.omms.msgexchange.nextgen.processing.CodecHolder -import com.gzzn.omms.msgexchange.nextgen.processing.HandlerHolder -import com.gzzn.omms.msgexchange.nextgen.processing.HandlerRegistry +import com.gzzn.omms.msgexchange.codec.DecodeFailure +import com.gzzn.omms.msgexchange.codec.DecodeResult +import com.gzzn.omms.msgexchange.codec.XmlCodec +import com.gzzn.omms.msgexchange.delivery.DeliveryPort +import com.gzzn.omms.msgexchange.domain.ErrorClass +import com.gzzn.omms.msgexchange.infra.redis.FlightRedisClient +import com.gzzn.omms.msgexchange.infra.redis.RedisScript +import com.gzzn.omms.msgexchange.processing.CodecHolder +import com.gzzn.omms.msgexchange.processing.HandlerHolder +import com.gzzn.omms.msgexchange.processing.HandlerRegistry import io.micronaut.context.annotation.Bean import io.micronaut.context.annotation.Factory import io.micronaut.context.annotation.Requires diff --git a/src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/infra/stub/StubRepositories.kt b/src/main/kotlin/com/gzzn/omms/msgexchange/infra/stub/StubRepositories.kt similarity index 84% rename from src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/infra/stub/StubRepositories.kt rename to src/main/kotlin/com/gzzn/omms/msgexchange/infra/stub/StubRepositories.kt index 0123713..33d59c8 100644 --- a/src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/infra/stub/StubRepositories.kt +++ b/src/main/kotlin/com/gzzn/omms/msgexchange/infra/stub/StubRepositories.kt @@ -1,19 +1,19 @@ -package com.gzzn.omms.msgexchange.nextgen.infra.stub +package com.gzzn.omms.msgexchange.infra.stub -import com.gzzn.omms.msgexchange.nextgen.domain.ErrorClass -import com.gzzn.omms.msgexchange.nextgen.domain.EventStatus -import com.gzzn.omms.msgexchange.nextgen.domain.MsgEvent -import com.gzzn.omms.msgexchange.nextgen.domain.ProcState -import com.gzzn.omms.msgexchange.nextgen.domain.ProcStatus -import com.gzzn.omms.msgexchange.nextgen.domain.RefUpsert -import com.gzzn.omms.msgexchange.nextgen.infra.persistence.CminmsgInboxRepository -import com.gzzn.omms.msgexchange.nextgen.infra.persistence.FlightStateRepository -import com.gzzn.omms.msgexchange.nextgen.infra.persistence.MsgEventRepository -import com.gzzn.omms.msgexchange.nextgen.infra.persistence.ProcStateRepository -import com.gzzn.omms.msgexchange.nextgen.infra.persistence.PumpJobRepository -import com.gzzn.omms.msgexchange.nextgen.infra.persistence.RefDataRepository -import com.gzzn.omms.msgexchange.nextgen.infra.persistence.ReqTrackRepository -import com.gzzn.omms.msgexchange.nextgen.infra.persistence.StaticRefRepository +import com.gzzn.omms.msgexchange.domain.ErrorClass +import com.gzzn.omms.msgexchange.domain.EventStatus +import com.gzzn.omms.msgexchange.domain.MsgEvent +import com.gzzn.omms.msgexchange.domain.ProcState +import com.gzzn.omms.msgexchange.domain.ProcStatus +import com.gzzn.omms.msgexchange.domain.RefUpsert +import com.gzzn.omms.msgexchange.infra.persistence.CminmsgInboxRepository +import com.gzzn.omms.msgexchange.infra.persistence.FlightStateRepository +import com.gzzn.omms.msgexchange.infra.persistence.MsgEventRepository +import com.gzzn.omms.msgexchange.infra.persistence.ProcStateRepository +import com.gzzn.omms.msgexchange.infra.persistence.PumpJobRepository +import com.gzzn.omms.msgexchange.infra.persistence.RefDataRepository +import com.gzzn.omms.msgexchange.infra.persistence.ReqTrackRepository +import com.gzzn.omms.msgexchange.infra.persistence.StaticRefRepository import io.micronaut.context.annotation.Requires import jakarta.inject.Singleton import java.time.Instant @@ -36,6 +36,8 @@ class StubProcState : ProcStateRepository { rows[cminmsgsId] = ProcState(cminmsgsId, state) } + override fun exists(cminmsgsId: Long): Boolean = rows.containsKey(cminmsgsId) + override fun headUnfinished(): ProcState? = rows.filterValues { it.state == ProcStatus.PENDING || it.state == ProcStatus.FAILED } .minByOrNull { it.key }?.value @@ -85,10 +87,21 @@ class StubProcState : ProcStateRepository { @Singleton class StubInbox : CminmsgInboxRepository { private val raws = linkedMapOf() + private val processed = mutableSetOf() - fun clear() { raws.clear() } + fun clear() { + raws.clear() + processed.clear() + } private val ids = AtomicLong(0) + /** 模拟上游外部写信箱(不经 compat HTTP、不自动入队 PG)。 */ + fun simulateExternalWrite(rawXml: String): Long { + val id = ids.incrementAndGet() + raws[id] = rawXml + return id + } + override fun insertRaw(rawXml: String): Long { val id = ids.incrementAndGet() raws[id] = rawXml @@ -97,8 +110,14 @@ class StubInbox : CminmsgInboxRepository { override fun rawOf(cminmsgsId: Long): String? = raws[cminmsgsId] + override fun pollUnprocessed(afterId: Long, limit: Int): List = + raws.keys + .filter { it > afterId && it !in processed } + .sorted() + .take(limit) + override fun backfillOnSuccess(cminmsgsId: Long, sndr: String, type: String, styp: String, seqn: Long) { - // 内存 stub:无列可回填,记录即可(后续由真实实装/审计消费) + processed += cminmsgsId } } diff --git a/src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/ingress/InboxController.kt b/src/main/kotlin/com/gzzn/omms/msgexchange/ingress/InboxController.kt similarity index 94% rename from src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/ingress/InboxController.kt rename to src/main/kotlin/com/gzzn/omms/msgexchange/ingress/InboxController.kt index fa4d375..94ec41b 100644 --- a/src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/ingress/InboxController.kt +++ b/src/main/kotlin/com/gzzn/omms/msgexchange/ingress/InboxController.kt @@ -1,4 +1,4 @@ -package com.gzzn.omms.msgexchange.nextgen.ingress +package com.gzzn.omms.msgexchange.ingress import io.micronaut.http.HttpResponse import io.micronaut.http.MediaType diff --git a/src/main/kotlin/com/gzzn/omms/msgexchange/ingress/InboxEnqueue.kt b/src/main/kotlin/com/gzzn/omms/msgexchange/ingress/InboxEnqueue.kt new file mode 100644 index 0000000..2fe04b2 --- /dev/null +++ b/src/main/kotlin/com/gzzn/omms/msgexchange/ingress/InboxEnqueue.kt @@ -0,0 +1,16 @@ +package com.gzzn.omms.msgexchange.ingress + +import com.gzzn.omms.msgexchange.infra.persistence.ProcStateRepository +import jakarta.inject.Singleton + +/** 流程 1 入队:共享库 CMINMSGS_ID → 自有 PG PROC_STATE(PENDING),幂等跳过已存在行。 */ +@Singleton +class InboxEnqueue(private val procState: ProcStateRepository) { + + /** @return true 若新建 PENDING 行;false 若已存在(轮询重扫/compensate 幂等)。 */ + fun enqueue(cminmsgsId: Long): Boolean { + if (procState.exists(cminmsgsId)) return false + procState.insert(cminmsgsId) + return true + } +} diff --git a/src/main/kotlin/com/gzzn/omms/msgexchange/ingress/InboxPoller.kt b/src/main/kotlin/com/gzzn/omms/msgexchange/ingress/InboxPoller.kt new file mode 100644 index 0000000..02a238e --- /dev/null +++ b/src/main/kotlin/com/gzzn/omms/msgexchange/ingress/InboxPoller.kt @@ -0,0 +1,65 @@ +package com.gzzn.omms.msgexchange.ingress + +import com.gzzn.omms.msgexchange.config.PipelineProps +import com.gzzn.omms.msgexchange.infra.persistence.CminmsgInboxRepository +import jakarta.inject.Singleton + +/** + * ACMA-8 流程 1 · 主路径:JDBC 轮询共享 MySQL CMINMSGS(`DATE_PROCESSED IS NULL`), + * 发现新信后入队自有 PG。与 legacy `MsgExchangeRunner.getNewMsgsAfterId(0L)` 同语义。 + */ +@Singleton +class InboxPoller( + private val inbox: CminmsgInboxRepository, + private val enqueue: InboxEnqueue, + private val props: PipelineProps, +) { + private val log = org.slf4j.LoggerFactory.getLogger(InboxPoller::class.java) + + @Volatile + private var running = false + + /** legacy 现役:afterId=0,每轮扫全部未处理行;PROC_STATE 判重防重复入队。 */ + fun pollOnce(): Int { + val batch = props.pipeline.claimBatch.coerceAtLeast(1) + val ids = inbox.pollUnprocessed(afterId = 0L, limit = batch) + var enqueued = 0 + for (id in ids) { + if (enqueue.enqueue(id)) { + enqueued++ + log.info("polled cminmsgsId={}", id) + } + } + return enqueued + } + + fun loop() { + running = true + log.info("inbox poller loop started") + while (running) { + try { + pollOnce() + sleepQuietly(props.pipeline.pollInterval) + } catch (_: InterruptedException) { + Thread.currentThread().interrupt() + break + } catch (e: Exception) { + log.error("inbox poller tick failed", e) + sleepQuietly(props.pipeline.pollInterval) + } + } + log.info("inbox poller loop stopped") + } + + fun stop() { + running = false + } + + private fun sleepQuietly(d: java.time.Duration) { + try { + Thread.sleep(d.toMillis().coerceAtLeast(1)) + } catch (_: InterruptedException) { + Thread.currentThread().interrupt() + } + } +} diff --git a/src/main/kotlin/com/gzzn/omms/msgexchange/ingress/InboxService.kt b/src/main/kotlin/com/gzzn/omms/msgexchange/ingress/InboxService.kt new file mode 100644 index 0000000..acb7527 --- /dev/null +++ b/src/main/kotlin/com/gzzn/omms/msgexchange/ingress/InboxService.kt @@ -0,0 +1,23 @@ +package com.gzzn.omms.msgexchange.ingress + +import com.gzzn.omms.msgexchange.infra.persistence.CminmsgInboxRepository +import jakarta.inject.Singleton +import java.time.Instant + +/** ACMA-8 流程 1 · compat 写路径:HTTP 落信 + PG 入队(非生产主拓扑;主路径=InboxPoller JDBC 轮询)。 */ +@Singleton +class InboxService( + private val inbox: CminmsgInboxRepository, + private val enqueue: InboxEnqueue, +) { + private val log = org.slf4j.LoggerFactory.getLogger(InboxService::class.java) + + data class Receipt(val cminmsgsId: Long, val receivedAt: Instant) + + fun accept(rawXml: String): Receipt { + val id = inbox.insertRaw(rawXml) + enqueue.enqueue(id) + log.info("compat-accepted cminmsgsId={}", id) + return Receipt(id, Instant.now()) + } +} diff --git a/src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/jobs/JobExecutor.kt b/src/main/kotlin/com/gzzn/omms/msgexchange/jobs/JobExecutor.kt similarity index 84% rename from src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/jobs/JobExecutor.kt rename to src/main/kotlin/com/gzzn/omms/msgexchange/jobs/JobExecutor.kt index 7dea6e7..b224ed7 100644 --- a/src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/jobs/JobExecutor.kt +++ b/src/main/kotlin/com/gzzn/omms/msgexchange/jobs/JobExecutor.kt @@ -1,12 +1,12 @@ -package com.gzzn.omms.msgexchange.nextgen.jobs +package com.gzzn.omms.msgexchange.jobs -import com.gzzn.omms.msgexchange.nextgen.infra.redis.FlightRedisClient -import com.gzzn.omms.msgexchange.nextgen.infra.redis.RedisScript -import com.gzzn.omms.msgexchange.nextgen.infra.persistence.FlightStateRepository -import com.gzzn.omms.msgexchange.nextgen.infra.persistence.MsgEventRepository -import com.gzzn.omms.msgexchange.nextgen.infra.persistence.PumpJobRepository -import com.gzzn.omms.msgexchange.nextgen.domain.MsgEvent -import com.gzzn.omms.msgexchange.nextgen.domain.Targets +import com.gzzn.omms.msgexchange.infra.redis.FlightRedisClient +import com.gzzn.omms.msgexchange.infra.redis.RedisScript +import com.gzzn.omms.msgexchange.infra.persistence.FlightStateRepository +import com.gzzn.omms.msgexchange.infra.persistence.MsgEventRepository +import com.gzzn.omms.msgexchange.infra.persistence.PumpJobRepository +import com.gzzn.omms.msgexchange.domain.MsgEvent +import com.gzzn.omms.msgexchange.domain.Targets import jakarta.inject.Singleton /** diff --git a/src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/ingress/InboxService.kt b/src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/ingress/InboxService.kt deleted file mode 100644 index b0edc56..0000000 --- a/src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/ingress/InboxService.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.gzzn.omms.msgexchange.nextgen.ingress - -import com.gzzn.omms.msgexchange.nextgen.infra.persistence.CminmsgInboxRepository -import com.gzzn.omms.msgexchange.nextgen.infra.persistence.ProcStateRepository -import jakarta.inject.Singleton -import java.time.Instant - -/** ACMA-8 流程 1 · compat 写路径:HTTP 落信 + PG 入队(非生产主拓扑;主路径=InboxPoller JDBC 轮询,U05)。 */ -@Singleton -class InboxService( - private val inbox: CminmsgInboxRepository, - private val procState: ProcStateRepository, - // TODO(U05): 主路径 InboxPoller — JDBC 轮询 DATE_PROCESSED IS NULL → procState.insert; - // compat 路径 insertRaw + PG 入队跨库;PG 失败以共享库重扫补建;wakePump 仅加速。 -) { - private val log = org.slf4j.LoggerFactory.getLogger(InboxService::class.java) - - data class Receipt(val cminmsgsId: Long, val receivedAt: Instant) - - fun accept(rawXml: String): Receipt { - val id = inbox.insertRaw(rawXml) // compat:共享信箱外部写(ACM2-12,与 PG 跨库) - procState.insert(id) // 自有 PG 入队;接收层无唯一约束(I3) - wakePump() - log.info("accepted cminmsgsId={}", id) - return Receipt(id, Instant.now()) - } - - private fun wakePump() = Unit // TODO: 主泵唤醒(仅加速) -} diff --git a/src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/processing/Handler.kt b/src/main/kotlin/com/gzzn/omms/msgexchange/processing/Handler.kt similarity index 83% rename from src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/processing/Handler.kt rename to src/main/kotlin/com/gzzn/omms/msgexchange/processing/Handler.kt index b2eebaa..e13b272 100644 --- a/src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/processing/Handler.kt +++ b/src/main/kotlin/com/gzzn/omms/msgexchange/processing/Handler.kt @@ -1,8 +1,8 @@ -package com.gzzn.omms.msgexchange.nextgen.processing +package com.gzzn.omms.msgexchange.processing -import com.gzzn.omms.msgexchange.nextgen.domain.DecodedMessage -import com.gzzn.omms.msgexchange.nextgen.domain.Decision -import com.gzzn.omms.msgexchange.nextgen.domain.MsgKind +import com.gzzn.omms.msgexchange.domain.DecodedMessage +import com.gzzn.omms.msgexchange.domain.Decision +import com.gzzn.omms.msgexchange.domain.MsgKind /** * ACMA-8 流程 2:Handler = 纯函数(状态与报文进,Decision 出,不碰 Redis/Kafka)。 diff --git a/src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/processing/Identity.kt b/src/main/kotlin/com/gzzn/omms/msgexchange/processing/Identity.kt similarity index 79% rename from src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/processing/Identity.kt rename to src/main/kotlin/com/gzzn/omms/msgexchange/processing/Identity.kt index 892b88e..37bd5b0 100644 --- a/src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/processing/Identity.kt +++ b/src/main/kotlin/com/gzzn/omms/msgexchange/processing/Identity.kt @@ -1,7 +1,7 @@ -package com.gzzn.omms.msgexchange.nextgen.processing +package com.gzzn.omms.msgexchange.processing -import com.gzzn.omms.msgexchange.nextgen.config.PipelineProps -import com.gzzn.omms.msgexchange.nextgen.domain.DecodedMessage +import com.gzzn.omms.msgexchange.config.PipelineProps +import com.gzzn.omms.msgexchange.domain.DecodedMessage import java.time.LocalDate /** diff --git a/src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/processing/Pump.kt b/src/main/kotlin/com/gzzn/omms/msgexchange/processing/Pump.kt similarity index 86% rename from src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/processing/Pump.kt rename to src/main/kotlin/com/gzzn/omms/msgexchange/processing/Pump.kt index ff01f52..2fca032 100644 --- a/src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/processing/Pump.kt +++ b/src/main/kotlin/com/gzzn/omms/msgexchange/processing/Pump.kt @@ -1,19 +1,19 @@ -package com.gzzn.omms.msgexchange.nextgen.processing +package com.gzzn.omms.msgexchange.processing -import com.gzzn.omms.msgexchange.nextgen.config.PipelineProps -import com.gzzn.omms.msgexchange.nextgen.domain.DecodedMessage -import com.gzzn.omms.msgexchange.nextgen.domain.ErrorClass -import com.gzzn.omms.msgexchange.nextgen.domain.MsgEvent -import com.gzzn.omms.msgexchange.nextgen.domain.ProcState -import com.gzzn.omms.msgexchange.nextgen.domain.ProcStatus -import com.gzzn.omms.msgexchange.nextgen.domain.Targets -import com.gzzn.omms.msgexchange.nextgen.infra.persistence.CminmsgInboxRepository -import com.gzzn.omms.msgexchange.nextgen.infra.persistence.MsgEventRepository -import com.gzzn.omms.msgexchange.nextgen.infra.persistence.ProcStateRepository -import com.gzzn.omms.msgexchange.nextgen.infra.persistence.PumpJobRepository -import com.gzzn.omms.msgexchange.nextgen.infra.redis.FlightRedisClient -import com.gzzn.omms.msgexchange.nextgen.infra.retry.ProcFailure -import com.gzzn.omms.msgexchange.nextgen.jobs.JobExecutor +import com.gzzn.omms.msgexchange.config.PipelineProps +import com.gzzn.omms.msgexchange.domain.DecodedMessage +import com.gzzn.omms.msgexchange.domain.ErrorClass +import com.gzzn.omms.msgexchange.domain.MsgEvent +import com.gzzn.omms.msgexchange.domain.ProcState +import com.gzzn.omms.msgexchange.domain.ProcStatus +import com.gzzn.omms.msgexchange.domain.Targets +import com.gzzn.omms.msgexchange.infra.persistence.CminmsgInboxRepository +import com.gzzn.omms.msgexchange.infra.persistence.MsgEventRepository +import com.gzzn.omms.msgexchange.infra.persistence.ProcStateRepository +import com.gzzn.omms.msgexchange.infra.persistence.PumpJobRepository +import com.gzzn.omms.msgexchange.infra.redis.FlightRedisClient +import com.gzzn.omms.msgexchange.infra.retry.ProcFailure +import com.gzzn.omms.msgexchange.jobs.JobExecutor import jakarta.inject.Singleton import java.time.Duration import java.time.Instant @@ -122,7 +122,7 @@ class MessageProcessor( private val log = org.slf4j.LoggerFactory.getLogger(MessageProcessor::class.java) fun processOne(head: ProcState) { - com.gzzn.omms.msgexchange.nextgen.infra.log.TraceLog.withTrace(head.cminmsgsId) { + com.gzzn.omms.msgexchange.infra.log.TraceLog.withTrace(head.cminmsgsId) { try { processInternal(head) } catch (e: InterruptedException) { @@ -151,8 +151,8 @@ class MessageProcessor( return } val decoded = when (val r = codecHolder.codec.decode(raw)) { - is com.gzzn.omms.msgexchange.nextgen.codec.DecodeResult.Ok -> r.message - is com.gzzn.omms.msgexchange.nextgen.codec.DecodeResult.Err -> { + is com.gzzn.omms.msgexchange.codec.DecodeResult.Ok -> r.message + is com.gzzn.omms.msgexchange.codec.DecodeResult.Err -> { // T06(U11):MALFORMED(报文非法)→ DEAD 不重试;CODEC_ERROR(可随 codec 修复重放)→ FAILED 退避 if (r.failure.errorClass == ErrorClass.MALFORMED) { log.error("decode MALFORMED -> DEAD id={} detail={}", head.cminmsgsId, r.failure.detail) @@ -185,9 +185,9 @@ class MessageProcessor( procFailure.fail(head, ErrorClass.UNSUPPORTED, "no-handler:${decoded.typeTag}") return } - val schdKind = decoded.kind as? com.gzzn.omms.msgexchange.nextgen.domain.MsgKind.Schd + val schdKind = decoded.kind as? com.gzzn.omms.msgexchange.domain.MsgKind.Schd if (schdKind != null && - schdKind.subtype == com.gzzn.omms.msgexchange.nextgen.domain.MsgKind.SchdSubtype.DNLD + schdKind.subtype == com.gzzn.omms.msgexchange.domain.MsgKind.SchdSubtype.DNLD ) { snapshotFlow.publishSnapshot(head, decoded) return @@ -214,5 +214,5 @@ class MessageProcessor( } /** 延迟装配占位(阶段 1 后续以 Micronaut Bean 替换直连构造)。 */ -class CodecHolder(val codec: com.gzzn.omms.msgexchange.nextgen.codec.XmlCodec) +class CodecHolder(val codec: com.gzzn.omms.msgexchange.codec.XmlCodec) class HandlerHolder(val registry: HandlerRegistry) diff --git a/src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/processing/SnapshotFlow.kt b/src/main/kotlin/com/gzzn/omms/msgexchange/processing/SnapshotFlow.kt similarity index 83% rename from src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/processing/SnapshotFlow.kt rename to src/main/kotlin/com/gzzn/omms/msgexchange/processing/SnapshotFlow.kt index 993951a..51811e0 100644 --- a/src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/processing/SnapshotFlow.kt +++ b/src/main/kotlin/com/gzzn/omms/msgexchange/processing/SnapshotFlow.kt @@ -1,14 +1,14 @@ -package com.gzzn.omms.msgexchange.nextgen.processing +package com.gzzn.omms.msgexchange.processing -import com.gzzn.omms.msgexchange.nextgen.domain.DecodedMessage -import com.gzzn.omms.msgexchange.nextgen.domain.ErrorClass -import com.gzzn.omms.msgexchange.nextgen.domain.ProcState -import com.gzzn.omms.msgexchange.nextgen.domain.ProcStatus -import com.gzzn.omms.msgexchange.nextgen.infra.persistence.ProcStateRepository -import com.gzzn.omms.msgexchange.nextgen.infra.persistence.RefDataRepository -import com.gzzn.omms.msgexchange.nextgen.infra.redis.FlightRedisClient -import com.gzzn.omms.msgexchange.nextgen.infra.redis.RedisScript -import com.gzzn.omms.msgexchange.nextgen.infra.retry.ProcFailure +import com.gzzn.omms.msgexchange.domain.DecodedMessage +import com.gzzn.omms.msgexchange.domain.ErrorClass +import com.gzzn.omms.msgexchange.domain.ProcState +import com.gzzn.omms.msgexchange.domain.ProcStatus +import com.gzzn.omms.msgexchange.infra.persistence.ProcStateRepository +import com.gzzn.omms.msgexchange.infra.persistence.RefDataRepository +import com.gzzn.omms.msgexchange.infra.redis.FlightRedisClient +import com.gzzn.omms.msgexchange.infra.redis.RedisScript +import com.gzzn.omms.msgexchange.infra.retry.ProcFailure import jakarta.inject.Singleton /** diff --git a/src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/reference/ReferenceService.kt b/src/main/kotlin/com/gzzn/omms/msgexchange/reference/ReferenceService.kt similarity index 83% rename from src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/reference/ReferenceService.kt rename to src/main/kotlin/com/gzzn/omms/msgexchange/reference/ReferenceService.kt index 1f3f76d..c81f312 100644 --- a/src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/reference/ReferenceService.kt +++ b/src/main/kotlin/com/gzzn/omms/msgexchange/reference/ReferenceService.kt @@ -1,6 +1,6 @@ -package com.gzzn.omms.msgexchange.nextgen.reference +package com.gzzn.omms.msgexchange.reference -import com.gzzn.omms.msgexchange.nextgen.infra.persistence.StaticRefRepository +import com.gzzn.omms.msgexchange.infra.persistence.StaticRefRepository import jakarta.inject.Singleton /** diff --git a/src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/reference/RequestCoordinator.kt b/src/main/kotlin/com/gzzn/omms/msgexchange/reference/RequestCoordinator.kt similarity index 90% rename from src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/reference/RequestCoordinator.kt rename to src/main/kotlin/com/gzzn/omms/msgexchange/reference/RequestCoordinator.kt index a4ef1ce..15a5bc8 100644 --- a/src/main/kotlin/com/gzzn/omms/msgexchange/nextgen/reference/RequestCoordinator.kt +++ b/src/main/kotlin/com/gzzn/omms/msgexchange/reference/RequestCoordinator.kt @@ -1,8 +1,8 @@ -package com.gzzn.omms.msgexchange.nextgen.reference +package com.gzzn.omms.msgexchange.reference -import com.gzzn.omms.msgexchange.nextgen.domain.RefUpsert -import com.gzzn.omms.msgexchange.nextgen.infra.persistence.ReqTrackRepository -import com.gzzn.omms.msgexchange.nextgen.infra.persistence.StaticRefRepository +import com.gzzn.omms.msgexchange.domain.RefUpsert +import com.gzzn.omms.msgexchange.infra.persistence.ReqTrackRepository +import com.gzzn.omms.msgexchange.infra.persistence.StaticRefRepository import jakarta.inject.Singleton import java.time.Instant diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index 1ba57c3..96ab705 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -32,5 +32,5 @@ - + diff --git a/src/test/kotlin/com/gzzn/omms/msgexchange/nextgen/PipelineSmokeTest.kt b/src/test/kotlin/com/gzzn/omms/msgexchange/PipelineSmokeTest.kt similarity index 77% rename from src/test/kotlin/com/gzzn/omms/msgexchange/nextgen/PipelineSmokeTest.kt rename to src/test/kotlin/com/gzzn/omms/msgexchange/PipelineSmokeTest.kt index 9bfc889..12128dd 100644 --- a/src/test/kotlin/com/gzzn/omms/msgexchange/nextgen/PipelineSmokeTest.kt +++ b/src/test/kotlin/com/gzzn/omms/msgexchange/PipelineSmokeTest.kt @@ -1,15 +1,15 @@ -package com.gzzn.omms.msgexchange.nextgen +package com.gzzn.omms.msgexchange -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 com.gzzn.omms.msgexchange.delivery.Dispatcher +import com.gzzn.omms.msgexchange.domain.ErrorClass +import com.gzzn.omms.msgexchange.domain.MsgEvent +import com.gzzn.omms.msgexchange.domain.ProcStatus +import com.gzzn.omms.msgexchange.domain.Targets +import com.gzzn.omms.msgexchange.infra.stub.StubDeliveryPort +import com.gzzn.omms.msgexchange.infra.stub.StubMsgEvents +import com.gzzn.omms.msgexchange.infra.stub.StubProcState +import com.gzzn.omms.msgexchange.ingress.InboxController +import com.gzzn.omms.msgexchange.processing.Pump import io.micronaut.context.ApplicationContext import io.micronaut.test.extensions.junit5.annotation.MicronautTest import jakarta.inject.Inject @@ -22,7 +22,7 @@ import org.junit.jupiter.api.Test * U07 端到端(stub 装配,等价 /beans 核验):无 MySQL/Redis/Kafka 下, * ①核心 bean 装配齐全;②compat HTTP 写→PG 入队→主泵领取→解码未实装→FAILED(CODEC_ERROR)+退避; * ③U11 重放把 FAILED 拉回 PENDING;④Dispatcher flushSchd 聚合发出 schd。 - * (生产主路径 JDBC 轮询 InboxPoller 属 U05,本测未覆盖。) + * (生产主路径 JDBC 轮询 InboxPoller;compat HTTP 写路径见 InboxService)。 * 后台循环关闭(autostart=false),按需手动 tick,避免测试泄漏线程。 */ @MicronautTest @@ -44,14 +44,14 @@ class PipelineSmokeTest { @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(com.gzzn.omms.msgexchange.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)) + assertNotNull(ctx.getBean(com.gzzn.omms.msgexchange.config.PipelineProps::class.java)) assertTrue(ctx.getBeansOfType(Pump::class.java).isNotEmpty()) assertTrue(ctx.getBeansOfType(Dispatcher::class.java).isNotEmpty()) assertNotNull(controller) @@ -82,7 +82,7 @@ class PipelineSmokeTest { 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) + val n = ctx.getBean(com.gzzn.omms.msgexchange.infra.retry.ReplayService::class.java) .replay(listOf(ErrorClass.CODEC_ERROR)) assertEquals(1, n) diff --git a/src/test/kotlin/com/gzzn/omms/msgexchange/nextgen/TestClocks.kt b/src/test/kotlin/com/gzzn/omms/msgexchange/TestClocks.kt similarity index 94% rename from src/test/kotlin/com/gzzn/omms/msgexchange/nextgen/TestClocks.kt rename to src/test/kotlin/com/gzzn/omms/msgexchange/TestClocks.kt index 9729b4a..fe0e193 100644 --- a/src/test/kotlin/com/gzzn/omms/msgexchange/nextgen/TestClocks.kt +++ b/src/test/kotlin/com/gzzn/omms/msgexchange/TestClocks.kt @@ -1,4 +1,4 @@ -package com.gzzn.omms.msgexchange.nextgen +package com.gzzn.omms.msgexchange import java.time.Clock import java.time.Instant diff --git a/src/test/kotlin/com/gzzn/omms/msgexchange/nextgen/config/InfraBindingStartupTest.kt b/src/test/kotlin/com/gzzn/omms/msgexchange/config/InfraBindingStartupTest.kt similarity index 96% rename from src/test/kotlin/com/gzzn/omms/msgexchange/nextgen/config/InfraBindingStartupTest.kt rename to src/test/kotlin/com/gzzn/omms/msgexchange/config/InfraBindingStartupTest.kt index 25d5ee8..1441ae4 100644 --- a/src/test/kotlin/com/gzzn/omms/msgexchange/nextgen/config/InfraBindingStartupTest.kt +++ b/src/test/kotlin/com/gzzn/omms/msgexchange/config/InfraBindingStartupTest.kt @@ -1,4 +1,4 @@ -package com.gzzn.omms.msgexchange.nextgen.config +package com.gzzn.omms.msgexchange.config import io.micronaut.test.extensions.junit5.annotation.MicronautTest import jakarta.inject.Inject diff --git a/src/test/kotlin/com/gzzn/omms/msgexchange/nextgen/config/PipelinePropsBindingTest.kt b/src/test/kotlin/com/gzzn/omms/msgexchange/config/PipelinePropsBindingTest.kt similarity index 97% rename from src/test/kotlin/com/gzzn/omms/msgexchange/nextgen/config/PipelinePropsBindingTest.kt rename to src/test/kotlin/com/gzzn/omms/msgexchange/config/PipelinePropsBindingTest.kt index 9b5d28f..dcde4b5 100644 --- a/src/test/kotlin/com/gzzn/omms/msgexchange/nextgen/config/PipelinePropsBindingTest.kt +++ b/src/test/kotlin/com/gzzn/omms/msgexchange/config/PipelinePropsBindingTest.kt @@ -1,4 +1,4 @@ -package com.gzzn.omms.msgexchange.nextgen.config +package com.gzzn.omms.msgexchange.config import io.micronaut.test.extensions.junit5.annotation.MicronautTest import io.micronaut.test.support.TestPropertyProvider diff --git a/src/test/kotlin/com/gzzn/omms/msgexchange/nextgen/config/PipelinePropsTest.kt b/src/test/kotlin/com/gzzn/omms/msgexchange/config/PipelinePropsTest.kt similarity index 94% rename from src/test/kotlin/com/gzzn/omms/msgexchange/nextgen/config/PipelinePropsTest.kt rename to src/test/kotlin/com/gzzn/omms/msgexchange/config/PipelinePropsTest.kt index 5be2651..2122031 100644 --- a/src/test/kotlin/com/gzzn/omms/msgexchange/nextgen/config/PipelinePropsTest.kt +++ b/src/test/kotlin/com/gzzn/omms/msgexchange/config/PipelinePropsTest.kt @@ -1,4 +1,4 @@ -package com.gzzn.omms.msgexchange.nextgen.config +package com.gzzn.omms.msgexchange.config import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test diff --git a/src/test/kotlin/com/gzzn/omms/msgexchange/nextgen/delivery/DispatcherTickTest.kt b/src/test/kotlin/com/gzzn/omms/msgexchange/delivery/DispatcherTickTest.kt similarity index 94% rename from src/test/kotlin/com/gzzn/omms/msgexchange/nextgen/delivery/DispatcherTickTest.kt rename to src/test/kotlin/com/gzzn/omms/msgexchange/delivery/DispatcherTickTest.kt index 2b91c3e..462bbf0 100644 --- a/src/test/kotlin/com/gzzn/omms/msgexchange/nextgen/delivery/DispatcherTickTest.kt +++ b/src/test/kotlin/com/gzzn/omms/msgexchange/delivery/DispatcherTickTest.kt @@ -1,13 +1,13 @@ -package com.gzzn.omms.msgexchange.nextgen.delivery +package com.gzzn.omms.msgexchange.delivery -import com.gzzn.omms.msgexchange.nextgen.MutableClock -import com.gzzn.omms.msgexchange.nextgen.config.PipelineProps -import com.gzzn.omms.msgexchange.nextgen.domain.ErrorClass -import com.gzzn.omms.msgexchange.nextgen.domain.EventStatus -import com.gzzn.omms.msgexchange.nextgen.domain.MsgEvent -import com.gzzn.omms.msgexchange.nextgen.domain.Targets -import com.gzzn.omms.msgexchange.nextgen.infra.persistence.MsgEventRepository -import com.gzzn.omms.msgexchange.nextgen.infra.retry.FailureScheduler +import com.gzzn.omms.msgexchange.MutableClock +import com.gzzn.omms.msgexchange.config.PipelineProps +import com.gzzn.omms.msgexchange.domain.ErrorClass +import com.gzzn.omms.msgexchange.domain.EventStatus +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 org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Assertions.assertNull diff --git a/src/test/kotlin/com/gzzn/omms/msgexchange/nextgen/delivery/SchdAggregationTest.kt b/src/test/kotlin/com/gzzn/omms/msgexchange/delivery/SchdAggregationTest.kt similarity index 88% rename from src/test/kotlin/com/gzzn/omms/msgexchange/nextgen/delivery/SchdAggregationTest.kt rename to src/test/kotlin/com/gzzn/omms/msgexchange/delivery/SchdAggregationTest.kt index 72cd9fc..715dcaa 100644 --- a/src/test/kotlin/com/gzzn/omms/msgexchange/nextgen/delivery/SchdAggregationTest.kt +++ b/src/test/kotlin/com/gzzn/omms/msgexchange/delivery/SchdAggregationTest.kt @@ -1,8 +1,8 @@ -package com.gzzn.omms.msgexchange.nextgen.delivery +package com.gzzn.omms.msgexchange.delivery -import com.gzzn.omms.msgexchange.nextgen.domain.EventStatus -import com.gzzn.omms.msgexchange.nextgen.domain.MsgEvent -import com.gzzn.omms.msgexchange.nextgen.domain.Targets +import com.gzzn.omms.msgexchange.domain.EventStatus +import com.gzzn.omms.msgexchange.domain.MsgEvent +import com.gzzn.omms.msgexchange.domain.Targets import org.junit.jupiter.api.Test import kotlin.test.assertEquals diff --git a/src/test/kotlin/com/gzzn/omms/msgexchange/nextgen/infra/health/HealthIndicatorsTest.kt b/src/test/kotlin/com/gzzn/omms/msgexchange/infra/health/HealthIndicatorsTest.kt similarity index 90% rename from src/test/kotlin/com/gzzn/omms/msgexchange/nextgen/infra/health/HealthIndicatorsTest.kt rename to src/test/kotlin/com/gzzn/omms/msgexchange/infra/health/HealthIndicatorsTest.kt index 9b0a1bd..0967f40 100644 --- a/src/test/kotlin/com/gzzn/omms/msgexchange/nextgen/infra/health/HealthIndicatorsTest.kt +++ b/src/test/kotlin/com/gzzn/omms/msgexchange/infra/health/HealthIndicatorsTest.kt @@ -1,8 +1,8 @@ -package com.gzzn.omms.msgexchange.nextgen.infra.health +package com.gzzn.omms.msgexchange.infra.health -import com.gzzn.omms.msgexchange.nextgen.delivery.DeliveryPort -import com.gzzn.omms.msgexchange.nextgen.infra.redis.FlightRedisClient -import com.gzzn.omms.msgexchange.nextgen.infra.redis.RedisScript +import com.gzzn.omms.msgexchange.delivery.DeliveryPort +import com.gzzn.omms.msgexchange.infra.redis.FlightRedisClient +import com.gzzn.omms.msgexchange.infra.redis.RedisScript import io.micronaut.health.HealthStatus import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test diff --git a/src/test/kotlin/com/gzzn/omms/msgexchange/nextgen/infra/retry/ReplayServiceTest.kt b/src/test/kotlin/com/gzzn/omms/msgexchange/infra/retry/ReplayServiceTest.kt similarity index 91% rename from src/test/kotlin/com/gzzn/omms/msgexchange/nextgen/infra/retry/ReplayServiceTest.kt rename to src/test/kotlin/com/gzzn/omms/msgexchange/infra/retry/ReplayServiceTest.kt index 383e9a6..1736c0f 100644 --- a/src/test/kotlin/com/gzzn/omms/msgexchange/nextgen/infra/retry/ReplayServiceTest.kt +++ b/src/test/kotlin/com/gzzn/omms/msgexchange/infra/retry/ReplayServiceTest.kt @@ -1,9 +1,9 @@ -package com.gzzn.omms.msgexchange.nextgen.infra.retry +package com.gzzn.omms.msgexchange.infra.retry -import com.gzzn.omms.msgexchange.nextgen.domain.ErrorClass -import com.gzzn.omms.msgexchange.nextgen.domain.ProcState -import com.gzzn.omms.msgexchange.nextgen.domain.ProcStatus -import com.gzzn.omms.msgexchange.nextgen.infra.persistence.ProcStateRepository +import com.gzzn.omms.msgexchange.domain.ErrorClass +import com.gzzn.omms.msgexchange.domain.ProcState +import com.gzzn.omms.msgexchange.domain.ProcStatus +import com.gzzn.omms.msgexchange.infra.persistence.ProcStateRepository import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Test @@ -24,6 +24,7 @@ class ReplayServiceTest { } override fun insert(cminmsgsId: Long, state: ProcStatus) = Unit + override fun exists(cminmsgsId: Long): Boolean = rows.containsKey(cminmsgsId) override fun headUnfinished(): ProcState? = null override fun tryBindIdentity(cminmsgsId: Long, identityKey: String): Boolean = true override fun ownerOfIdentity(identityKey: String): Long? = null diff --git a/src/test/kotlin/com/gzzn/omms/msgexchange/ingress/InboxPollerTest.kt b/src/test/kotlin/com/gzzn/omms/msgexchange/ingress/InboxPollerTest.kt new file mode 100644 index 0000000..4d858df --- /dev/null +++ b/src/test/kotlin/com/gzzn/omms/msgexchange/ingress/InboxPollerTest.kt @@ -0,0 +1,45 @@ +package com.gzzn.omms.msgexchange.ingress + +import com.gzzn.omms.msgexchange.domain.ProcStatus +import com.gzzn.omms.msgexchange.infra.stub.StubInbox +import com.gzzn.omms.msgexchange.infra.stub.StubProcState +import com.gzzn.omms.msgexchange.processing.Pump +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.BeforeEach +import org.junit.jupiter.api.Test + +/** 主路径:JDBC 轮询语义(stub 下 InboxPoller + 外部写信箱模拟)。 */ +@MicronautTest +class InboxPollerTest { + + @Inject lateinit var poller: InboxPoller + @Inject lateinit var pump: Pump + @Inject lateinit var stubInbox: StubInbox + @Inject lateinit var stubProc: StubProcState + + @BeforeEach + fun clean() { + stubInbox.clear() + stubProc.clear() + } + + @Test + fun `poller enqueues externally written mailbox rows`() { + val id = stubInbox.simulateExternalWrite("") + assertEquals(1, poller.pollOnce()) + assertNotNull(stubProc.snapshotOf(id)) + assertEquals(ProcStatus.PENDING, stubProc.snapshotOf(id)!!.state) + } + + @Test + fun `poller is idempotent for already enqueued rows`() { + val id = stubInbox.simulateExternalWrite("") + poller.pollOnce() + assertEquals(0, poller.pollOnce()) + pump.tick() + assertEquals(ProcStatus.FAILED, stubProc.snapshotOf(id)!!.state) + } +} diff --git a/src/test/kotlin/com/gzzn/omms/msgexchange/nextgen/processing/IdentityTest.kt b/src/test/kotlin/com/gzzn/omms/msgexchange/processing/IdentityTest.kt similarity index 80% rename from src/test/kotlin/com/gzzn/omms/msgexchange/nextgen/processing/IdentityTest.kt rename to src/test/kotlin/com/gzzn/omms/msgexchange/processing/IdentityTest.kt index fe060a6..5474c1e 100644 --- a/src/test/kotlin/com/gzzn/omms/msgexchange/nextgen/processing/IdentityTest.kt +++ b/src/test/kotlin/com/gzzn/omms/msgexchange/processing/IdentityTest.kt @@ -1,9 +1,9 @@ -package com.gzzn.omms.msgexchange.nextgen.processing +package com.gzzn.omms.msgexchange.processing -import com.gzzn.omms.msgexchange.nextgen.config.PipelineProps -import com.gzzn.omms.msgexchange.nextgen.domain.DecodedMessage -import com.gzzn.omms.msgexchange.nextgen.domain.MetaFields -import com.gzzn.omms.msgexchange.nextgen.domain.MsgKind +import com.gzzn.omms.msgexchange.config.PipelineProps +import com.gzzn.omms.msgexchange.domain.DecodedMessage +import com.gzzn.omms.msgexchange.domain.MetaFields +import com.gzzn.omms.msgexchange.domain.MsgKind import org.junit.jupiter.api.Test import java.time.LocalDate import kotlin.test.assertEquals diff --git a/src/test/kotlin/com/gzzn/omms/msgexchange/nextgen/processing/MessageProcessorTest.kt b/src/test/kotlin/com/gzzn/omms/msgexchange/processing/MessageProcessorTest.kt similarity index 87% rename from src/test/kotlin/com/gzzn/omms/msgexchange/nextgen/processing/MessageProcessorTest.kt rename to src/test/kotlin/com/gzzn/omms/msgexchange/processing/MessageProcessorTest.kt index e73cc49..8824343 100644 --- a/src/test/kotlin/com/gzzn/omms/msgexchange/nextgen/processing/MessageProcessorTest.kt +++ b/src/test/kotlin/com/gzzn/omms/msgexchange/processing/MessageProcessorTest.kt @@ -1,33 +1,33 @@ -package com.gzzn.omms.msgexchange.nextgen.processing +package com.gzzn.omms.msgexchange.processing -import com.gzzn.omms.msgexchange.nextgen.MutableClock -import com.gzzn.omms.msgexchange.nextgen.codec.DecodeFailure -import com.gzzn.omms.msgexchange.nextgen.codec.DecodeResult -import com.gzzn.omms.msgexchange.nextgen.codec.XmlCodec -import com.gzzn.omms.msgexchange.nextgen.config.PipelineProps -import com.gzzn.omms.msgexchange.nextgen.domain.DecodedMessage -import com.gzzn.omms.msgexchange.nextgen.domain.Decision -import com.gzzn.omms.msgexchange.nextgen.domain.ErrorClass -import com.gzzn.omms.msgexchange.nextgen.domain.EventStatus -import com.gzzn.omms.msgexchange.nextgen.domain.MsgEvent -import com.gzzn.omms.msgexchange.nextgen.domain.MsgKind -import com.gzzn.omms.msgexchange.nextgen.domain.MetaFields -import com.gzzn.omms.msgexchange.nextgen.domain.NotifyPayload -import com.gzzn.omms.msgexchange.nextgen.domain.ProcState -import com.gzzn.omms.msgexchange.nextgen.domain.ProcStatus -import com.gzzn.omms.msgexchange.nextgen.domain.SchdPush -import com.gzzn.omms.msgexchange.nextgen.domain.Targets -import com.gzzn.omms.msgexchange.nextgen.infra.persistence.CminmsgInboxRepository -import com.gzzn.omms.msgexchange.nextgen.infra.persistence.MsgEventRepository -import com.gzzn.omms.msgexchange.nextgen.infra.persistence.ProcStateRepository -import com.gzzn.omms.msgexchange.nextgen.infra.persistence.PumpJobRepository -import com.gzzn.omms.msgexchange.nextgen.infra.persistence.RefDataRepository -import com.gzzn.omms.msgexchange.nextgen.infra.persistence.ReqTrackRepository -import com.gzzn.omms.msgexchange.nextgen.infra.persistence.FlightStateRepository -import com.gzzn.omms.msgexchange.nextgen.infra.redis.FlightRedisClient -import com.gzzn.omms.msgexchange.nextgen.infra.redis.RedisScript -import com.gzzn.omms.msgexchange.nextgen.infra.retry.FailureScheduler -import com.gzzn.omms.msgexchange.nextgen.infra.retry.ProcFailure +import com.gzzn.omms.msgexchange.MutableClock +import com.gzzn.omms.msgexchange.codec.DecodeFailure +import com.gzzn.omms.msgexchange.codec.DecodeResult +import com.gzzn.omms.msgexchange.codec.XmlCodec +import com.gzzn.omms.msgexchange.config.PipelineProps +import com.gzzn.omms.msgexchange.domain.DecodedMessage +import com.gzzn.omms.msgexchange.domain.Decision +import com.gzzn.omms.msgexchange.domain.ErrorClass +import com.gzzn.omms.msgexchange.domain.EventStatus +import com.gzzn.omms.msgexchange.domain.MsgEvent +import com.gzzn.omms.msgexchange.domain.MsgKind +import com.gzzn.omms.msgexchange.domain.MetaFields +import com.gzzn.omms.msgexchange.domain.NotifyPayload +import com.gzzn.omms.msgexchange.domain.ProcState +import com.gzzn.omms.msgexchange.domain.ProcStatus +import com.gzzn.omms.msgexchange.domain.SchdPush +import com.gzzn.omms.msgexchange.domain.Targets +import com.gzzn.omms.msgexchange.infra.persistence.CminmsgInboxRepository +import com.gzzn.omms.msgexchange.infra.persistence.MsgEventRepository +import com.gzzn.omms.msgexchange.infra.persistence.ProcStateRepository +import com.gzzn.omms.msgexchange.infra.persistence.PumpJobRepository +import com.gzzn.omms.msgexchange.infra.persistence.RefDataRepository +import com.gzzn.omms.msgexchange.infra.persistence.ReqTrackRepository +import com.gzzn.omms.msgexchange.infra.persistence.FlightStateRepository +import com.gzzn.omms.msgexchange.infra.redis.FlightRedisClient +import com.gzzn.omms.msgexchange.infra.redis.RedisScript +import com.gzzn.omms.msgexchange.infra.retry.FailureScheduler +import com.gzzn.omms.msgexchange.infra.retry.ProcFailure import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Assertions.assertNull @@ -50,6 +50,8 @@ class MessageProcessorTest { override fun insert(cminmsgsId: Long, state: ProcStatus) { record[cminmsgsId] = ProcState(cminmsgsId, state) } + override fun exists(cminmsgsId: Long): Boolean = record.containsKey(cminmsgsId) + override fun headUnfinished(): ProcState? = null override fun tryBindIdentity(cminmsgsId: Long, identityKey: String): Boolean { @@ -99,6 +101,8 @@ class MessageProcessorTest { override fun insertRaw(rawXml: String): Long = 0 + override fun pollUnprocessed(afterId: Long, limit: Int): List = emptyList() + override fun rawOf(cminmsgsId: Long): String? { throwOnRawOf?.let { throw it } return raws[cminmsgsId]