refactor(flight-state): 按 flight-state.md 审计定稿全量重构脚手架与 SQL (ACM2-31)
- SQL 基线 V1__flight_state_baseline.sql 整体取代 V1.0.0–V1.4.0: PIPELINE_LOCK/PROC_STATE/MSG_EVENT/REQ_TRACK/BACKFILL_TODO/FLIGHT_SCHD + 8 张资源明细表 + FLIGHT_ROUTE_POINT + SCHD_SNAP_LOG 留痕层 - 废除 FDAY 日代/SCHD_GEN/名单差删:OPERATION_DAY 不可变(应用层校验 + 条件更新强化 §7.4),STATE 仅 ACTIVE/DELETED,物理清除只在历史归档后 - 处理器化:applyScheduleRecords(§5.1 七步同一事务,重放判定/整包 DEAD(PROTOCOL)/归属冲突不落地)+ FLOP/FDEL/ADFT(tombstone 仅 ACTIVE→DELETED,重复 FDEL 幂等不推进版本) - 投递:KAFKA_SCHD 同 FLID 按最新 STATE_VERSION 合并,被压掉事件关闭, TOMBSTONE 发 null 值消息(键缺失=删除旧值 §7.3) - 回填待办改为业务事务内预登记,消除提交后写待办的崩溃窗口(§7.2/§10) - XML 解码改为 jackson-dataformat-xml 数据类直接映射(SIS 信封强类型, FLTR 开放标签泛型承载) - 历史归档/物理清除顺序不可颠倒:归档确认成功集才物理删除,未接通删 0 条 - 移除 PUMP_JOB 队列/ReferenceService/FlightStoreDiffTool 等旧机制与测试, 新增运营日/引擎/快照/FDEL/归档顺序不变性回归测试
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
package com.gzzn.omms.msgexchange.infra
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.module.kotlin.registerKotlinModule
|
||||
import io.micronaut.context.annotation.Factory
|
||||
import jakarta.inject.Singleton
|
||||
|
||||
/** 统一 ObjectMapper 装配(处理器出站载荷 JSON 序列化共用)。 */
|
||||
@Factory
|
||||
class JacksonFactory {
|
||||
@Singleton
|
||||
fun objectMapper(): ObjectMapper = ObjectMapper().registerKotlinModule()
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package com.gzzn.omms.msgexchange.infra.persistence
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
|
||||
/**
|
||||
* 航班字段集(FLIGHT_SCHD 宽表字段)→ KAFKA_SCHD 事件载荷序列化。
|
||||
* 仅用于 MSG_EVENT 出站载荷(报文线格式,legacy 下游按 JSON 消费),与库内存储无关;
|
||||
* 键按字典序输出保证同状态产出字节级稳定载荷(重放/幂等判据不因遍历序漂移)。
|
||||
*/
|
||||
object FlightFieldsJson {
|
||||
private val mapper = ObjectMapper()
|
||||
|
||||
/** Fields whose legacy wire representation is a JSON array/object rather than a JSON string. */
|
||||
private val STRUCTURED_FIELDS = setOf(
|
||||
"ROUT", "ERUT", "CHDT", "GTDT", "PSDT", "CKDT", "CLDT", "DELY",
|
||||
"CHOT", "ABTM", "SRVT", "VIPF", "MAFL", "FDIV", "FRET", "FLAB",
|
||||
)
|
||||
|
||||
fun toJson(fields: Map<String, String>): String {
|
||||
val node = mapper.createObjectNode()
|
||||
fields.toSortedMap().forEach { (key, value) ->
|
||||
if (key in STRUCTURED_FIELDS) {
|
||||
val parsed = runCatching { mapper.readTree(value) }.getOrNull()
|
||||
if (parsed != null && (parsed.isArray || parsed.isObject || parsed.isNull)) {
|
||||
node.set<com.fasterxml.jackson.databind.JsonNode>(key, parsed)
|
||||
} else {
|
||||
node.put(key, value)
|
||||
}
|
||||
} else {
|
||||
node.put(key, value)
|
||||
}
|
||||
}
|
||||
return node.toString()
|
||||
}
|
||||
}
|
||||
@@ -1,39 +1,55 @@
|
||||
package com.gzzn.omms.msgexchange.infra.persistence
|
||||
|
||||
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.domain.SnapshotLogEntry
|
||||
import com.gzzn.omms.msgexchange.domain.flight.FlightMainRow
|
||||
import com.gzzn.omms.msgexchange.domain.flight.FlightSnapshot
|
||||
import com.gzzn.omms.msgexchange.domain.flight.HistoryCandidate
|
||||
import com.gzzn.omms.msgexchange.domain.flight.HistoryRules
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.time.ZoneId
|
||||
|
||||
/**
|
||||
* ACM2-28 仓储接口(接口驱动,主泵/调度循环可单测;Micronaut Data JDBC 实装属 U05 批次)。
|
||||
* 存储边界:自有 PostgreSQL(datasources.default)= 本文件除 CminmsgInboxRepository 外
|
||||
* 的全部接口(消息管道 PROC_STATE/MSG_EVENT、PUMP_JOB、REQ_TRACK、21 类 REF_MASTER);
|
||||
* 共享 MySQL 信箱(CMINMSGS / COUTMSGS)经信箱封装访问,仅 DML、不建表;
|
||||
* 主路径=上游外部写 CMINMSGS → 本系统 JDBC 轮询读;compat=insertRaw HTTP 写;
|
||||
* 自有库 = 消息管道 + 运营航班 FLIGHT_SCHD/SCHD_GEN + 静态数据;Redis 已退出阶段 A 权威与写路径。
|
||||
*/
|
||||
/** 单航班字段集(field → value,与 legacy flightInfo hash 同构)。 */
|
||||
typealias FlightFields = Map<String, String>
|
||||
// =====================================================================
|
||||
// 仓储契约(docs/flight-state.md §3.2 表职责)。
|
||||
// 决策层写路径约定:所有 FLIGHT_SCHD 及明细写操作必须发生在
|
||||
// 「持有 PIPELINE_LOCK 的同一事务」内(§1:锁只串行化 DB 事务)。
|
||||
// =====================================================================
|
||||
|
||||
/** 自有 PG 单事务原子保障(§1 目标 3:状态、事件、处理终态同事务提交)。 */
|
||||
interface PipelineTransactionManager {
|
||||
fun <T> inTransaction(block: () -> T): T
|
||||
}
|
||||
|
||||
/** 单行锁(§3.2 PIPELINE_LOCK):事务内第一步 SELECT ... FOR UPDATE,串行化状态写事务。 */
|
||||
interface PipelineLockRepository {
|
||||
fun lock()
|
||||
}
|
||||
|
||||
/** PROC_STATE:每消息一行;兼作快照重放判定(§5.1 步骤 2:MSG_ID 已有成功终态 → 重放)。 */
|
||||
interface ProcStateRepository {
|
||||
fun insert(cminmsgsId: Long, state: ProcStatus = ProcStatus.PENDING)
|
||||
fun insert(msgId: Long, state: ProcStatus = ProcStatus.PENDING)
|
||||
|
||||
/** 主路径轮询/compat 入队前判重(PG 已有行则跳过)。 */
|
||||
fun exists(cminmsgsId: Long): Boolean
|
||||
fun exists(msgId: Long): Boolean
|
||||
|
||||
/** I1:严格 FIFO 队头(最小未完成 CMINMSGS_ID)。 */
|
||||
fun find(msgId: Long): ProcState?
|
||||
|
||||
fun findSuccessTerminal(msgId: Long): Boolean
|
||||
|
||||
/** 严格 FIFO 队头(最小未完成 MSG_ID)。 */
|
||||
fun headUnfinished(): ProcState?
|
||||
|
||||
/** I3:identity 首次绑定;返回 false = 另一条消息已持有该键。 */
|
||||
fun tryBindIdentity(cminmsgsId: Long, identityKey: String): Boolean
|
||||
/** identity 首次绑定;返回 false = 另一条消息已持有该键。 */
|
||||
fun tryBindIdentity(msgId: Long, identityKey: String): Boolean
|
||||
|
||||
fun ownerOfIdentity(identityKey: String): Long?
|
||||
|
||||
fun update(
|
||||
cminmsgsId: Long,
|
||||
msgId: Long,
|
||||
state: ProcStatus,
|
||||
nextAttemptAt: Instant? = null,
|
||||
attempts: Int? = null,
|
||||
@@ -41,21 +57,20 @@ interface ProcStateRepository {
|
||||
lastError: String? = null,
|
||||
)
|
||||
|
||||
/**
|
||||
* U11 显式重放入口(ReplayService):仅把给定 errorClass 集合中的行从 FAILED/DEAD 置回 PENDING,
|
||||
* 以便主泵重新领取。实现约定:ATTEMPTS=0、NEXT_ATTEMPT_AT=NULL(立即重试),
|
||||
* ERROR_CLASS/LAST_ERROR 保留作审计。返回受影响行数。
|
||||
*/
|
||||
/** 显式重放入口:仅把给定 errorClass 集合中的行从 FAILED/DEAD 置回 PENDING(ATTEMPTS=0)。 */
|
||||
fun requeueByErrorClasses(errorClasses: List<ErrorClass>): Int
|
||||
}
|
||||
|
||||
/** MSG_EVENT outbox(§7.3)。KAFKA_SCHD 合并同 FLID 未发事件按最新 STATE_VERSION 输出。 */
|
||||
interface MsgEventRepository {
|
||||
fun insertAll(events: List<MsgEvent>): List<Long>
|
||||
|
||||
/** I1 双层同策略:每 target 严格 FIFO 队头。 */
|
||||
fun headUnsent(target: String): MsgEvent?
|
||||
|
||||
fun claimBatch(target: String, limit: Int): List<MsgEvent> // ORDER BY EVENT_ID ASC
|
||||
fun claimBatch(target: String, limit: Int): List<MsgEvent>
|
||||
|
||||
/** 同 FLID 未发 KAFKA_SCHD 事件合并:每 FLID 取最新 STATE_VERSION 一条(§7.3)。 */
|
||||
fun mergePendingSchd(limit: Int): List<MsgEvent>
|
||||
|
||||
fun markSent(eventId: Long)
|
||||
|
||||
@@ -64,108 +79,100 @@ interface MsgEventRepository {
|
||||
fun scheduleRetry(eventId: Long, nextAttemptAt: Instant, attempts: Int)
|
||||
|
||||
fun markDead(eventId: Long, errorClass: ErrorClass, lastError: String, attempts: Int? = null)
|
||||
|
||||
/** 阶段 B(定案 2):Delivery 同线程在 ES 投递成功后同步 enqueue 删除事件。 */
|
||||
fun insertSync(events: List<MsgEvent>)
|
||||
}
|
||||
|
||||
/** 完整态落库结果:DAY_GUARD_VIOLATION = OPERATION_DAY 不可变条件更新未命中(§7.4)。 */
|
||||
enum class PersistOutcome { INSERTED, UPDATED, DAY_GUARD_VIOLATION }
|
||||
|
||||
/**
|
||||
* 阶段 A 运营航班权威与日计划代(ACM2-28 定案 + ACM2-29 全宽表·零子表定案):
|
||||
* - 表 FLIGHT_SCHD:一行一航班的运营航班宽表(FLID 主键 + FDAY 可空所属代 + SCHD.FLTR 标量字段列
|
||||
* + 集合平铺标量列/紧凑 VARCHAR 字串列),零子表、零 CLOB,字段即列、天然可索引可直查,
|
||||
* PG/Oracle 11g 方言一致;读侧视图由平铺列重建集合键,与 legacy flightInfo hash 同构;
|
||||
* - 运营资源消息语义 = 单资源集合级全量快照替换;外层航班增量仍为字段级合并(仅新增/覆盖);
|
||||
* - 表 PIPELINE_LOCK:单写者行级互斥(SELECT ... FOR UPDATE,PG/11g 同构,无 DBMS_LOCK 特权依赖);
|
||||
* - 表 SCHD_GEN:各日代版本(SQL CAS);表 SCHD_GEN_FLID:当前代有效航班 FLID 集合(差删依据);
|
||||
* - Redis 退出动态权威与全部写路径;事务 2 与快照发布全在自有库内单事务原子提交;
|
||||
* - 增量更新(FLOP/ADFT):新插 FDAY=NULL,已有行保留原 FDAY,按字段列更新
|
||||
* (与 legacy flightInfo hash「仅新增/覆盖、不删除缺失字段」同语义);
|
||||
* - 快照(DNLD):声明/更新 FDAY 归属,航班字段整体替换;
|
||||
* - 按代差删域化:DELETE FROM FLIGHT_SCHD WHERE FDAY = :day AND FLID IN (:diffSet)
|
||||
* 仅删除仍属旧代的行,ADFT(FDAY=NULL)与已迁移至新代的同 FLID 行天然存活。
|
||||
* 航班当前态权威(§3.2 FLIGHT_SCHD + 9 张明细表)。
|
||||
* 唯一写路径 = persistFullState:写入前在内存生成完整新状态(引擎产物)再落库,
|
||||
* 明细集合按组先删后插(§3.3)。
|
||||
*/
|
||||
interface FlightSchdRepository {
|
||||
data class FlightRecord(
|
||||
val flid: String,
|
||||
val fday: String?,
|
||||
val fields: FlightFields,
|
||||
val createdAt: Instant,
|
||||
val updatedAt: Instant,
|
||||
)
|
||||
interface FlightStateRepository {
|
||||
/** 主行点查(身份/版本判定;批量用于快照归属校验 §5.3)。 */
|
||||
fun findMainRow(flid: String): FlightMainRow?
|
||||
|
||||
data class GenMeta(
|
||||
val fday: String,
|
||||
val version: Long,
|
||||
val flids: Set<String>,
|
||||
val lastMessageId: String? = null,
|
||||
val updatedAt: Instant = Instant.now(),
|
||||
)
|
||||
fun findMainRows(flids: Collection<String>): Map<String, FlightMainRow>
|
||||
|
||||
/** v2(P4 退场):legacy upsertSnapshotBatch/upsertIncremental 已删除——唯一写路径是 persistNextStates。 */
|
||||
|
||||
/** 按代差删域化:仅删除 FDAY = day 且在 delFlids 中的记录(ADFT 与跨代已迁移行受保护)。 */
|
||||
fun deleteDiffByDay(day: String, delFlids: Collection<String>): Int
|
||||
|
||||
/** 点查单航班字段集;无字段行(含航班不存在)返回 null。 */
|
||||
fun findByFlid(flid: String): FlightFields?
|
||||
|
||||
/** v2:点查单航班 nextState(含 state_version / last_message_id 追踪字段)。 */
|
||||
fun findNextStateByFlid(flid: String): com.gzzn.omms.msgexchange.domain.flight.FlightNextState?
|
||||
|
||||
/** 点查多航班字段集:FLID → 字段集,仅含实际存在的航班。 */
|
||||
fun findByFlids(flids: Collection<String>): Map<String, FlightFields>
|
||||
|
||||
/** 按计划日查询当前有效航班。 */
|
||||
fun findByDay(day: String): List<Pair<String, FlightFields>>
|
||||
|
||||
/** 全量查询(供影子对拍 / 一致性对账)。 */
|
||||
fun findAll(): Map<String, FlightFields>
|
||||
/** 完整当前态:主行 + 全部明细(一致性读边界由调用方事务保证,§9)。 */
|
||||
fun loadFullSnapshot(flid: String): FlightSnapshot?
|
||||
|
||||
/**
|
||||
* 历史清场删除:仅删除已确认归档至 ES 的 FLID 集合;空集合不执行;分批参数化删除。
|
||||
* 返回实际删除行数(允许重放时为 0)。
|
||||
* 完整当前态落库(§3.3):主行 upsert + 全部明细按组先删后插;
|
||||
* STATE_VERSION 以 snapshot.stateVersion 落库。
|
||||
* SCHD(整体替换/清除)、FLOP/ADFT(合并后全量写)共用此唯一写路径。
|
||||
* 条件更新带 `WHERE operation_day IS NULL OR operation_day = :day`(§7.4 不可变强化)。
|
||||
*/
|
||||
fun deleteByFlids(flids: Set<String>): Int
|
||||
|
||||
/** 读计划代(SCHD_GEN)。 */
|
||||
fun getGen(day: String): GenMeta?
|
||||
fun persistFullState(snapshot: FlightSnapshot, msgId: Long, now: Instant): PersistOutcome
|
||||
|
||||
/**
|
||||
* 计划代 SQL 版本 CAS:仅当 expected 与数据库中当前版本一致(或不存在且 expected=0)时写入/推进新版本。
|
||||
* 返回 true 表示推进成功,false 表示发生 CAS 冲突。
|
||||
* FDEL(§6.2):ACTIVE → 置 DELETED、推进版本、明细保留、返回 true(发布删除事件);
|
||||
* 已 DELETED 或不存在 → 返回 false(幂等成功,不推进版本不重复发布)。
|
||||
*/
|
||||
fun putGenIfVersion(day: String, expected: Long, newGen: GenMeta, now: Instant = Instant.now()): Boolean
|
||||
fun markDeleted(flid: String, msgId: Long, now: Instant): Boolean
|
||||
|
||||
/** ADFT 生命周期重激活(§6.3):DELETED → ACTIVE,推进版本;非 DELETED 返回 false。 */
|
||||
fun revive(flid: String, msgId: Long, now: Instant): Boolean
|
||||
|
||||
/** §8.1:按窗口规则选出历史候选(含 DELETED;候选时间按机场时区折算)。 */
|
||||
fun findHistoryCandidates(rules: HistoryRules, zone: ZoneId, now: Instant): List<HistoryCandidate>
|
||||
|
||||
/**
|
||||
* 历史代清理:清理 cutoffDay 之前的历史代记录(与 FLIGHT_SCHD 历史清场生命周期对齐)。
|
||||
* §8.2 步骤 3:物理删除主行与明细(仅历史存储确认成功后调用;
|
||||
* 历史存储未接通时调用方必须传空集合——删 0 条)。
|
||||
*/
|
||||
fun deleteGenBefore(cutoffDay: String): Int
|
||||
fun purgeArchived(flids: Collection<String>): Int
|
||||
|
||||
/**
|
||||
* v2 无损明细:将 nextState 写入宽表 + 明细表,并更新追踪字段。
|
||||
* 必须在 PipelineTransactionManager 事务内调用。
|
||||
*/
|
||||
fun persistNextStates(
|
||||
day: String?,
|
||||
states: List<com.gzzn.omms.msgexchange.domain.flight.FlightNextState>,
|
||||
snapshotReplace: Boolean,
|
||||
now: Instant = Instant.now(),
|
||||
)
|
||||
/** §8.3 观测:OPERATION_DAY 仍为 NULL 的航班数(只增不删,终止规则未定 §10)。 */
|
||||
fun countOperationDayNull(): Int
|
||||
}
|
||||
|
||||
/** 事务管理器抽象:自有 PG 单事务原子保障。 */
|
||||
interface PipelineTransactionManager {
|
||||
fun <T> inTransaction(block: () -> T): T
|
||||
/** §5.5 SCHD_SNAP_LOG:事务外追加留痕,不参与决策;一行 = 一次尝试(重放也记)。 */
|
||||
interface SnapshotLogRepository {
|
||||
fun append(entry: SnapshotLogEntry)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 请求状态机(§7.1):只有 RESP 完成 RQFD 请求,按(运营日、发送方、请求类型)
|
||||
* 匹配最新一条 PENDING;同类请求只留一条有效,新请求置旧为 EXPIRED。
|
||||
*/
|
||||
interface ReqTrackRepository {
|
||||
enum class ReqState { PENDING, SENT, DONE, EXPIRED }
|
||||
|
||||
data class Req(
|
||||
val reqId: Long,
|
||||
val reqType: String,
|
||||
val operationDay: LocalDate,
|
||||
val sender: String,
|
||||
val state: ReqState,
|
||||
val coutmsgsId: Long? = null,
|
||||
val sentAt: Instant? = null,
|
||||
)
|
||||
|
||||
/** 登记新请求:同类(类型+运营日+发送方)旧有效请求先置 EXPIRED。 */
|
||||
fun insert(reqType: String, operationDay: LocalDate, sender: String): Long
|
||||
|
||||
fun findLatest(reqType: String, operationDay: LocalDate, sender: String, states: List<ReqState>): Req?
|
||||
|
||||
/** RESP 完成请求(§7.1):匹配最新一条 PENDING/SENT;无匹配返回 false(迟到不报错)。 */
|
||||
fun completeLatest(reqType: String, operationDay: LocalDate, sender: String): Boolean
|
||||
|
||||
fun linkCoutmsgs(reqId: Long, coutmsgsId: Long)
|
||||
|
||||
fun markSent(reqId: Long, sentAt: Instant)
|
||||
|
||||
fun expire(reqId: Long)
|
||||
}
|
||||
|
||||
/**
|
||||
* v2 §5(ACM2-29 P2-3):提交后共享信箱回填的持久补偿待办。
|
||||
* 业务事务已提交(PROC_STATE=SUCCEEDED)后 backfillOnSuccess 失败 → 落本表重试;
|
||||
* 回填失败不得把已成功的业务事务重新标记为失败,也不得重放业务变更。
|
||||
* 共享信箱回填补偿待办(§7.2):业务事务内预登记(消除崩溃窗口 §10 偏差),
|
||||
* 提交后由 BackfillSweepJob 重试;回填失败不得把 SUCCEEDED 改回 FAILED。
|
||||
*/
|
||||
interface BackfillTodoRepository {
|
||||
data class BackfillTask(
|
||||
val cminmsgsId: Long,
|
||||
val msgId: Long,
|
||||
val sndr: String,
|
||||
val type: String,
|
||||
val styp: String,
|
||||
@@ -176,104 +183,25 @@ interface BackfillTodoRepository {
|
||||
/** 失败即落库(幂等 upsert,同 ID 重复失败只刷新错误与重试时间)。 */
|
||||
fun record(task: BackfillTask, lastError: String?, now: Instant = Instant.now())
|
||||
|
||||
/** 到期待重试的补偿任务(next_attempt_at <= now,按到期时间升序)。 */
|
||||
fun findDue(now: Instant = Instant.now(), limit: Int = 50): List<BackfillTask>
|
||||
|
||||
/** 重试失败:累加 attempts 并按退避推后 next_attempt_at。 */
|
||||
fun markFailed(cminmsgsId: Long, lastError: String?, nextAttemptAt: Instant, now: Instant = Instant.now())
|
||||
fun markFailed(msgId: Long, lastError: String?, nextAttemptAt: Instant, now: Instant = Instant.now())
|
||||
|
||||
/** 重试成功:移除补偿待办。 */
|
||||
fun delete(cminmsgsId: Long)
|
||||
fun delete(msgId: Long)
|
||||
|
||||
/** 当前待办数量(测试/对账用)。 */
|
||||
fun count(): Int
|
||||
}
|
||||
|
||||
@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 类同步)与请求应答路径。
|
||||
*/
|
||||
interface StaticRefRepository {
|
||||
fun upsertAll(refs: List<RefUpsert>)
|
||||
|
||||
fun findByType(type: String): List<RefUpsert>
|
||||
}
|
||||
|
||||
/**
|
||||
* 15 类请求状态机——自有 PG `REQ_TRACK`(ACM2-12;Reference & Query 域)。
|
||||
* 与共享库 COUTMSGS(出站信箱)跨库:先 COUTMSGS 落库成功 → 再 markSent,补偿重扫,最终一致。
|
||||
*/
|
||||
interface ReqTrackRepository {
|
||||
data class Req(
|
||||
val reqId: Long,
|
||||
val reqType: String,
|
||||
val state: String, // REGISTERED/SENT/WAITING/DONE/EXPIRED
|
||||
val sentAt: Instant? = null,
|
||||
)
|
||||
|
||||
fun findOpenByKind(kind: String): Req?
|
||||
|
||||
fun forceExpireOpenOf(kind: String)
|
||||
|
||||
fun insert(kind: String, paramsJson: String): Long
|
||||
|
||||
fun linkCoutmsgs(reqId: Long, coutmsgsId: Long)
|
||||
|
||||
fun markSent(reqId: Long, sentAt: Instant)
|
||||
|
||||
fun expireIfWaiting(reqId: Long)
|
||||
|
||||
fun markDone(reqId: Long)
|
||||
}
|
||||
|
||||
/**
|
||||
* 泵作业调度记录——自有 PG `PUMP_JOB`(ACM2-12)。
|
||||
* 决策 1 修订:作业不插队,仅在消息队头空闲/退避窗口由主泵执行(跨库/异队列无全序);
|
||||
* 作业动作本身(归档写共享库 CMINMSGS_HST、清场删 ES+PG 等)仍在各自目标存储。
|
||||
*/
|
||||
interface PumpJobRepository {
|
||||
data class Job(val jobId: Long, val kind: String) // ARCHIVE/HISTORY_SWEEP/PROJECTION_REBUILD
|
||||
|
||||
fun enqueue(kind: String)
|
||||
|
||||
fun headQueued(): Job?
|
||||
|
||||
fun markRunning(jobId: Long)
|
||||
|
||||
fun markDone(jobId: Long)
|
||||
|
||||
fun markFailed(jobId: Long, lastError: String)
|
||||
}
|
||||
|
||||
/**
|
||||
* 共享信箱 CMINMSGS 访问(ACM2-12:库属他人系统,本系统不建表)。
|
||||
*
|
||||
* **主路径(生产)**:`pollNew` / `rawOf` — JDBC 轮询/读取上游外部写入的报文(U05 InboxPoller)。
|
||||
* **compat 路径**:`insertRaw` — HTTP `POST /cminmsgs/send` 辅助写(手工/对拍,非主拓扑)。
|
||||
*
|
||||
* 入队模型:发现或 compat 写得到 CMINMSGS_ID → 自有 PG 建 PROC_STATE(PENDING);
|
||||
* PG 建行失败以共享库 DATE_PROCESSED IS NULL 重扫补建。
|
||||
* `backfillOnSuccess` = 处理成功后的外部回填(DATE_PROCESSED/STATUS,最终一致)。
|
||||
* 共享 MySQL 信箱 CMINMSGS 访问(他人系统库,本系统不建表)。
|
||||
* 主路径 JDBC 轮询读 + 处理回填;compat HTTP 写;出站写 COUTMSGS 由出站适配层承担。
|
||||
*/
|
||||
interface CminmsgInboxRepository {
|
||||
/** compat HTTP 写路径:向共享信箱插入原文(U16 契约对拍)。 */
|
||||
fun insertRaw(rawXml: String): Long
|
||||
|
||||
/** 主路径/处理:按 CMINMSGS_ID 读取上游已落信的原文 XML。 */
|
||||
fun rawOf(cminmsgsId: Long): String?
|
||||
fun rawOf(msgId: 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)
|
||||
fun backfillOnSuccess(msgId: Long, sndr: String, type: String, styp: String, seqn: Long)
|
||||
}
|
||||
|
||||
-162
@@ -1,162 +0,0 @@
|
||||
package com.gzzn.omms.msgexchange.infra.persistence.jdbc
|
||||
|
||||
import com.gzzn.omms.msgexchange.domain.flight.FlightNextState
|
||||
import java.time.Instant
|
||||
import javax.sql.DataSource
|
||||
|
||||
/**
|
||||
* v2 明细表读写(docs/flight-state.md §3.2)。
|
||||
* 主表保存标量,明细表保存重复集合;不存在旧槽位读写回退。
|
||||
*/
|
||||
internal object FlightDetailTables {
|
||||
private data class TableSpec(
|
||||
val table: String,
|
||||
val collectionKey: String,
|
||||
val seqAttr: String,
|
||||
val columns: List<Pair<String, String>>, // json attr -> db column
|
||||
)
|
||||
|
||||
private val SPECS = listOf(
|
||||
TableSpec("flight_gate", "GTDT", "GTNO", listOf(
|
||||
"GATE" to "gate", "PGOT" to "pgot", "PGCT" to "pgct", "GOTM" to "gotm", "GCTM" to "gctm", "GTYP" to "gtyp",
|
||||
)),
|
||||
TableSpec("flight_checkin", "CKDT", "CKNO", listOf(
|
||||
"CHKC" to "chkc", "CCLS" to "ccls", "PCOT" to "pcot", "PCCT" to "pcct", "COTM" to "cotm", "CCTM" to "cctm", "CTYP" to "ctyp",
|
||||
)),
|
||||
TableSpec("flight_belt", "CLDT", "CLNO", listOf(
|
||||
"BELT" to "belt", "BCLS" to "bcls", "PCOT" to "pcot", "PCCT" to "pcct", "FBAG" to "fbag", "LBAG" to "lbag", "BTYP" to "btyp",
|
||||
)),
|
||||
TableSpec("flight_stand_plan", "PSDT", "PSNO", listOf(
|
||||
"PSST" to "psst", "STST" to "stst", "STET" to "stet",
|
||||
)),
|
||||
TableSpec("flight_chute", "CHDT", "CHNO", listOf(
|
||||
"CHUT" to "chut", "CHCLS" to "chcls", "PCBT" to "pcbt", "PCET" to "pcet", "CBTM" to "cbtm", "CETM" to "cetm", "CHTYP" to "chtyp",
|
||||
)),
|
||||
TableSpec("flight_delay", "DELY", "DLNO", listOf(
|
||||
"CODE" to "code", "STRT" to "strt", "DURA" to "dura", "REMC" to "remc",
|
||||
)),
|
||||
TableSpec("flight_bridge_op", "ABTM", "ASNO", listOf(
|
||||
"ABDG" to "abdg", "ABOP" to "abop", "AOTM" to "aotm",
|
||||
)),
|
||||
TableSpec("flight_chock_op", "CHOT", "CSNO", listOf(
|
||||
"CHID" to "chid", "CHST" to "chst", "CHTM" to "chtm",
|
||||
)),
|
||||
)
|
||||
|
||||
fun replaceAll(ds: DataSource, state: FlightNextState, now: Instant) {
|
||||
val ts = now.toSqlTimestamp()
|
||||
val conn = ds.obtainConnection()
|
||||
try {
|
||||
for (spec in SPECS) {
|
||||
ds.update("DELETE FROM ${spec.table} WHERE flid = ?") { ps -> ps.setString(1, state.flid) }
|
||||
val items = state.collections[spec.collectionKey] ?: continue
|
||||
if (items.isEmpty()) continue
|
||||
val colNames = listOf("flid", "ordinal", "source_seq", "record_version") +
|
||||
spec.columns.map { it.second } + listOf("created_at", "updated_at")
|
||||
val sql = "INSERT INTO ${spec.table} (${colNames.joinToString(", ")}) VALUES (${
|
||||
colNames.joinToString(", ") { "?" }
|
||||
})"
|
||||
conn.prepareStatement(sql).use { ps ->
|
||||
items.forEachIndexed { index, item ->
|
||||
var i = 1
|
||||
ps.setString(i++, state.flid)
|
||||
ps.setInt(i++, index + 1)
|
||||
ps.setString(i++, item[spec.seqAttr])
|
||||
ps.setLong(i++, state.stateVersion)
|
||||
spec.columns.forEach { (attr, _) -> ps.setString(i++, item[attr]) }
|
||||
ps.setTimestamp(i++, ts)
|
||||
ps.setTimestamp(i++, ts)
|
||||
ps.addBatch()
|
||||
}
|
||||
ps.executeBatch()
|
||||
}
|
||||
}
|
||||
replaceRoutes(ds, state, now)
|
||||
} finally {
|
||||
conn.releaseIfNotInTransaction()
|
||||
}
|
||||
}
|
||||
|
||||
private fun replaceRoutes(ds: DataSource, state: FlightNextState, now: Instant) {
|
||||
val ts = now.toSqlTimestamp()
|
||||
ds.update("DELETE FROM flight_route_point WHERE flid = ?") { ps -> ps.setString(1, state.flid) }
|
||||
listOf("ROUT" to "ROUT", "ERUT" to "ERUT").forEach { (key, kind) ->
|
||||
val items = state.collections[key] ?: return@forEach
|
||||
if (items.isEmpty()) return@forEach
|
||||
val conn = ds.obtainConnection()
|
||||
try {
|
||||
conn.prepareStatement(
|
||||
"INSERT INTO flight_route_point (flid, ordinal, source_seq, record_version, route_kind, apcd, scat, scdt, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
).use { ps ->
|
||||
items.forEachIndexed { index, item ->
|
||||
ps.setString(1, state.flid)
|
||||
ps.setInt(2, index + 1)
|
||||
ps.setString(3, item["RTNO"])
|
||||
ps.setLong(4, state.stateVersion)
|
||||
ps.setString(5, kind)
|
||||
ps.setString(6, item["APCD"])
|
||||
ps.setString(7, item["SCAT"])
|
||||
ps.setString(8, item["SCDT"])
|
||||
ps.setTimestamp(9, ts)
|
||||
ps.setTimestamp(10, ts)
|
||||
ps.addBatch()
|
||||
}
|
||||
ps.executeBatch()
|
||||
}
|
||||
} finally {
|
||||
conn.releaseIfNotInTransaction()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteForFlids(ds: DataSource, flids: Collection<String>) {
|
||||
if (flids.isEmpty()) return
|
||||
val tables = SPECS.map { it.table } + "flight_route_point"
|
||||
for (table in tables) {
|
||||
for (chunk in flids.chunked(200)) {
|
||||
val placeholders = chunk.joinToString(",") { "?" }
|
||||
ds.update("DELETE FROM $table WHERE flid IN ($placeholders)") { ps ->
|
||||
chunk.forEachIndexed { i, flid -> ps.setString(i + 1, flid) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loadCollections(ds: DataSource, flid: String): Map<String, String> {
|
||||
val out = linkedMapOf<String, String>()
|
||||
val mapper = com.fasterxml.jackson.databind.ObjectMapper()
|
||||
for (spec in SPECS) {
|
||||
val rows = ds.query(
|
||||
"SELECT * FROM ${spec.table} WHERE flid = ? ORDER BY ordinal ASC",
|
||||
{ ps -> ps.setString(1, flid) },
|
||||
) { rs ->
|
||||
// 只输出非空属性(缺失键语义),保证 write→read 线格式一致(评审 F5)
|
||||
spec.columns.associate { (attr, col) -> attr to rs.getString(col) }
|
||||
.filterValues { it != null }
|
||||
.toMutableMap()
|
||||
.apply { rs.getString("source_seq")?.let { put(spec.seqAttr, it) } }
|
||||
}
|
||||
if (rows.isNotEmpty()) {
|
||||
out[spec.collectionKey] = mapper.writeValueAsString(rows)
|
||||
}
|
||||
}
|
||||
listOf("ROUT" to "ROUT", "ERUT" to "ERUT").forEach { (key, kind) ->
|
||||
val rows = ds.query(
|
||||
"SELECT source_seq, apcd, scat, scdt FROM flight_route_point WHERE flid = ? AND route_kind = ? ORDER BY ordinal ASC",
|
||||
{ ps -> ps.setString(1, flid); ps.setString(2, kind) },
|
||||
) { rs ->
|
||||
buildMap {
|
||||
rs.getString("source_seq")?.let { put("RTNO", it) }
|
||||
rs.getString("apcd")?.let { put("APCD", it) }
|
||||
rs.getString("scat")?.let { put("SCAT", it) }
|
||||
rs.getString("scdt")?.let { put("SCDT", it) }
|
||||
}
|
||||
}
|
||||
if (rows.isNotEmpty()) {
|
||||
out[key] = mapper.writeValueAsString(rows)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
}
|
||||
-70
@@ -1,70 +0,0 @@
|
||||
package com.gzzn.omms.msgexchange.infra.persistence.jdbc
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.gzzn.omms.msgexchange.infra.persistence.FlightFields
|
||||
|
||||
/**
|
||||
* FLIGHT_SCHD 读侧视图组装(v2 权威读,P4 退场后唯一读路径)。
|
||||
*
|
||||
* - 标量列直读 + 无界集合文本列(SRVT/VIPF/MAFL)
|
||||
* - FDIV/FRET/FLAB 由主表前缀标量列重建对象
|
||||
* - 10 类重复集合由明细表重建(保序、保源序号),legacy 槽位/里程碑列已随 V1.4.0 退场
|
||||
*
|
||||
* 过渡期三条读路径(assembleMerged/assembleLegacySlotsOnly)与 FS7 双读对拍
|
||||
* 已随回退窗口关闭一并移除(V1.4.0,ACM2-29 P4)。
|
||||
*/
|
||||
object FlightSchdReadAssembler {
|
||||
|
||||
/** SCHD.FLTR 标量列 + legacy 派生列(与 V1.1.0 迁移一致)。 */
|
||||
val SCALAR_COLUMNS: List<String> = listOf(
|
||||
"ALCD", "ALSC", "FLNO", "MVIN", "SODT", "FLTY", "FLIN", "ACFT", "RENO",
|
||||
"TAOP", "TAFL", "TAID", "TRML", "MAXP", "CSOP", "CSFT", "MAID",
|
||||
"ESTT", "ACTT", "STND", "PHAG", "CNCL", "REMC", "BOTM", "LACL", "FINT",
|
||||
"APPT", "EGSR", "EGST", "FHAG", "MHAG", "VIPP", "VIPR", "LBNO", "LBWT",
|
||||
"PAXC", "EXSC", "EXSR", "FTSS", "PEDT", "NEAT", "PADT", "NAAT",
|
||||
"ABDG", "LPSDT", "ABN",
|
||||
)
|
||||
|
||||
val EXCEPTION_COLUMNS: List<String> = listOf(
|
||||
"FDIV_DDES", "FDIV_DDIR", "FDIV_REMC", "FRET_REID", "FRET_RSN",
|
||||
"FLAB_ARES", "FLAB_RSN",
|
||||
)
|
||||
|
||||
val TEXT_COLUMNS: List<String> = listOf("SRVT_TEXT", "VIPF_TEXT", "MAFL_TEXT")
|
||||
|
||||
/** 库列名全集(读侧 SELECT 稳定顺序;与 V1.4.0 后表结构一致)。 */
|
||||
val ALL_COLUMNS: List<String> = SCALAR_COLUMNS + EXCEPTION_COLUMNS + TEXT_COLUMNS
|
||||
|
||||
private val mapper = ObjectMapper()
|
||||
|
||||
/** v2 权威读:标量 + 异常 + 文本列 + 明细表集合。 */
|
||||
fun assembleDetailOnly(
|
||||
flid: String,
|
||||
row: Map<String, String?>,
|
||||
detailCollections: Map<String, String>,
|
||||
): FlightFields {
|
||||
val fields = assembleScalarsAndExceptions(flid, row)
|
||||
detailCollections.forEach { (key, value) -> fields[key] = value }
|
||||
return fields
|
||||
}
|
||||
|
||||
private fun assembleScalarsAndExceptions(flid: String, row: Map<String, String?>): LinkedHashMap<String, String> {
|
||||
val fields = linkedMapOf<String, String>()
|
||||
fields["FLID"] = flid
|
||||
SCALAR_COLUMNS.forEach { column -> row[column]?.let { fields[column] = it } }
|
||||
TEXT_COLUMNS.forEach { column -> row[column]?.let { fields[column.removeSuffix("_TEXT")] = it } }
|
||||
exceptionView("FDIV", listOf("DDES" to "FDIV_DDES", "DDIR" to "FDIV_DDIR", "REMC" to "FDIV_REMC"), row)
|
||||
?.let { fields["FDIV"] = it }
|
||||
exceptionView("FRET", listOf("REID" to "FRET_REID", "RSN" to "FRET_RSN"), row)
|
||||
?.let { fields["FRET"] = it }
|
||||
exceptionView("FLAB", listOf("ARES" to "FLAB_ARES", "RSN" to "FLAB_RSN"), row)
|
||||
?.let { fields["FLAB"] = it }
|
||||
return fields
|
||||
}
|
||||
|
||||
private fun exceptionView(key: String, attrs: List<Pair<String, String>>, row: Map<String, String?>): String? {
|
||||
val node = mapper.createObjectNode()
|
||||
attrs.forEach { (attr, column) -> row[column]?.let { node.put(attr, it) } }
|
||||
return if (node.size() == 0) null else mapper.writeValueAsString(node)
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -27,10 +27,10 @@ class JdbcCminmsgInboxRepository(
|
||||
ps.setString(1, rawXml)
|
||||
}
|
||||
|
||||
override fun rawOf(cminmsgsId: Long): String? =
|
||||
override fun rawOf(msgId: Long): String? =
|
||||
ds.queryOne(
|
||||
"SELECT CMINMSGS_CLOB_MSG FROM cminmsgs WHERE CMINMSGS_ID = ?",
|
||||
{ ps -> ps.setLong(1, cminmsgsId) },
|
||||
{ ps -> ps.setLong(1, msgId) },
|
||||
) { rs -> rs.getString("CMINMSGS_CLOB_MSG") }
|
||||
|
||||
override fun pollUnprocessed(afterId: Long, limit: Int): List<Long> =
|
||||
@@ -47,7 +47,7 @@ class JdbcCminmsgInboxRepository(
|
||||
},
|
||||
) { rs -> rs.getLong("CMINMSGS_ID") }
|
||||
|
||||
override fun backfillOnSuccess(cminmsgsId: Long, sndr: String, type: String, styp: String, seqn: Long) {
|
||||
override fun backfillOnSuccess(msgId: Long, sndr: String, type: String, styp: String, seqn: Long) {
|
||||
ds.update(
|
||||
"""
|
||||
UPDATE cminmsgs
|
||||
@@ -57,7 +57,7 @@ class JdbcCminmsgInboxRepository(
|
||||
""".trimIndent(),
|
||||
) { ps ->
|
||||
ps.setString(1, "PROCESSED")
|
||||
ps.setLong(2, cminmsgsId)
|
||||
ps.setLong(2, msgId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+568
-700
File diff suppressed because it is too large
Load Diff
@@ -1,23 +0,0 @@
|
||||
package com.gzzn.omms.msgexchange.infra.pipeline
|
||||
|
||||
import com.gzzn.omms.msgexchange.codec.JacksonXmlCodec
|
||||
import com.gzzn.omms.msgexchange.processing.CodecHolder
|
||||
import com.gzzn.omms.msgexchange.processing.Handler
|
||||
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 jakarta.inject.Singleton
|
||||
|
||||
/** 主泵解码与 Handler 装配(stub / 实装模式共用真实 XmlCodec + 已注册 Handler)。 */
|
||||
@Factory
|
||||
class PipelineHolderFactory {
|
||||
|
||||
@Bean
|
||||
@Singleton
|
||||
fun codecHolder(codec: JacksonXmlCodec): CodecHolder = CodecHolder(codec)
|
||||
|
||||
@Bean
|
||||
@Singleton
|
||||
fun handlerHolder(handlers: List<Handler>): HandlerHolder = HandlerHolder(HandlerRegistry(handlers))
|
||||
}
|
||||
@@ -20,14 +20,14 @@ class ProcFailure(
|
||||
val attempts = head.attempts + 1
|
||||
if (scheduler.exhausted(attempts)) {
|
||||
procState.update(
|
||||
head.cminmsgsId, ProcStatus.DEAD,
|
||||
head.msgId, ProcStatus.DEAD,
|
||||
attempts = attempts,
|
||||
errorClass = ErrorClass.EXHAUSTED,
|
||||
lastError = "$reason; attempts=$attempts",
|
||||
)
|
||||
} else {
|
||||
procState.update(
|
||||
head.cminmsgsId, ProcStatus.FAILED,
|
||||
head.msgId, ProcStatus.FAILED,
|
||||
attempts = attempts,
|
||||
nextAttemptAt = scheduler.nextAttemptAt(attempts),
|
||||
errorClass = ec,
|
||||
|
||||
@@ -5,16 +5,28 @@ import io.micronaut.context.annotation.Requires
|
||||
import jakarta.inject.Singleton
|
||||
|
||||
/**
|
||||
* U07:stub 适配层——DeliveryPort 内存实现,仅在 msgx.stubs=true 时生效。
|
||||
* XmlCodec / Handler 由 [com.gzzn.omms.msgexchange.infra.pipeline.PipelineHolderFactory] 统一装配。
|
||||
* stub 适配层——DeliveryPort 内存实现,仅在 msgx.stubs=true 时生效。
|
||||
* 记录 (topic, key, payload):payload = null 表示 TOMBSTONE(§7.3 键缺失=删除旧值)。
|
||||
*/
|
||||
@Requires(property = "msgx.stubs", value = "true")
|
||||
@Singleton
|
||||
class StubDeliveryPort : DeliveryPort {
|
||||
val sent = mutableListOf<Pair<String, String>>()
|
||||
data class Sent(val topic: String, val key: String, val payload: String?)
|
||||
|
||||
fun clear() { sent.clear() }
|
||||
val sent = mutableListOf<Sent>()
|
||||
val tombstones: List<Sent> get() = sent.filter { it.payload == null }
|
||||
|
||||
override fun sendKafka(topic: String, payloadJson: String) { sent += topic to payloadJson }
|
||||
override fun indexFlightHts(payloadJson: String) = Unit
|
||||
fun clear() = sent.clear()
|
||||
|
||||
override fun sendKafka(topic: String, key: String, payloadJson: String) {
|
||||
sent += Sent(topic, key, payloadJson)
|
||||
}
|
||||
|
||||
override fun sendKafkaSchd(topic: String, key: String, payloadJson: String) {
|
||||
sent += Sent(topic, key, payloadJson)
|
||||
}
|
||||
|
||||
override fun sendKafkaNull(topic: String, key: String) {
|
||||
sent += Sent(topic, key, null)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,79 +2,105 @@ package com.gzzn.omms.msgexchange.infra.stub
|
||||
|
||||
import com.gzzn.omms.msgexchange.domain.ErrorClass
|
||||
import com.gzzn.omms.msgexchange.domain.EventStatus
|
||||
import com.gzzn.omms.msgexchange.domain.EventType
|
||||
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.domain.flight.FlightMainRow
|
||||
import com.gzzn.omms.msgexchange.domain.flight.FlightSnapshot
|
||||
import com.gzzn.omms.msgexchange.domain.flight.FlightState
|
||||
import com.gzzn.omms.msgexchange.domain.flight.HistoryCandidate
|
||||
import com.gzzn.omms.msgexchange.domain.flight.HistoryRules
|
||||
import com.gzzn.omms.msgexchange.infra.persistence.BackfillTodoRepository
|
||||
import com.gzzn.omms.msgexchange.infra.persistence.CminmsgInboxRepository
|
||||
import com.gzzn.omms.msgexchange.infra.persistence.FlightFields
|
||||
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.PersistOutcome
|
||||
import com.gzzn.omms.msgexchange.infra.persistence.PipelineLockRepository
|
||||
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
|
||||
import com.gzzn.omms.msgexchange.infra.persistence.ReqTrackRepository
|
||||
import com.gzzn.omms.msgexchange.infra.persistence.StaticRefRepository
|
||||
import com.gzzn.omms.msgexchange.domain.SnapshotLogEntry
|
||||
import com.gzzn.omms.msgexchange.infra.persistence.SnapshotLogRepository
|
||||
import io.micronaut.context.annotation.Requires
|
||||
import jakarta.inject.Singleton
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.time.ZoneId
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
|
||||
/**
|
||||
* U07(U01 遗留 stub):内存仓储装配——仅当 `msgx.stubs=true`(dev/影子冒烟)时生效
|
||||
* (@Requires),让「启动 + compat HTTP 写 + 主泵/投递循环 + /beans 端到端」在无 MySQL/Redis/Kafka 时可跑。
|
||||
* 与真实 Micronaut Data 实装按模块替换;切换点条件显式,不误入生产。
|
||||
* stub 仓储(msgx.stubs=true 时装配)——内存实现全部契约,测试与无库环境用。
|
||||
* 事务管理器直接执行 block(无嵌套语义);锁为 no-op(单线程测试前提)。
|
||||
*/
|
||||
@Requires(property = "msgx.stubs", value = "true")
|
||||
@Singleton
|
||||
@Requires(property = "msgx.stubs", value = "true")
|
||||
class StubPipelineTx : PipelineTransactionManager {
|
||||
override fun <T> inTransaction(block: () -> T): T = block()
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Requires(property = "msgx.stubs", value = "true")
|
||||
class StubPipelineLock : PipelineLockRepository {
|
||||
override fun lock() = Unit
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Requires(property = "msgx.stubs", value = "true")
|
||||
class StubProcState : ProcStateRepository {
|
||||
private val rows = linkedMapOf<Long, ProcState>()
|
||||
private val bound = mutableMapOf<String, Long>()
|
||||
val rows = linkedMapOf<Long, ProcState>()
|
||||
val bound = linkedMapOf<String, Long>()
|
||||
|
||||
fun clear() { rows.clear(); bound.clear() }
|
||||
|
||||
override fun insert(cminmsgsId: Long, state: ProcStatus) {
|
||||
rows[cminmsgsId] = ProcState(cminmsgsId, state)
|
||||
fun clear() {
|
||||
rows.clear(); bound.clear()
|
||||
}
|
||||
|
||||
override fun exists(cminmsgsId: Long): Boolean = rows.containsKey(cminmsgsId)
|
||||
override fun insert(msgId: Long, state: ProcStatus) {
|
||||
rows.getOrPut(msgId) { ProcState(msgId, state) }
|
||||
}
|
||||
|
||||
override fun exists(msgId: Long): Boolean = rows.containsKey(msgId)
|
||||
|
||||
override fun find(msgId: Long): ProcState? = rows[msgId]
|
||||
|
||||
override fun findSuccessTerminal(msgId: Long): Boolean = rows[msgId]?.state == ProcStatus.SUCCEEDED
|
||||
|
||||
override fun headUnfinished(): ProcState? =
|
||||
rows.filterValues { it.state == ProcStatus.PENDING || it.state == ProcStatus.FAILED }
|
||||
.minByOrNull { it.key }?.value
|
||||
rows.values.filter { it.state == ProcStatus.PENDING || it.state == ProcStatus.FAILED }.minByOrNull { it.msgId }
|
||||
|
||||
override fun tryBindIdentity(cminmsgsId: Long, identityKey: String): Boolean {
|
||||
override fun tryBindIdentity(msgId: Long, identityKey: String): Boolean {
|
||||
val owner = bound[identityKey]
|
||||
if (owner != null && owner != cminmsgsId) return false
|
||||
bound[identityKey] = cminmsgsId
|
||||
rows[cminmsgsId] = (rows[cminmsgsId] ?: ProcState(cminmsgsId, ProcStatus.PENDING)).copy(identityKey = identityKey)
|
||||
if (owner != null && owner != msgId) return false
|
||||
bound[identityKey] = msgId
|
||||
rows[msgId] = (rows[msgId] ?: ProcState(msgId, ProcStatus.PENDING)).copy(identityKey = identityKey)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun ownerOfIdentity(identityKey: String): Long? = bound[identityKey]
|
||||
|
||||
override fun update(
|
||||
cminmsgsId: Long, state: ProcStatus, nextAttemptAt: Instant?, attempts: Int?,
|
||||
errorClass: ErrorClass?, lastError: String?,
|
||||
msgId: Long,
|
||||
state: ProcStatus,
|
||||
nextAttemptAt: Instant?,
|
||||
attempts: Int?,
|
||||
errorClass: ErrorClass?,
|
||||
lastError: String?,
|
||||
) {
|
||||
val old = rows[cminmsgsId] ?: ProcState(cminmsgsId, state)
|
||||
rows[cminmsgsId] = old.copy(
|
||||
val old = rows[msgId] ?: ProcState(msgId, state)
|
||||
rows[msgId] = old.copy(
|
||||
state = state,
|
||||
nextAttemptAt = nextAttemptAt ?: old.nextAttemptAt,
|
||||
attempts = attempts ?: old.attempts,
|
||||
errorClass = errorClass ?: old.errorClass,
|
||||
lastError = lastError ?: old.lastError,
|
||||
updatedAt = Instant.now(),
|
||||
)
|
||||
}
|
||||
|
||||
override fun requeueByErrorClasses(errorClasses: List<ErrorClass>): Int {
|
||||
var n = 0
|
||||
rows.keys.toList().forEach { id ->
|
||||
val s = rows[id]!!
|
||||
if (s.errorClass != null && s.errorClass in errorClasses &&
|
||||
(s.state == ProcStatus.FAILED || s.state == ProcStatus.DEAD)) {
|
||||
rows.forEach { (id, s) ->
|
||||
if (s.errorClass in errorClasses && (s.state == ProcStatus.FAILED || s.state == ProcStatus.DEAD)) {
|
||||
rows[id] = s.copy(state = ProcStatus.PENDING, attempts = 0, nextAttemptAt = null)
|
||||
n++
|
||||
}
|
||||
@@ -82,360 +108,252 @@ class StubProcState : ProcStateRepository {
|
||||
return n
|
||||
}
|
||||
|
||||
/** 测试/运维观测用:读取当前状态行。 */
|
||||
fun snapshotOf(cminmsgsId: Long): ProcState? = rows[cminmsgsId]
|
||||
fun snapshotOf(msgId: Long): ProcState? = rows[msgId]
|
||||
}
|
||||
|
||||
@Requires(property = "msgx.stubs", value = "true")
|
||||
@Singleton
|
||||
class StubInbox : CminmsgInboxRepository {
|
||||
private val raws = linkedMapOf<Long, String>()
|
||||
private val processed = mutableSetOf<Long>()
|
||||
|
||||
fun clear() {
|
||||
raws.clear()
|
||||
processed.clear()
|
||||
}
|
||||
@Requires(property = "msgx.stubs", value = "true")
|
||||
class StubMsgEvents : MsgEventRepository {
|
||||
val rows = linkedMapOf<Long, MsgEvent>()
|
||||
private val ids = AtomicLong(0)
|
||||
|
||||
/** 模拟上游外部写信箱(不经 compat HTTP、不自动入队 PG)。 */
|
||||
fun simulateExternalWrite(rawXml: String): Long {
|
||||
fun clear() = rows.clear()
|
||||
|
||||
override fun insertAll(events: List<MsgEvent>): List<Long> =
|
||||
events.map { e ->
|
||||
val id = ids.incrementAndGet()
|
||||
rows[id] = e.copy(eventId = id)
|
||||
id
|
||||
}
|
||||
|
||||
override fun headUnsent(target: String): MsgEvent? =
|
||||
rows.values.filter { it.target == target && it.state == EventStatus.PENDING }.minByOrNull { it.eventId!! }
|
||||
|
||||
override fun claimBatch(target: String, limit: Int): List<MsgEvent> =
|
||||
rows.values.filter { it.target == target && it.state == EventStatus.PENDING }.sortedBy { it.eventId!! }.take(limit)
|
||||
|
||||
/** §7.3:同 FLID 取最新 STATE_VERSION,按事件序输出。 */
|
||||
override fun mergePendingSchd(limit: Int): List<MsgEvent> =
|
||||
rows.values
|
||||
.filter { it.target == "KAFKA:schd" && it.state == EventStatus.PENDING }
|
||||
.groupBy { it.partitionKey }
|
||||
.map { (_, group) -> group.maxBy { it.stateVersion } }
|
||||
.sortedBy { it.eventId!! }
|
||||
.take(limit)
|
||||
|
||||
override fun markSent(eventId: Long) {
|
||||
rows[eventId] = (rows[eventId] ?: return).copy(state = EventStatus.SENT)
|
||||
}
|
||||
|
||||
override fun markAllSent(eventIds: List<Long>) {
|
||||
eventIds.forEach { markSent(it) }
|
||||
}
|
||||
|
||||
override fun scheduleRetry(eventId: Long, nextAttemptAt: Instant, attempts: Int) {
|
||||
rows[eventId] = (rows[eventId] ?: return).copy(nextAttemptAt = nextAttemptAt, attempts = attempts)
|
||||
}
|
||||
|
||||
override fun markDead(eventId: Long, errorClass: ErrorClass, lastError: String, attempts: Int?) {
|
||||
rows[eventId] = (rows[eventId] ?: return).copy(
|
||||
state = EventStatus.DEAD, errorClass = errorClass, lastError = lastError,
|
||||
attempts = attempts ?: rows[eventId]?.attempts ?: 0,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Requires(property = "msgx.stubs", value = "true")
|
||||
class StubFlightState : FlightStateRepository {
|
||||
val mains = linkedMapOf<String, FlightMainRow>()
|
||||
val snapshots = linkedMapOf<String, FlightSnapshot>() // 内存全量态(主行+明细一体)
|
||||
|
||||
fun clear() {
|
||||
mains.clear(); snapshots.clear()
|
||||
}
|
||||
|
||||
override fun findMainRow(flid: String): FlightMainRow? = mains[flid]
|
||||
|
||||
override fun findMainRows(flids: Collection<String>): Map<String, FlightMainRow> =
|
||||
flids.mapNotNull { flid -> mains[flid]?.let { flid to it } }.toMap()
|
||||
|
||||
override fun loadFullSnapshot(flid: String): FlightSnapshot? = snapshots[flid]
|
||||
|
||||
/** §7.4 不可变条件:已有非空 OPERATION_DAY 且与新值不同 → DAY_GUARD_VIOLATION。 */
|
||||
override fun persistFullState(snapshot: FlightSnapshot, msgId: Long, now: Instant): PersistOutcome {
|
||||
val existing = mains[snapshot.flid]
|
||||
if (existing?.operationDay != null && existing.operationDay != snapshot.operationDay) {
|
||||
return PersistOutcome.DAY_GUARD_VIOLATION
|
||||
}
|
||||
val main = FlightMainRow(
|
||||
flid = snapshot.flid,
|
||||
operationDay = snapshot.operationDay ?: existing?.operationDay,
|
||||
state = snapshot.state,
|
||||
stateVersion = snapshot.stateVersion,
|
||||
lastMsgId = msgId,
|
||||
updatedAt = now,
|
||||
)
|
||||
mains[snapshot.flid] = main
|
||||
snapshots[snapshot.flid] = snapshot.copy(operationDay = main.operationDay)
|
||||
return if (existing == null) PersistOutcome.INSERTED else PersistOutcome.UPDATED
|
||||
}
|
||||
|
||||
override fun markDeleted(flid: String, msgId: Long, now: Instant): Boolean {
|
||||
val main = mains[flid] ?: return false
|
||||
if (main.state != FlightState.ACTIVE) return false
|
||||
mains[flid] = main.copy(state = FlightState.DELETED, stateVersion = main.stateVersion + 1, lastMsgId = msgId, updatedAt = now)
|
||||
snapshots[flid] = (snapshots[flid] ?: return true).copy(state = FlightState.DELETED, stateVersion = main.stateVersion + 1)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun revive(flid: String, msgId: Long, now: Instant): Boolean {
|
||||
val main = mains[flid] ?: return false
|
||||
if (main.state != FlightState.DELETED) return false
|
||||
mains[flid] = main.copy(state = FlightState.ACTIVE, stateVersion = main.stateVersion + 1, lastMsgId = msgId, updatedAt = now)
|
||||
snapshots[flid] = (snapshots[flid] ?: return true).copy(state = FlightState.ACTIVE, stateVersion = main.stateVersion + 1)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun findHistoryCandidates(rules: HistoryRules, zone: ZoneId, now: Instant): List<HistoryCandidate> =
|
||||
mains.values.mapNotNull { main ->
|
||||
val snap = snapshots[main.flid]
|
||||
val cancelled = snap?.scalars?.get("CNCL")
|
||||
val hasTerminalField = listOf("CNCL", "NAAT", "NEAT").any { !snap?.scalars?.get(it).isNullOrBlank() }
|
||||
val idleHit = !hasTerminalField && main.updatedAt < now.minusSeconds(rules.idleHours * 3600)
|
||||
val deletedHit = main.state == FlightState.DELETED && main.updatedAt < now.minusSeconds(rules.deletedHours * 3600)
|
||||
if (idleHit || deletedHit) {
|
||||
HistoryCandidate(main.flid, main.state, main.stateVersion, wasNeverFdel = main.state == FlightState.ACTIVE)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun purgeArchived(flids: Collection<String>): Int {
|
||||
var n = 0
|
||||
flids.forEach { flid ->
|
||||
if (mains.remove(flid) != null) n++
|
||||
snapshots.remove(flid)
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
override fun countOperationDayNull(): Int = mains.values.count { it.operationDay == null }
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Requires(property = "msgx.stubs", value = "true")
|
||||
class StubSnapshotLog : SnapshotLogRepository {
|
||||
val entries = mutableListOf<SnapshotLogEntry>()
|
||||
|
||||
fun clear() = entries.clear()
|
||||
|
||||
override fun append(entry: SnapshotLogEntry) {
|
||||
entries.add(entry)
|
||||
}
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Requires(property = "msgx.stubs", value = "true")
|
||||
class StubReqTrack : ReqTrackRepository {
|
||||
val rows = linkedMapOf<Long, ReqTrackRepository.Req>()
|
||||
private val ids = AtomicLong(0)
|
||||
|
||||
fun clear() = rows.clear()
|
||||
|
||||
override fun insert(reqType: String, operationDay: LocalDate, sender: String): Long {
|
||||
// §7.1:同类(类型+运营日+发送方)旧有效请求先置 EXPIRED
|
||||
rows.values.filter {
|
||||
it.reqType == reqType && it.operationDay == operationDay && it.sender == sender &&
|
||||
(it.state == ReqTrackRepository.ReqState.PENDING || it.state == ReqTrackRepository.ReqState.SENT)
|
||||
}.forEach { rows[it.reqId] = it.copy(state = ReqTrackRepository.ReqState.EXPIRED) }
|
||||
val id = ids.incrementAndGet()
|
||||
raws[id] = rawXml
|
||||
rows[id] = ReqTrackRepository.Req(id, reqType, operationDay, sender, ReqTrackRepository.ReqState.PENDING)
|
||||
return id
|
||||
}
|
||||
|
||||
override fun findLatest(
|
||||
reqType: String,
|
||||
operationDay: LocalDate,
|
||||
sender: String,
|
||||
states: List<ReqTrackRepository.ReqState>,
|
||||
): ReqTrackRepository.Req? =
|
||||
rows.values.filter {
|
||||
it.reqType == reqType && it.operationDay == operationDay && it.sender == sender && it.state in states
|
||||
}.maxByOrNull { it.reqId }
|
||||
|
||||
override fun completeLatest(reqType: String, operationDay: LocalDate, sender: String): Boolean {
|
||||
val req = findLatest(reqType, operationDay, sender, listOf(ReqTrackRepository.ReqState.PENDING, ReqTrackRepository.ReqState.SENT))
|
||||
?: return false
|
||||
rows[req.reqId] = req.copy(state = ReqTrackRepository.ReqState.DONE)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun linkCoutmsgs(reqId: Long, coutmsgsId: Long) {
|
||||
rows[reqId]?.let { rows[reqId] = it.copy(coutmsgsId = coutmsgsId) }
|
||||
}
|
||||
|
||||
override fun markSent(reqId: Long, sentAt: Instant) {
|
||||
rows[reqId]?.let { rows[reqId] = it.copy(state = ReqTrackRepository.ReqState.SENT, sentAt = sentAt) }
|
||||
}
|
||||
|
||||
override fun expire(reqId: Long) {
|
||||
rows[reqId]?.let { rows[reqId] = it.copy(state = ReqTrackRepository.ReqState.EXPIRED) }
|
||||
}
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Requires(property = "msgx.stubs", value = "true")
|
||||
class StubBackfillTodo : BackfillTodoRepository {
|
||||
val tasks = linkedMapOf<Long, BackfillTodoRepository.BackfillTask>()
|
||||
private val errors = linkedMapOf<Long, String?>()
|
||||
private val due = linkedMapOf<Long, Instant>()
|
||||
|
||||
fun clear() { tasks.clear(); errors.clear(); due.clear() }
|
||||
|
||||
fun lastErrorOf(msgId: Long): String? = errors[msgId]
|
||||
|
||||
override fun record(task: BackfillTodoRepository.BackfillTask, lastError: String?, now: Instant) {
|
||||
errors[task.msgId] = lastError
|
||||
due.putIfAbsent(task.msgId, now)
|
||||
tasks[task.msgId] = task.copy(attempts = tasks[task.msgId]?.attempts ?: 0)
|
||||
}
|
||||
|
||||
override fun findDue(now: Instant, limit: Int): List<BackfillTodoRepository.BackfillTask> =
|
||||
tasks.keys.filter { (due[it] ?: Instant.EPOCH) <= now }.take(limit).mapNotNull { tasks[it] }
|
||||
|
||||
override fun markFailed(msgId: Long, lastError: String?, nextAttemptAt: Instant, now: Instant) {
|
||||
errors[msgId] = lastError
|
||||
due[msgId] = nextAttemptAt
|
||||
tasks[msgId]?.let { tasks[msgId] = it.copy(attempts = it.attempts + 1) }
|
||||
}
|
||||
|
||||
override fun delete(msgId: Long) {
|
||||
tasks.remove(msgId); errors.remove(msgId); due.remove(msgId)
|
||||
}
|
||||
|
||||
override fun count(): Int = tasks.size
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Requires(property = "msgx.stubs", value = "true")
|
||||
class StubInbox : CminmsgInboxRepository {
|
||||
val raws = linkedMapOf<Long, String>()
|
||||
private val ids = AtomicLong(0)
|
||||
|
||||
fun clear() = raws.clear()
|
||||
|
||||
override fun insertRaw(rawXml: String): Long {
|
||||
val id = ids.incrementAndGet()
|
||||
raws[id] = rawXml
|
||||
return id
|
||||
}
|
||||
|
||||
override fun rawOf(cminmsgsId: Long): String? = raws[cminmsgsId]
|
||||
override fun rawOf(msgId: Long): String? = raws[msgId]
|
||||
|
||||
override fun pollUnprocessed(afterId: Long, limit: Int): List<Long> =
|
||||
raws.keys
|
||||
.filter { it > afterId && it !in processed }
|
||||
.sorted()
|
||||
.take(limit)
|
||||
raws.keys.filter { it > afterId }.sorted().take(limit)
|
||||
|
||||
override fun backfillOnSuccess(cminmsgsId: Long, sndr: String, type: String, styp: String, seqn: Long) {
|
||||
processed += cminmsgsId
|
||||
}
|
||||
}
|
||||
override fun backfillOnSuccess(msgId: Long, sndr: String, type: String, styp: String, seqn: Long) = Unit
|
||||
|
||||
@Requires(property = "msgx.stubs", value = "true")
|
||||
@Singleton
|
||||
class StubMsgEvents : MsgEventRepository {
|
||||
private val rows = linkedMapOf<Long, MsgEvent>()
|
||||
|
||||
fun clear() { rows.clear() }
|
||||
private val ids = AtomicLong(0)
|
||||
|
||||
override fun insertAll(events: List<MsgEvent>): List<Long> = events.map { e ->
|
||||
val id = ids.incrementAndGet()
|
||||
rows[id] = e.copy(eventId = id)
|
||||
id
|
||||
}
|
||||
|
||||
override fun headUnsent(target: String): MsgEvent? =
|
||||
rows.values.filter { it.target == target && it.state == EventStatus.PENDING }
|
||||
.minByOrNull { it.eventId ?: Long.MAX_VALUE }
|
||||
|
||||
override fun claimBatch(target: String, limit: Int): List<MsgEvent> =
|
||||
rows.values.filter { it.target == target && it.state == EventStatus.PENDING }
|
||||
.sortedBy { it.eventId ?: Long.MAX_VALUE }
|
||||
.take(limit)
|
||||
|
||||
override fun markSent(eventId: Long) = mutate(eventId) { it.copy(state = EventStatus.SENT) }
|
||||
|
||||
override fun markAllSent(eventIds: List<Long>) = eventIds.forEach(::markSent)
|
||||
|
||||
override fun scheduleRetry(eventId: Long, nextAttemptAt: Instant, attempts: Int) =
|
||||
mutate(eventId) { it.copy(state = EventStatus.PENDING, attempts = attempts, nextAttemptAt = nextAttemptAt) }
|
||||
|
||||
override fun markDead(eventId: Long, errorClass: ErrorClass, lastError: String, attempts: Int?) =
|
||||
mutate(eventId) { it.copy(state = EventStatus.DEAD, errorClass = errorClass, lastError = lastError, attempts = attempts ?: it.attempts) }
|
||||
|
||||
override fun insertSync(events: List<MsgEvent>) {
|
||||
insertAll(events)
|
||||
}
|
||||
|
||||
private fun mutate(eventId: Long, f: (MsgEvent) -> MsgEvent) {
|
||||
val cur = rows[eventId] ?: return
|
||||
rows[eventId] = f(cur)
|
||||
}
|
||||
}
|
||||
|
||||
@Requires(property = "msgx.stubs", value = "true")
|
||||
@Singleton
|
||||
class StubPumpJobs : PumpJobRepository {
|
||||
|
||||
fun clear() { rows.clear() }
|
||||
data class JobRow(val job: PumpJobRepository.Job, var state: String, var lastError: String? = null)
|
||||
|
||||
private val rows = linkedMapOf<Long, JobRow>()
|
||||
private val ids = AtomicLong(0)
|
||||
|
||||
override fun enqueue(kind: String) {
|
||||
val id = ids.incrementAndGet()
|
||||
rows[id] = JobRow(PumpJobRepository.Job(id, kind), "QUEUED")
|
||||
}
|
||||
|
||||
override fun headQueued(): PumpJobRepository.Job? =
|
||||
rows.values.firstOrNull { it.state == "QUEUED" }?.job
|
||||
|
||||
override fun markRunning(jobId: Long) { rows[jobId]?.state = "RUNNING" }
|
||||
override fun markDone(jobId: Long) { rows[jobId]?.state = "DONE" }
|
||||
override fun markFailed(jobId: Long, lastError: String) { rows[jobId]?.state = "FAILED"; rows[jobId]?.lastError = lastError }
|
||||
}
|
||||
|
||||
@Requires(property = "msgx.stubs", value = "true")
|
||||
@Singleton
|
||||
class StubPipelineTransactionManager : PipelineTransactionManager {
|
||||
override fun <T> inTransaction(block: () -> T): T = block()
|
||||
}
|
||||
|
||||
@Requires(property = "msgx.stubs", value = "true")
|
||||
@Singleton
|
||||
class StubFlightSchd : FlightSchdRepository {
|
||||
data class Record(
|
||||
val flid: String,
|
||||
val fday: String?,
|
||||
val fields: FlightFields,
|
||||
val stateVersion: Long = 0L,
|
||||
val lastMessageId: String = "",
|
||||
val createdAt: Instant,
|
||||
val updatedAt: Instant,
|
||||
)
|
||||
|
||||
private val records = linkedMapOf<String, Record>()
|
||||
private val detailCollections = mutableMapOf<String, Map<String, List<Map<String, String>>>>()
|
||||
private val gens = mutableMapOf<String, FlightSchdRepository.GenMeta>()
|
||||
|
||||
fun clear() {
|
||||
records.clear()
|
||||
detailCollections.clear()
|
||||
gens.clear()
|
||||
}
|
||||
|
||||
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 { flid ->
|
||||
records.remove(flid)
|
||||
detailCollections.remove(flid)
|
||||
}
|
||||
return toRemove.size
|
||||
}
|
||||
|
||||
override fun findByFlid(flid: String): FlightFields? {
|
||||
val rec = records[flid] ?: return null
|
||||
return mergeFields(rec)
|
||||
}
|
||||
|
||||
private fun mergeFields(rec: Record): FlightFields {
|
||||
val out = linkedMapOf<String, String>()
|
||||
out.putAll(rec.fields)
|
||||
val mapper = com.fasterxml.jackson.databind.ObjectMapper()
|
||||
detailCollections[rec.flid]?.forEach { (key, items) ->
|
||||
out[key] = mapper.writeValueAsString(items)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
override fun findNextStateByFlid(flid: String): com.gzzn.omms.msgexchange.domain.flight.FlightNextState? {
|
||||
val rec = records[flid] ?: return null
|
||||
return com.gzzn.omms.msgexchange.domain.flight.FlightNextState(
|
||||
flid = rec.flid,
|
||||
scalars = rec.fields.filterKeys { it != "FLID" },
|
||||
collections = detailCollections[flid] ?: emptyMap(),
|
||||
stateVersion = rec.stateVersion,
|
||||
lastMessageId = rec.lastMessageId,
|
||||
)
|
||||
}
|
||||
|
||||
override fun findByFlids(flids: Collection<String>): Map<String, FlightFields> =
|
||||
flids.mapNotNull { flid -> records[flid]?.let { flid to mergeFields(it) } }.toMap()
|
||||
|
||||
override fun findByDay(day: String): List<Pair<String, FlightFields>> =
|
||||
records.values.filter { it.fday == day }
|
||||
.sortedBy { it.flid }
|
||||
.map { it.flid to mergeFields(it) }
|
||||
|
||||
override fun findAll(): Map<String, FlightFields> =
|
||||
records.mapValues { mergeFields(it.value) }
|
||||
|
||||
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++
|
||||
detailCollections.remove(flid)
|
||||
}
|
||||
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 persistNextStates(
|
||||
day: String?,
|
||||
states: List<com.gzzn.omms.msgexchange.domain.flight.FlightNextState>,
|
||||
snapshotReplace: Boolean,
|
||||
now: Instant,
|
||||
) {
|
||||
for (state in states) {
|
||||
val scalarFields = linkedMapOf("FLID" to state.flid)
|
||||
state.scalars.forEach { (k, v) -> scalarFields[k] = v }
|
||||
val existing = records[state.flid]
|
||||
if (snapshotReplace && day != null) {
|
||||
records[state.flid] = Record(
|
||||
flid = state.flid,
|
||||
fday = day,
|
||||
fields = scalarFields,
|
||||
stateVersion = state.stateVersion,
|
||||
lastMessageId = state.lastMessageId,
|
||||
createdAt = existing?.createdAt ?: now,
|
||||
updatedAt = now,
|
||||
)
|
||||
detailCollections[state.flid] = state.collections
|
||||
} else {
|
||||
// nextState 由引擎在当前态上合并而来,是完整权威态:直接整体替换(含清除语义)
|
||||
records[state.flid] = Record(
|
||||
flid = state.flid,
|
||||
fday = existing?.fday,
|
||||
fields = scalarFields,
|
||||
stateVersion = state.stateVersion,
|
||||
lastMessageId = state.lastMessageId,
|
||||
createdAt = existing?.createdAt ?: now,
|
||||
updatedAt = now,
|
||||
)
|
||||
detailCollections[state.flid] = state.collections
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 保留供审计断言)。 */
|
||||
@Requires(property = "msgx.stubs", value = "true")
|
||||
@Singleton
|
||||
class StubStaticRef : StaticRefRepository {
|
||||
private val rows = mutableMapOf<Pair<String, String>, RefUpsert>()
|
||||
|
||||
fun clear() { rows.clear() }
|
||||
|
||||
override fun upsertAll(refs: List<RefUpsert>) {
|
||||
refs.forEach { rows[it.rtype to it.rkey] = it }
|
||||
}
|
||||
|
||||
override fun findByType(type: String): List<RefUpsert> =
|
||||
rows.values.filter { it.rtype == type }
|
||||
}
|
||||
|
||||
@Requires(property = "msgx.stubs", value = "true")
|
||||
@Singleton
|
||||
class StubReqTrack : ReqTrackRepository {
|
||||
private val rows = mutableMapOf<Long, ReqTrackRepository.Req>()
|
||||
|
||||
fun clear() { rows.clear() }
|
||||
private val ids = AtomicLong(0)
|
||||
|
||||
override fun findOpenByKind(kind: String): ReqTrackRepository.Req? =
|
||||
rows.values.firstOrNull { it.reqType == kind && it.state in setOf("REGISTERED", "SENT", "WAITING") }
|
||||
|
||||
override fun forceExpireOpenOf(kind: String) {
|
||||
rows.keys.toList().forEach { id ->
|
||||
val r = rows[id]!!
|
||||
if (r.reqType == kind && r.state in setOf("REGISTERED", "SENT", "WAITING"))
|
||||
rows[id] = r.copy(state = "EXPIRED")
|
||||
}
|
||||
}
|
||||
|
||||
override fun insert(kind: String, paramsJson: String): Long {
|
||||
val id = ids.incrementAndGet()
|
||||
rows[id] = ReqTrackRepository.Req(id, kind, "REGISTERED")
|
||||
return id
|
||||
}
|
||||
|
||||
override fun linkCoutmsgs(reqId: Long, coutmsgsId: Long) = Unit
|
||||
override fun markSent(reqId: Long, sentAt: Instant) { rows[reqId]?.let { rows[reqId] = it.copy(state = "SENT", sentAt = sentAt) } }
|
||||
override fun expireIfWaiting(reqId: Long) { rows[reqId]?.let { rows[reqId] = it.copy(state = "EXPIRED") } }
|
||||
override fun markDone(reqId: Long) { rows[reqId]?.let { rows[reqId] = it.copy(state = "DONE") } }
|
||||
}
|
||||
|
||||
@Requires(property = "msgx.stubs", value = "true")
|
||||
@Singleton
|
||||
class StubBackfillTodo : BackfillTodoRepository {
|
||||
private data class Row(val task: BackfillTodoRepository.BackfillTask, var nextAttemptAt: Instant)
|
||||
|
||||
private val rows = linkedMapOf<Long, Row>()
|
||||
private val errors = linkedMapOf<Long, String?>()
|
||||
|
||||
@Synchronized
|
||||
fun clear() {
|
||||
rows.clear()
|
||||
errors.clear()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun tasks(): List<BackfillTodoRepository.BackfillTask> = rows.values.map { it.task }
|
||||
|
||||
@Synchronized
|
||||
fun lastErrorOf(cminmsgsId: Long): String? = errors[cminmsgsId]
|
||||
|
||||
@Synchronized
|
||||
override fun record(task: BackfillTodoRepository.BackfillTask, lastError: String?, now: Instant) {
|
||||
rows[task.cminmsgsId] = Row(task, now)
|
||||
errors[task.cminmsgsId] = lastError
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun findDue(now: Instant, limit: Int): List<BackfillTodoRepository.BackfillTask> =
|
||||
rows.values.filter { it.nextAttemptAt <= now }.map { it.task }.take(limit)
|
||||
|
||||
@Synchronized
|
||||
override fun markFailed(cminmsgsId: Long, lastError: String?, nextAttemptAt: Instant, now: Instant) {
|
||||
rows[cminmsgsId]?.let { rows[cminmsgsId] = it.copy(task = it.task.copy(attempts = it.task.attempts + 1), nextAttemptAt = nextAttemptAt) }
|
||||
errors[cminmsgsId] = lastError
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun delete(cminmsgsId: Long) {
|
||||
rows.remove(cminmsgsId)
|
||||
errors.remove(cminmsgsId)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun count(): Int = rows.size
|
||||
/** 测试辅助:模拟上游外部写入共享信箱(不经本系统)。 */
|
||||
fun simulateExternalWrite(rawXml: String): Long = insertRaw(rawXml)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user