feat(ingress): 实现 JDBC 信箱轮询、入站持久化与对拍测试 (U05)
This commit is contained in:
+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