feat(ingress): 实现 JDBC 信箱轮询、入站持久化与对拍测试 (U05)
This commit is contained in:
@@ -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.
|
||||
+2
-1
@@ -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 {
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.gzzn.omms.msgexchange.nextgen
|
||||
package com.gzzn.omms.msgexchange
|
||||
|
||||
import io.micronaut.runtime.Micronaut
|
||||
|
||||
+9
-6
@@ -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 阻塞,加速退出
|
||||
+3
-3
@@ -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 可一键重放)。
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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
|
||||
+8
-8
@@ -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
|
||||
+3
-3
@@ -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 聚合(纯函数,可单测):
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.gzzn.omms.msgexchange.nextgen.domain
|
||||
package com.gzzn.omms.msgexchange.domain
|
||||
|
||||
/**
|
||||
* ACMA-8 流程 2:Handler 决策(纯函数)产物——状态与报文进,变更与事件出,
|
||||
+1
-1
@@ -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 幂等键来源)。
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.gzzn.omms.msgexchange.nextgen.domain
|
||||
package com.gzzn.omms.msgexchange.domain
|
||||
|
||||
/**
|
||||
* ACMA-8 MSG_EVENT(统一投递事件 / outbox)。
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.gzzn.omms.msgexchange.nextgen.domain
|
||||
package com.gzzn.omms.msgexchange.domain
|
||||
|
||||
/**
|
||||
* ACMA-8 数据模型 / PROC_STATE 状态机(六迁移表)。
|
||||
+3
-3
@@ -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
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.gzzn.omms.msgexchange.nextgen.infra.log
|
||||
package com.gzzn.omms.msgexchange.infra.log
|
||||
|
||||
import org.slf4j.MDC
|
||||
|
||||
+16
-7
@@ -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<Long>
|
||||
|
||||
fun backfillOnSuccess(cminmsgsId: Long, sndr: String, type: String, styp: String, seqn: Long)
|
||||
}
|
||||
+63
@@ -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<Long> =
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 <T> DataSource.query(sql: String, bind: (java.sql.PreparedStatement) -> Unit, map: (ResultSet) -> T): List<T> =
|
||||
connection.use { conn ->
|
||||
conn.prepareStatement(sql).use { ps ->
|
||||
bind(ps)
|
||||
ps.executeQuery().use { rs ->
|
||||
buildList {
|
||||
while (rs.next()) add(map(rs))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun <T> 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+426
@@ -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<ErrorClass>): 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<MsgEvent>): List<Long> =
|
||||
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<MsgEvent> =
|
||||
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<Long>) {
|
||||
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<MsgEvent>) {
|
||||
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<String, RefDataRepository.GenMeta>()
|
||||
|
||||
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<RefUpsert>) {
|
||||
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<RefUpsert> =
|
||||
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<Pair<String, String>>) = Unit
|
||||
override fun findByDay(day: String): List<Pair<String, String>> = emptyList()
|
||||
}
|
||||
+30
@@ -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)
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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 脚本装载与执行入口。
|
||||
+2
-2
@@ -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
|
||||
+5
-5
@@ -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
|
||||
|
||||
/**
|
||||
+3
-3
@@ -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
|
||||
|
||||
/**
|
||||
+11
-11
@@ -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
|
||||
+36
-17
@@ -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<Long, String>()
|
||||
private val processed = mutableSetOf<Long>()
|
||||
|
||||
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<Long> =
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
+8
-8
@@ -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
|
||||
|
||||
/**
|
||||
@@ -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: 主泵唤醒(仅加速)
|
||||
}
|
||||
+4
-4
@@ -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)。
|
||||
+3
-3
@@ -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
|
||||
|
||||
/**
|
||||
+21
-21
@@ -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)
|
||||
+10
-10
@@ -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
|
||||
|
||||
/**
|
||||
+2
-2
@@ -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
|
||||
|
||||
/**
|
||||
+4
-4
@@ -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
|
||||
|
||||
@@ -32,5 +32,5 @@
|
||||
<appender-ref ref="ASYNC_LOGSTASH"/>
|
||||
</root>
|
||||
|
||||
<logger name="com.gzzn.omms.msgexchange.nextgen" level="DEBUG"/>
|
||||
<logger name="com.gzzn.omms.msgexchange" level="DEBUG"/>
|
||||
</configuration>
|
||||
|
||||
+15
-15
@@ -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)
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.gzzn.omms.msgexchange.nextgen
|
||||
package com.gzzn.omms.msgexchange
|
||||
|
||||
import java.time.Clock
|
||||
import java.time.Instant
|
||||
+1
-1
@@ -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
|
||||
+1
-1
@@ -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
|
||||
+1
-1
@@ -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
|
||||
+9
-9
@@ -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
|
||||
+4
-4
@@ -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
|
||||
|
||||
+4
-4
@@ -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
|
||||
+6
-5
@@ -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
|
||||
@@ -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("<MSG/>")
|
||||
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("<MSG/>")
|
||||
poller.pollOnce()
|
||||
assertEquals(0, poller.pollOnce())
|
||||
pump.tick()
|
||||
assertEquals(ProcStatus.FAILED, stubProc.snapshotOf(id)!!.state)
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -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
|
||||
+33
-29
@@ -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<Long> = emptyList()
|
||||
|
||||
override fun rawOf(cminmsgsId: Long): String? {
|
||||
throwOnRawOf?.let { throw it }
|
||||
return raws[cminmsgsId]
|
||||
Reference in New Issue
Block a user