feat(ingress): 实现 JDBC 信箱轮询、入站持久化与对拍测试 (U05)

This commit is contained in:
windyboy
2026-09-07 15:11:33 +08:00
parent dc68f1e1f8
commit c7b4b527ef
51 changed files with 1005 additions and 235 deletions
@@ -1,4 +1,4 @@
package com.gzzn.omms.msgexchange.nextgen
package com.gzzn.omms.msgexchange
import io.micronaut.runtime.Micronaut
@@ -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
/**
* U07T03+N29管道生命周期装配服务启动ServerStartupEvent在各自专用单线程
* msgx-pump / msgx-dispatcher不占用 Netty event loop上拉起 Pump Dispatcher 循环
* U07T03+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 阻塞,加速退出
@@ -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,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
@@ -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
@@ -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,4 +1,4 @@
package com.gzzn.omms.msgexchange.nextgen.domain
package com.gzzn.omms.msgexchange.domain
/**
* ACMA-8 流程 2Handler 决策纯函数产物状态与报文进变更与事件出
@@ -1,4 +1,4 @@
package com.gzzn.omms.msgexchange.nextgen.domain
package com.gzzn.omms.msgexchange.domain
/**
* 解码后的入站报文统一内部模型META 字段实名 SNDR/SEQN/DTTMlegacy META.javaI3 幂等键来源
@@ -1,4 +1,4 @@
package com.gzzn.omms.msgexchange.nextgen.domain
package com.gzzn.omms.msgexchange.domain
/**
* ACMA-8 MSG_EVENT统一投递事件 / outbox
@@ -1,4 +1,4 @@
package com.gzzn.omms.msgexchange.nextgen.domain
package com.gzzn.omms.msgexchange.domain
/**
* ACMA-8 数据模型 / PROC_STATE 状态机六迁移表
@@ -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,4 +1,4 @@
package com.gzzn.omms.msgexchange.nextgen.infra.log
package com.gzzn.omms.msgexchange.infra.log
import org.slf4j.MDC
@@ -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)
}
@@ -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)
}
}
}
@@ -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()
}
@@ -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,4 +1,4 @@
package com.gzzn.omms.msgexchange.nextgen.infra.redis
package com.gzzn.omms.msgexchange.infra.redis
/**
* ACMA-8 流程 4 / I4 / I5Redis Lua 脚本装载与执行入口
@@ -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
@@ -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
/**
@@ -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
/**
@@ -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
@@ -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,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())
}
}
@@ -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: 主泵唤醒(仅加速)
}
@@ -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 流程 2Handler = 纯函数状态与报文进Decision 不碰 Redis/Kafka
@@ -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
/**
@@ -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 -> {
// T06U11):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)
@@ -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
/**
@@ -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
/**
@@ -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