feat(processing): 实现自有 PostgreSQL 运营航班权威存储与单事务闭环 (ACM2-28)
- FS1: 增加 Flyway 迁移 V1.1.0__flight_schd.sql,创建 FLIGHT_SCHD 与 SCHD_GEN - FS2: 实现 FlightSchdRepository 接口及 JdbcFlightSchdRepository 与 StubFlightSchd,增强 JdbcOps 事务管理 - FS3: 扩展 MessageProcessor 事务 2 与按 FLID 点查视图,合并变更、事件与终态入单事务提交 - FS4: SnapshotFlow SQL 化(批处理 upsert、域内差删、SQL CAS 推进与熔断保护),JobExecutor 接入 PG 清场删除 - FS5: 彻底退役 Redis 权威与写路径,移除 FlightRedisClient、Lua 脚本、健康指示器与配置残留 - FS6: 补齐 U09/U29 不变量门禁(崩溃幂等、CAS 防并发、ADFT 存活保障、非 UTC JVM/会话时区无漂移)与 FlywayMigrationTest - FS7: 交付影子对拍比较内核 FlightStoreDiffTool 与单元测试 - FS8: 全面回改 decision-flight-state、architecture、design、user-stories 权威文档与规范
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
package com.gzzn.omms.msgexchange.infra.health
|
||||
|
||||
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
|
||||
@@ -11,20 +10,12 @@ import jakarta.inject.Singleton
|
||||
import org.reactivestreams.Publisher
|
||||
|
||||
/**
|
||||
* U12(R05):阶段 A 关键依赖的自定义健康指示器——
|
||||
* Redis(flightInfo 权威存储)与 Kafka(投递端口)。经 BeanProvider 可选解析:
|
||||
* U12(R05):自定义健康指示器——
|
||||
* Kafka(投递端口)。经 BeanProvider 可选解析:
|
||||
* 缺 bean(如未用 stub 也未实装)时指示 DOWN 而非启动失败;
|
||||
* UP 判据为真实 ping(false/异常 → DOWN),而非仅 bean 存在(复审 P1 修正)。
|
||||
* (ACM2-28:Redis 退出阶段 A 权威与写路径,redis-flight-store 指示器移除)。
|
||||
*/
|
||||
@Singleton
|
||||
class FlightRedisHealthIndicator(
|
||||
private val redis: BeanProvider<FlightRedisClient>,
|
||||
) : HealthIndicator {
|
||||
|
||||
override fun getResult(): Publisher<HealthResult> =
|
||||
Publishers.just(redisHealth(if (redis.isPresent) redis.get() else null))
|
||||
}
|
||||
|
||||
@Singleton
|
||||
class KafkaDeliveryHealthIndicator(
|
||||
private val port: BeanProvider<DeliveryPort>,
|
||||
@@ -34,12 +25,6 @@ class KafkaDeliveryHealthIndicator(
|
||||
Publishers.just(kafkaHealth(if (port.isPresent) port.get() else null))
|
||||
}
|
||||
|
||||
/** ping 判定独立成纯函数便于单测:client 为 null = bean 缺失;ping false/异常 = DOWN。 */
|
||||
internal fun redisHealth(client: FlightRedisClient?): HealthResult =
|
||||
healthOf("redis-flight-store", "flight store", client?.let {
|
||||
try { it.ping() } catch (e: Exception) { false }
|
||||
})
|
||||
|
||||
internal fun kafkaHealth(port: DeliveryPort?): HealthResult =
|
||||
healthOf("kafka-delivery", "delivery port", port?.let {
|
||||
try { it.ping() } catch (e: Exception) { false }
|
||||
|
||||
@@ -14,7 +14,7 @@ import java.time.Instant
|
||||
* 的全部接口(消息管道 PROC_STATE/MSG_EVENT、PUMP_JOB、REQ_TRACK、21 类 REF_MASTER);
|
||||
* 共享 MySQL 信箱(CMINMSGS / COUTMSGS)经信箱封装访问,仅 DML、不建表;
|
||||
* 主路径=上游外部写 CMINMSGS → 本系统 JDBC 轮询读;compat=insertRaw HTTP 写;
|
||||
* Redis = 航班动态 + 快照 gen(RefDataRepository 目标实现);FLIGHT_STATE 缓做。
|
||||
* 自有 PG = 消息管道 + 运营航班 FLIGHT_SCHD/SCHD_GEN + 静态数据;Redis 已退出阶段 A 权威与写路径。
|
||||
*/
|
||||
interface ProcStateRepository {
|
||||
fun insert(cminmsgsId: Long, state: ProcStatus = ProcStatus.PENDING)
|
||||
@@ -68,25 +68,88 @@ interface MsgEventRepository {
|
||||
}
|
||||
|
||||
/**
|
||||
* 快照 generation(SCHD_GEN)协议——只留 gen。
|
||||
* ACM2-12:gen 迁 Redis(与 flightInfo 同源,Lua 内原子「覆盖+按代差删+版本推进」,
|
||||
* DB 仅写 SUCCEEDED;重放幂等由 Lua 承接,协议重设计属 U09)。本接口为过渡占位,
|
||||
* 目标实现为 Redis gen store(script 化),非关系表。
|
||||
* 阶段 A 运营航班权威与日计划代(ACM2-28 采纳选项 C 定案):
|
||||
* - 表 FLIGHT_SCHD:当前运营航班全量权威态(SCHD 快照 + FLOP/ADFT 增量合并),落自有 PostgreSQL;
|
||||
* - 表 SCHD_GEN:各日代版本与当前代有效航班全量集合(差删依据),由 Redis 回归自有 PG;
|
||||
* - Redis 退出动态权威与全部写路径;
|
||||
* - 事务 2 与快照发布全在自有 PG 内以单事务原子提交;
|
||||
* - 增量更新(FLOP/ADFT):新插 FDAY=NULL,已有行通过 ON CONFLICT 保留原 FDAY;
|
||||
* - 按代差删域化:DELETE FROM FLIGHT_SCHD WHERE FDAY = :day AND FLID = ANY(:diffSet)
|
||||
* 仅删除仍属旧代的行,ADFT(FDAY=NULL)与已迁移至新代的同 FLID 行天然存活。
|
||||
*/
|
||||
interface RefDataRepository {
|
||||
data class GenMeta(val flids: List<String>, val version: Long)
|
||||
interface FlightSchdRepository {
|
||||
data class FlightRecord(
|
||||
val flid: String,
|
||||
val fday: String?,
|
||||
val fltrJson: String,
|
||||
val createdAt: Instant,
|
||||
val updatedAt: Instant,
|
||||
)
|
||||
|
||||
data class GenMeta(
|
||||
val fday: String,
|
||||
val version: Long,
|
||||
val flids: Set<String>,
|
||||
val updatedAt: Instant = Instant.now(),
|
||||
)
|
||||
|
||||
/** 快照全量写入(DNLD):强行声明/更新 FDAY 归属,批处理写入。 */
|
||||
fun upsertSnapshotBatch(day: String, flights: List<Pair<String, String>>, now: Instant = Instant.now())
|
||||
|
||||
/** 增量更新(FLOP/ADFT):新插 FDAY=NULL,已有行保留原 FDAY。 */
|
||||
fun upsertIncremental(changes: List<com.gzzn.omms.msgexchange.domain.FlightChange>, now: Instant = Instant.now())
|
||||
|
||||
/** 按代差删域化:仅删除 FDAY = day 且在 delFlids 中的记录(ADFT 与跨代已迁移行受保护)。 */
|
||||
fun deleteDiffByDay(day: String, delFlids: Collection<String>): Int
|
||||
|
||||
/** 点查单航班 FLTR_JSON。 */
|
||||
fun findByFlid(flid: String): String?
|
||||
|
||||
/** 点查多航班 FLTR_JSON。 */
|
||||
fun findByFlids(flids: Collection<String>): Map<String, String>
|
||||
|
||||
/** 按计划日查询当前有效航班。 */
|
||||
fun findByDay(day: String): List<Pair<String, String>>
|
||||
|
||||
/** 全量查询(供影子对拍 / 一致性对账)。 */
|
||||
fun findAll(): Map<String, String>
|
||||
|
||||
/**
|
||||
* 历史清场删除:仅删除已确认归档至 ES 的 FLID 集合;空集合不执行;分批参数化删除。
|
||||
* 返回实际删除行数(允许重放时为 0)。
|
||||
*/
|
||||
fun deleteByFlids(flids: Set<String>): Int
|
||||
|
||||
/** 读计划代(SCHD_GEN)。 */
|
||||
fun getGen(day: String): GenMeta?
|
||||
|
||||
/** 流程 4:版本 CAS(expected 未变才写,重放 no-op,不二次自增)。 */
|
||||
fun putGenIfVersion(day: String, expected: Long, new: GenMeta): Boolean
|
||||
/**
|
||||
* 计划代 SQL 版本 CAS:仅当 expected 与数据库中当前版本一致(或不存在且 expected=0)时写入/推进新版本。
|
||||
* 返回 true 表示推进成功,false 表示发生 CAS 冲突。
|
||||
*/
|
||||
fun putGenIfVersion(day: String, expected: Long, newGen: GenMeta, now: Instant = Instant.now()): Boolean
|
||||
|
||||
/**
|
||||
* 历史代清理:清理 cutoffDay 之前的历史代记录(与 FLIGHT_SCHD 历史清场生命周期对齐)。
|
||||
*/
|
||||
fun deleteGenBefore(cutoffDay: String): Int
|
||||
}
|
||||
|
||||
/** 事务管理器抽象:自有 PG 单事务原子保障。 */
|
||||
interface PipelineTransactionManager {
|
||||
fun <T> inTransaction(block: () -> T): T
|
||||
}
|
||||
|
||||
@Deprecated("Replaced by FlightSchdRepository in ACM2-28", ReplaceWith("FlightSchdRepository"))
|
||||
interface RefDataRepository {
|
||||
data class GenMeta(val flids: List<String>, val version: Long)
|
||||
fun getGen(day: String): GenMeta?
|
||||
fun putGenIfVersion(day: String, expected: Long, new: GenMeta): Boolean
|
||||
}
|
||||
/**
|
||||
* 21 类静态主数据(航空公司/航线/机位/登机桥等)——自有 PostgreSQL `REF_MASTER` 表
|
||||
* (ACM2-12:与消息管道同自有库;SOURCE=ADMINAPI/AODB/PIPELINE,N19 对齐)。
|
||||
* 与主链弱事务耦合:写入者为 ReferenceService(21 类同步)与请求应答路径;
|
||||
* Redis 只作只读热点投影(legacy orms_stand 语义延续,阶段 6)。
|
||||
* 与主链弱事务耦合:写入者为 ReferenceService(21 类同步)与请求应答路径。
|
||||
*/
|
||||
interface StaticRefRepository {
|
||||
fun upsertAll(refs: List<RefUpsert>)
|
||||
@@ -124,7 +187,7 @@ interface ReqTrackRepository {
|
||||
/**
|
||||
* 泵作业调度记录——自有 PG `PUMP_JOB`(ACM2-12)。
|
||||
* 决策 1 修订:作业不插队,仅在消息队头空闲/退避窗口由主泵执行(跨库/异队列无全序);
|
||||
* 作业动作本身(归档写共享库 CMINMSGS_HST、清场删 Redis/ES 等)仍在各自目标存储。
|
||||
* 作业动作本身(归档写共享库 CMINMSGS_HST、清场删 ES+PG 等)仍在各自目标存储。
|
||||
*/
|
||||
interface PumpJobRepository {
|
||||
data class Job(val jobId: Long, val kind: String) // ARCHIVE/HISTORY_SWEEP/PROJECTION_REBUILD
|
||||
@@ -141,9 +204,9 @@ interface PumpJobRepository {
|
||||
}
|
||||
|
||||
/**
|
||||
* 阶段 B 权威(ACM2-12:缓做,不落表——航班动态权威保持 Redis;阶段 B 重新
|
||||
* 评估后再定是否引入事务化权威)。本接口仅供占位与测试,勿据此建表。
|
||||
* 历史占位接口(已被 ACM2-28 之 FlightSchdRepository 取代,保留供兼容与过渡)。
|
||||
*/
|
||||
@Deprecated("Replaced by FlightSchdRepository in ACM2-28", ReplaceWith("FlightSchdRepository"))
|
||||
interface FlightStateRepository {
|
||||
/** 阶段 B 权威;replaceDay = 单事务删差集+写新代+版本提升。 */
|
||||
fun replaceDay(day: String, flights: List<Pair<String, String>>)
|
||||
|
||||
@@ -10,9 +10,51 @@ 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 ->
|
||||
private val transactionConnection = ThreadLocal<java.sql.Connection?>()
|
||||
|
||||
internal fun <T> DataSource.withTransaction(block: () -> T): T {
|
||||
val existing = transactionConnection.get()
|
||||
if (existing != null) {
|
||||
return block()
|
||||
}
|
||||
val conn = this.connection
|
||||
val oldAutoCommit = conn.autoCommit
|
||||
conn.autoCommit = false
|
||||
transactionConnection.set(conn)
|
||||
try {
|
||||
val result = block()
|
||||
conn.commit()
|
||||
return result
|
||||
} catch (t: Throwable) {
|
||||
try {
|
||||
conn.rollback()
|
||||
} catch (rbEx: Throwable) {
|
||||
t.addSuppressed(rbEx)
|
||||
}
|
||||
throw t
|
||||
} finally {
|
||||
transactionConnection.remove()
|
||||
try {
|
||||
conn.autoCommit = oldAutoCommit
|
||||
} catch (_: Throwable) {
|
||||
}
|
||||
conn.close()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun DataSource.obtainConnection(): java.sql.Connection =
|
||||
transactionConnection.get() ?: this.connection
|
||||
|
||||
internal fun java.sql.Connection.releaseIfNotInTransaction() {
|
||||
if (transactionConnection.get() !== this) {
|
||||
this.close()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun <T> DataSource.query(sql: String, bind: (java.sql.PreparedStatement) -> Unit, map: (ResultSet) -> T): List<T> {
|
||||
val conn = obtainConnection()
|
||||
try {
|
||||
return conn.prepareStatement(sql).use { ps ->
|
||||
bind(ps)
|
||||
ps.executeQuery().use { rs ->
|
||||
buildList {
|
||||
@@ -20,22 +62,30 @@ internal fun <T> DataSource.query(sql: String, bind: (java.sql.PreparedStatement
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
conn.releaseIfNotInTransaction()
|
||||
}
|
||||
}
|
||||
|
||||
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 ->
|
||||
internal fun DataSource.update(sql: String, bind: (java.sql.PreparedStatement) -> Unit): Int {
|
||||
val conn = obtainConnection()
|
||||
try {
|
||||
return conn.prepareStatement(sql).use { ps ->
|
||||
bind(ps)
|
||||
ps.executeUpdate()
|
||||
}
|
||||
} finally {
|
||||
conn.releaseIfNotInTransaction()
|
||||
}
|
||||
}
|
||||
|
||||
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 ->
|
||||
internal fun DataSource.updateReturningLong(sql: String, bind: (java.sql.PreparedStatement) -> Unit): Long {
|
||||
val conn = obtainConnection()
|
||||
try {
|
||||
return conn.prepareStatement(sql, java.sql.Statement.RETURN_GENERATED_KEYS).use { ps ->
|
||||
bind(ps)
|
||||
ps.executeUpdate()
|
||||
ps.generatedKeys.use { keys ->
|
||||
@@ -43,4 +93,7 @@ internal fun DataSource.updateReturningLong(sql: String, bind: (java.sql.Prepare
|
||||
keys.getLong(1)
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
conn.releaseIfNotInTransaction()
|
||||
}
|
||||
}
|
||||
|
||||
+240
-12
@@ -6,8 +6,10 @@ 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.FlightSchdRepository
|
||||
import com.gzzn.omms.msgexchange.infra.persistence.FlightStateRepository
|
||||
import com.gzzn.omms.msgexchange.infra.persistence.MsgEventRepository
|
||||
import com.gzzn.omms.msgexchange.infra.persistence.PipelineTransactionManager
|
||||
import com.gzzn.omms.msgexchange.infra.persistence.ProcStateRepository
|
||||
import com.gzzn.omms.msgexchange.infra.persistence.PumpJobRepository
|
||||
import com.gzzn.omms.msgexchange.infra.persistence.RefDataRepository
|
||||
@@ -286,21 +288,242 @@ class JdbcPumpJobRepository(
|
||||
}
|
||||
}
|
||||
|
||||
/** gen→Redis 过渡占位:U09 前进程内 CAS(与 StubRefData 同语义)。 */
|
||||
/** 自有 PostgreSQL 单事务管理器(ACM2-28 事务 2 与快照发布原子提交)。 */
|
||||
@Singleton
|
||||
@Requires(property = "msgx.stubs", notEquals = "true")
|
||||
@Requires(property = "datasources.default.enabled", value = "true")
|
||||
class JdbcRefDataRepository : RefDataRepository {
|
||||
private val gens = mutableMapOf<String, RefDataRepository.GenMeta>()
|
||||
class JdbcPipelineTransactionManager(
|
||||
private val ds: DataSource,
|
||||
) : PipelineTransactionManager {
|
||||
override fun <T> inTransaction(block: () -> T): T = ds.withTransaction(block)
|
||||
}
|
||||
|
||||
override fun getGen(day: String): RefDataRepository.GenMeta? = gens[day]
|
||||
@Singleton
|
||||
@Requires(property = "msgx.stubs", notEquals = "true")
|
||||
@Requires(property = "datasources.default.enabled", value = "true")
|
||||
class JdbcFlightSchdRepository(
|
||||
private val ds: DataSource,
|
||||
) : FlightSchdRepository {
|
||||
private val flidsTypeRef = object : com.fasterxml.jackson.core.type.TypeReference<Set<String>>() {}
|
||||
private val mapper = com.fasterxml.jackson.databind.ObjectMapper()
|
||||
|
||||
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
|
||||
private fun serializeFlids(flids: Collection<String>): String = mapper.writeValueAsString(flids)
|
||||
|
||||
private fun parseFlids(json: String): Set<String> {
|
||||
if (json.isBlank()) return emptySet()
|
||||
return try {
|
||||
mapper.readValue(json, flidsTypeRef)
|
||||
} catch (_: Exception) {
|
||||
emptySet()
|
||||
}
|
||||
}
|
||||
|
||||
private fun toSqlDate(day: String): java.sql.Date =
|
||||
java.sql.Date.valueOf(day.trim().take(10))
|
||||
|
||||
override fun upsertSnapshotBatch(day: String, flights: List<Pair<String, String>>, now: Instant) {
|
||||
if (flights.isEmpty()) return
|
||||
val sql = """
|
||||
INSERT INTO flight_schd (flid, fday, fltr_json, created_at, updated_at)
|
||||
VALUES (?, ?, ?::jsonb, ?, ?)
|
||||
ON CONFLICT (flid) DO UPDATE SET
|
||||
fday = EXCLUDED.fday,
|
||||
fltr_json = EXCLUDED.fltr_json,
|
||||
updated_at = EXCLUDED.updated_at
|
||||
""".trimIndent()
|
||||
val sqlDate = toSqlDate(day)
|
||||
val sqlTimestamp = now.toSqlTimestamp()
|
||||
val conn = ds.obtainConnection()
|
||||
try {
|
||||
conn.prepareStatement(sql).use { ps ->
|
||||
var count = 0
|
||||
for ((flid, json) in flights) {
|
||||
ps.setString(1, flid)
|
||||
ps.setDate(2, sqlDate)
|
||||
ps.setString(3, json)
|
||||
ps.setTimestamp(4, sqlTimestamp)
|
||||
ps.setTimestamp(5, sqlTimestamp)
|
||||
ps.addBatch()
|
||||
count++
|
||||
if (count % 200 == 0) {
|
||||
ps.executeBatch()
|
||||
}
|
||||
}
|
||||
if (count % 200 != 0) {
|
||||
ps.executeBatch()
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
conn.releaseIfNotInTransaction()
|
||||
}
|
||||
}
|
||||
|
||||
override fun upsertIncremental(changes: List<com.gzzn.omms.msgexchange.domain.FlightChange>, now: Instant) {
|
||||
if (changes.isEmpty()) return
|
||||
val sql = """
|
||||
INSERT INTO flight_schd (flid, fday, fltr_json, created_at, updated_at)
|
||||
VALUES (?, NULL, ?::jsonb, ?, ?)
|
||||
ON CONFLICT (flid) DO UPDATE SET
|
||||
fltr_json = EXCLUDED.fltr_json,
|
||||
updated_at = EXCLUDED.updated_at
|
||||
""".trimIndent()
|
||||
val sqlTimestamp = now.toSqlTimestamp()
|
||||
val conn = ds.obtainConnection()
|
||||
try {
|
||||
conn.prepareStatement(sql).use { ps ->
|
||||
var count = 0
|
||||
for (c in changes) {
|
||||
ps.setString(1, c.flid)
|
||||
ps.setString(2, c.payloadJson)
|
||||
ps.setTimestamp(3, sqlTimestamp)
|
||||
ps.setTimestamp(4, sqlTimestamp)
|
||||
ps.addBatch()
|
||||
count++
|
||||
if (count % 200 == 0) {
|
||||
ps.executeBatch()
|
||||
}
|
||||
}
|
||||
if (count % 200 != 0) {
|
||||
ps.executeBatch()
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
conn.releaseIfNotInTransaction()
|
||||
}
|
||||
}
|
||||
|
||||
override fun deleteDiffByDay(day: String, delFlids: Collection<String>): Int {
|
||||
if (delFlids.isEmpty()) return 0
|
||||
var totalDeleted = 0
|
||||
val sqlDate = toSqlDate(day)
|
||||
for (chunk in delFlids.chunked(200)) {
|
||||
val placeholders = chunk.joinToString(",") { "?" }
|
||||
val sql = "DELETE FROM flight_schd WHERE fday = ? AND flid IN ($placeholders)"
|
||||
totalDeleted += ds.update(sql) { ps ->
|
||||
ps.setDate(1, sqlDate)
|
||||
chunk.forEachIndexed { i, flid -> ps.setString(i + 2, flid) }
|
||||
}
|
||||
}
|
||||
return totalDeleted
|
||||
}
|
||||
|
||||
override fun findByFlid(flid: String): String? =
|
||||
ds.queryOne(
|
||||
"SELECT fltr_json FROM flight_schd WHERE flid = ?",
|
||||
{ ps -> ps.setString(1, flid) },
|
||||
) { rs -> rs.getString("fltr_json") }
|
||||
|
||||
override fun findByFlids(flids: Collection<String>): Map<String, String> {
|
||||
if (flids.isEmpty()) return emptyMap()
|
||||
val result = mutableMapOf<String, String>()
|
||||
for (chunk in flids.chunked(200)) {
|
||||
val placeholders = chunk.joinToString(",") { "?" }
|
||||
val sql = "SELECT flid, fltr_json FROM flight_schd WHERE flid IN ($placeholders)"
|
||||
val pairs = ds.query(
|
||||
sql,
|
||||
{ ps -> chunk.forEachIndexed { i, flid -> ps.setString(i + 1, flid) } },
|
||||
) { rs -> rs.getString("flid") to rs.getString("fltr_json") }
|
||||
result.putAll(pairs)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
override fun findByDay(day: String): List<Pair<String, String>> =
|
||||
ds.query(
|
||||
"SELECT flid, fltr_json FROM flight_schd WHERE fday = ? ORDER BY flid ASC",
|
||||
{ ps -> ps.setDate(1, toSqlDate(day)) },
|
||||
) { rs -> rs.getString("flid") to rs.getString("fltr_json") }
|
||||
|
||||
override fun findAll(): Map<String, String> =
|
||||
ds.query(
|
||||
"SELECT flid, fltr_json FROM flight_schd ORDER BY flid ASC",
|
||||
{},
|
||||
) { rs -> rs.getString("flid") to rs.getString("fltr_json") }.toMap()
|
||||
|
||||
override fun deleteByFlids(flids: Set<String>): Int {
|
||||
if (flids.isEmpty()) return 0
|
||||
var totalDeleted = 0
|
||||
for (chunk in flids.chunked(200)) {
|
||||
val placeholders = chunk.joinToString(",") { "?" }
|
||||
val sql = "DELETE FROM flight_schd WHERE flid IN ($placeholders)"
|
||||
totalDeleted += ds.update(sql) { ps ->
|
||||
chunk.forEachIndexed { i, flid -> ps.setString(i + 1, flid) }
|
||||
}
|
||||
}
|
||||
return totalDeleted
|
||||
}
|
||||
|
||||
override fun getGen(day: String): FlightSchdRepository.GenMeta? =
|
||||
ds.queryOne(
|
||||
"SELECT fday, version, flids_json, updated_at FROM schd_gen WHERE fday = ?",
|
||||
{ ps -> ps.setDate(1, toSqlDate(day)) },
|
||||
) { rs ->
|
||||
FlightSchdRepository.GenMeta(
|
||||
fday = rs.getDate("fday").toString(),
|
||||
version = rs.getLong("version"),
|
||||
flids = parseFlids(rs.getString("flids_json")),
|
||||
updatedAt = rs.getInstant("updated_at") ?: Instant.now(),
|
||||
)
|
||||
}
|
||||
|
||||
override fun putGenIfVersion(day: String, expected: Long, newGen: FlightSchdRepository.GenMeta, now: Instant): Boolean {
|
||||
val flidsJson = serializeFlids(newGen.flids)
|
||||
val sqlDate = toSqlDate(day)
|
||||
val sqlTimestamp = now.toSqlTimestamp()
|
||||
if (expected == 0L) {
|
||||
val inserted = ds.update(
|
||||
"""
|
||||
INSERT INTO schd_gen (fday, version, flids_json, updated_at)
|
||||
VALUES (?, ?, ?::jsonb, ?)
|
||||
ON CONFLICT (fday) DO NOTHING
|
||||
""".trimIndent(),
|
||||
) { ps ->
|
||||
ps.setDate(1, sqlDate)
|
||||
ps.setLong(2, newGen.version)
|
||||
ps.setString(3, flidsJson)
|
||||
ps.setTimestamp(4, sqlTimestamp)
|
||||
}
|
||||
if (inserted == 1) return true
|
||||
}
|
||||
|
||||
val updated = ds.update(
|
||||
"""
|
||||
UPDATE schd_gen
|
||||
SET version = ?, flids_json = ?::jsonb, updated_at = ?
|
||||
WHERE fday = ? AND version = ?
|
||||
""".trimIndent(),
|
||||
) { ps ->
|
||||
ps.setLong(1, newGen.version)
|
||||
ps.setString(2, flidsJson)
|
||||
ps.setTimestamp(3, sqlTimestamp)
|
||||
ps.setDate(4, sqlDate)
|
||||
ps.setLong(5, expected)
|
||||
}
|
||||
return updated == 1
|
||||
}
|
||||
|
||||
override fun deleteGenBefore(cutoffDay: String): Int =
|
||||
ds.update(
|
||||
"DELETE FROM schd_gen WHERE fday < ?",
|
||||
{ ps -> ps.setDate(1, toSqlDate(cutoffDay)) },
|
||||
)
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Requires(property = "msgx.stubs", notEquals = "true")
|
||||
@Requires(property = "datasources.default.enabled", value = "true")
|
||||
class JdbcRefDataRepository(
|
||||
private val flightSchd: FlightSchdRepository,
|
||||
) : RefDataRepository {
|
||||
override fun getGen(day: String): RefDataRepository.GenMeta? =
|
||||
flightSchd.getGen(day)?.let { RefDataRepository.GenMeta(it.flids.toList(), it.version) }
|
||||
|
||||
override fun putGenIfVersion(day: String, expected: Long, new: RefDataRepository.GenMeta): Boolean =
|
||||
flightSchd.putGenIfVersion(
|
||||
day,
|
||||
expected,
|
||||
FlightSchdRepository.GenMeta(day, new.version, new.flids.toSet()),
|
||||
)
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@@ -420,7 +643,12 @@ class JdbcReqTrackRepository(
|
||||
@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()
|
||||
class JdbcFlightStateRepository(
|
||||
private val flightSchd: FlightSchdRepository,
|
||||
) : FlightStateRepository {
|
||||
override fun replaceDay(day: String, flights: List<Pair<String, String>>) =
|
||||
flightSchd.upsertSnapshotBatch(day, flights)
|
||||
|
||||
override fun findByDay(day: String): List<Pair<String, String>> =
|
||||
flightSchd.findByDay(day)
|
||||
}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
package com.gzzn.omms.msgexchange.infra.redis
|
||||
|
||||
/**
|
||||
* ACMA-8 流程 4 / I4 / I5:Redis Lua 脚本装载与执行入口。
|
||||
* 阶段 A 全部 flightInfo 写均经此处,且仅由主泵线程调用(I5)。
|
||||
*/
|
||||
enum class RedisScript(val classpathLocation: String) {
|
||||
/** 同一 hash 原子“覆盖新代 + 按代差删”(setArg 为 N 对 field/value,delArg 为差集)。 */
|
||||
SNAPSHOT_REPLACE("lua/snapshot_replace.lua"),
|
||||
|
||||
/** 3:30 清场批量删除(仅 ES 归档成功集)。 */
|
||||
BATCH_DELETE("lua/batch_delete.lua"),
|
||||
}
|
||||
|
||||
interface FlightRedisClient {
|
||||
/**
|
||||
* 执行脚本。SNAPSHOT_REPLACE:setPairs 为新代全量 field/value,delFields 为按代差集;
|
||||
* BATCH_DELETE:delFields 为待删 FLID 集。
|
||||
*/
|
||||
fun eval(script: RedisScript, setPairs: List<Pair<String, String>> = emptyList(), delFields: List<String> = emptyList())
|
||||
|
||||
/** 阶段 A 权威读(处理决策 loadState、3:30 清场 findAll)。 */
|
||||
fun hgetAllFlightInfo(): Map<String, String>
|
||||
|
||||
/** 连通性探测(健康检查用;实现必须为快速调用,失败返回 false 而非抛出穿出)。 */
|
||||
fun ping(): Boolean
|
||||
}
|
||||
|
||||
// TODO(阶段1后续): 基于 micronaut-redis-lettuce 的实装(脚本自 classpath 装载并缓存 SHA)。
|
||||
@@ -5,8 +5,6 @@ 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
|
||||
@@ -16,7 +14,7 @@ import io.micronaut.context.annotation.Requires
|
||||
import jakarta.inject.Singleton
|
||||
|
||||
/**
|
||||
* U07:stub 适配层——XmlCodec / Redis 客户端 / DeliveryPort / Handler 装配,仅在 msgx.stubs=true 时生效。
|
||||
* U07:stub 适配层——XmlCodec / DeliveryPort / Handler 装配,仅在 msgx.stubs=true 时生效(Redis 客户端已按 ACM2-28 移除)。
|
||||
* stub codec 未实装 → 报文 decode 返回 CODEC_ERROR(走 FAILED 可重放路径,链路上可观测),
|
||||
* 语义见 ACM2-10 U11:不把“未实装”写成报文非法/终态。
|
||||
*/
|
||||
@@ -29,30 +27,6 @@ class StubXmlCodec : XmlCodec {
|
||||
override fun encodeRqrd(kind: String, rangeJson: String): String = ""
|
||||
}
|
||||
|
||||
@Requires(property = "msgx.stubs", value = "true")
|
||||
@Singleton
|
||||
class StubRedis : FlightRedisClient {
|
||||
val hash = mutableMapOf<String, String>()
|
||||
val evalCalls = mutableListOf<RedisScript>()
|
||||
|
||||
fun clear() { hash.clear(); evalCalls.clear() }
|
||||
|
||||
override fun eval(script: RedisScript, setPairs: List<Pair<String, String>>, delFields: List<String>) {
|
||||
evalCalls += script
|
||||
when (script) {
|
||||
RedisScript.SNAPSHOT_REPLACE -> {
|
||||
hash.putAll(setPairs)
|
||||
delFields.forEach { hash.remove(it) }
|
||||
}
|
||||
RedisScript.BATCH_DELETE -> delFields.forEach { hash.remove(it) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun hgetAllFlightInfo(): Map<String, String> = hash.toMap()
|
||||
|
||||
override fun ping(): Boolean = true
|
||||
}
|
||||
|
||||
@Requires(property = "msgx.stubs", value = "true")
|
||||
@Singleton
|
||||
class StubDeliveryPort : DeliveryPort {
|
||||
@@ -62,7 +36,6 @@ class StubDeliveryPort : DeliveryPort {
|
||||
|
||||
override fun sendKafka(topic: String, payloadJson: String) { sent += topic to payloadJson }
|
||||
override fun indexFlightHts(payloadJson: String) = Unit
|
||||
override fun projectRedis(payloadJson: String) = Unit
|
||||
}
|
||||
|
||||
/** Holder 工厂:CodecHolder/HandlerHolder 由 Micronaut Bean 提供(取代直连构造占位)。 */
|
||||
|
||||
@@ -7,8 +7,10 @@ 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.FlightSchdRepository
|
||||
import com.gzzn.omms.msgexchange.infra.persistence.FlightStateRepository
|
||||
import com.gzzn.omms.msgexchange.infra.persistence.MsgEventRepository
|
||||
import com.gzzn.omms.msgexchange.infra.persistence.PipelineTransactionManager
|
||||
import com.gzzn.omms.msgexchange.infra.persistence.ProcStateRepository
|
||||
import com.gzzn.omms.msgexchange.infra.persistence.PumpJobRepository
|
||||
import com.gzzn.omms.msgexchange.infra.persistence.RefDataRepository
|
||||
@@ -187,22 +189,118 @@ class StubPumpJobs : PumpJobRepository {
|
||||
override fun markFailed(jobId: Long, lastError: String) { rows[jobId]?.state = "FAILED"; rows[jobId]?.lastError = lastError }
|
||||
}
|
||||
|
||||
/** 快照 gen 协议 stub(业务库 REF_DATA 的 SCHD_GEN 行;ACM2-11 拆分后 21 类不在本实现)。 */
|
||||
@Requires(property = "msgx.stubs", value = "true")
|
||||
@Singleton
|
||||
class StubRefData : RefDataRepository {
|
||||
private val gens = mutableMapOf<String, RefDataRepository.GenMeta>()
|
||||
class StubPipelineTransactionManager : PipelineTransactionManager {
|
||||
override fun <T> inTransaction(block: () -> T): T = block()
|
||||
}
|
||||
|
||||
fun clear() { gens.clear() }
|
||||
@Requires(property = "msgx.stubs", value = "true")
|
||||
@Singleton
|
||||
class StubFlightSchd : FlightSchdRepository {
|
||||
data class Record(
|
||||
val flid: String,
|
||||
val fday: String?,
|
||||
val fltrJson: String,
|
||||
val createdAt: Instant,
|
||||
val updatedAt: Instant,
|
||||
)
|
||||
|
||||
override fun getGen(day: String): RefDataRepository.GenMeta? = gens[day]
|
||||
private val records = mutableMapOf<String, Record>()
|
||||
private val gens = mutableMapOf<String, FlightSchdRepository.GenMeta>()
|
||||
|
||||
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
|
||||
fun clear() {
|
||||
records.clear()
|
||||
gens.clear()
|
||||
}
|
||||
|
||||
override fun upsertSnapshotBatch(day: String, flights: List<Pair<String, String>>, now: Instant) {
|
||||
for ((flid, json) in flights) {
|
||||
val existing = records[flid]
|
||||
records[flid] = Record(
|
||||
flid = flid,
|
||||
fday = day,
|
||||
fltrJson = json,
|
||||
createdAt = existing?.createdAt ?: now,
|
||||
updatedAt = now,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun upsertIncremental(changes: List<com.gzzn.omms.msgexchange.domain.FlightChange>, now: Instant) {
|
||||
for (c in changes) {
|
||||
val existing = records[c.flid]
|
||||
records[c.flid] = Record(
|
||||
flid = c.flid,
|
||||
fday = existing?.fday, // preserve existing fday; null if new
|
||||
fltrJson = c.payloadJson,
|
||||
createdAt = existing?.createdAt ?: now,
|
||||
updatedAt = now,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun deleteDiffByDay(day: String, delFlids: Collection<String>): Int {
|
||||
if (delFlids.isEmpty()) return 0
|
||||
val delSet = delFlids.toSet()
|
||||
val toRemove = records.filter { (flid, rec) -> rec.fday == day && flid in delSet }.keys
|
||||
toRemove.forEach { records.remove(it) }
|
||||
return toRemove.size
|
||||
}
|
||||
|
||||
override fun findByFlid(flid: String): String? = records[flid]?.fltrJson
|
||||
|
||||
override fun findByFlids(flids: Collection<String>): Map<String, String> =
|
||||
flids.mapNotNull { flid -> records[flid]?.let { flid to it.fltrJson } }.toMap()
|
||||
|
||||
override fun findByDay(day: String): List<Pair<String, String>> =
|
||||
records.values.filter { it.fday == day }
|
||||
.sortedBy { it.flid }
|
||||
.map { it.flid to it.fltrJson }
|
||||
|
||||
override fun findAll(): Map<String, String> =
|
||||
records.mapValues { it.value.fltrJson }
|
||||
|
||||
override fun deleteByFlids(flids: Set<String>): Int {
|
||||
if (flids.isEmpty()) return 0
|
||||
var count = 0
|
||||
for (flid in flids) {
|
||||
if (records.remove(flid) != null) count++
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
override fun getGen(day: String): FlightSchdRepository.GenMeta? = gens[day]
|
||||
|
||||
override fun putGenIfVersion(day: String, expected: Long, newGen: FlightSchdRepository.GenMeta, now: Instant): Boolean {
|
||||
val current = gens[day]?.version ?: 0L
|
||||
if (current != expected) return false
|
||||
gens[day] = newGen.copy(updatedAt = now)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun deleteGenBefore(cutoffDay: String): Int {
|
||||
val toRemove = gens.keys.filter { it < cutoffDay }
|
||||
toRemove.forEach { gens.remove(it) }
|
||||
return toRemove.size
|
||||
}
|
||||
}
|
||||
|
||||
/** 快照 gen 协议 stub:委托至 StubFlightSchd。 */
|
||||
@Requires(property = "msgx.stubs", value = "true")
|
||||
@Singleton
|
||||
class StubRefData(
|
||||
private val flightSchd: FlightSchdRepository,
|
||||
) : RefDataRepository {
|
||||
override fun getGen(day: String): RefDataRepository.GenMeta? =
|
||||
flightSchd.getGen(day)?.let { RefDataRepository.GenMeta(it.flids.toList(), it.version) }
|
||||
|
||||
override fun putGenIfVersion(day: String, expected: Long, new: RefDataRepository.GenMeta): Boolean =
|
||||
flightSchd.putGenIfVersion(
|
||||
day,
|
||||
expected,
|
||||
FlightSchdRepository.GenMeta(day, new.version, new.flids.toSet()),
|
||||
)
|
||||
}
|
||||
|
||||
/** 21 类静态主数据 stub(独立 PG reference 库;内存实现,source 保留供审计断言)。 */
|
||||
@@ -254,14 +352,12 @@ class StubReqTrack : ReqTrackRepository {
|
||||
|
||||
@Requires(property = "msgx.stubs", value = "true")
|
||||
@Singleton
|
||||
class StubFlightState : FlightStateRepository {
|
||||
private val byDay = mutableMapOf<String, MutableList<Pair<String, String>>>()
|
||||
class StubFlightState(
|
||||
private val flightSchd: FlightSchdRepository,
|
||||
) : FlightStateRepository {
|
||||
override fun replaceDay(day: String, flights: List<Pair<String, String>>) =
|
||||
flightSchd.upsertSnapshotBatch(day, flights)
|
||||
|
||||
fun clear() { byDay.clear() }
|
||||
|
||||
override fun replaceDay(day: String, flights: List<Pair<String, String>>) {
|
||||
byDay[day] = flights.toMutableList()
|
||||
}
|
||||
|
||||
override fun findByDay(day: String): List<Pair<String, String>> = byDay[day] ?: emptyList()
|
||||
override fun findByDay(day: String): List<Pair<String, String>> =
|
||||
flightSchd.findByDay(day)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user