feat(persistence): v2 persistNextStates dual-write and detail-table reads

Wire findNextStateByFlid, CAS last_message_id, detail-table CRUD, and stub
parity for incremental/snapshot nextState persistence without legacy slot writes.
This commit is contained in:
windyboy
2026-09-08 11:26:19 +08:00
parent 57bb5ea233
commit bf1b5a371d
4 changed files with 426 additions and 23 deletions
@@ -97,6 +97,7 @@ interface FlightSchdRepository {
val fday: String, val fday: String,
val version: Long, val version: Long,
val flids: Set<String>, val flids: Set<String>,
val lastMessageId: String? = null,
val updatedAt: Instant = Instant.now(), val updatedAt: Instant = Instant.now(),
) )
@@ -112,6 +113,9 @@ interface FlightSchdRepository {
/** 点查单航班字段集;无字段行(含航班不存在)返回 null。 */ /** 点查单航班字段集;无字段行(含航班不存在)返回 null。 */
fun findByFlid(flid: String): FlightFields? fun findByFlid(flid: String): FlightFields?
/** v2:点查单航班 nextState(含 state_version / last_message_id 追踪字段)。 */
fun findNextStateByFlid(flid: String): com.gzzn.omms.msgexchange.domain.flight.FlightNextState?
/** 点查多航班字段集:FLID → 字段集,仅含实际存在的航班。 */ /** 点查多航班字段集:FLID → 字段集,仅含实际存在的航班。 */
fun findByFlids(flids: Collection<String>): Map<String, FlightFields> fun findByFlids(flids: Collection<String>): Map<String, FlightFields>
@@ -140,6 +144,17 @@ interface FlightSchdRepository {
* 历史代清理:清理 cutoffDay 之前的历史代记录(与 FLIGHT_SCHD 历史清场生命周期对齐)。 * 历史代清理:清理 cutoffDay 之前的历史代记录(与 FLIGHT_SCHD 历史清场生命周期对齐)。
*/ */
fun deleteGenBefore(cutoffDay: String): Int fun deleteGenBefore(cutoffDay: String): Int
/**
* v2 无损明细:将 nextState 写入宽表 + 明细表,并更新追踪字段。
* 必须在 PipelineTransactionManager 事务内调用。
*/
fun persistNextStates(
day: String?,
states: List<com.gzzn.omms.msgexchange.domain.flight.FlightNextState>,
snapshotReplace: Boolean,
now: Instant = Instant.now(),
)
} }
/** 事务管理器抽象:自有 PG 单事务原子保障。 */ /** 事务管理器抽象:自有 PG 单事务原子保障。 */
@@ -0,0 +1,179 @@
package com.gzzn.omms.msgexchange.infra.persistence.jdbc
import com.gzzn.omms.msgexchange.domain.flight.FlightNextState
import java.sql.Timestamp
import java.time.Instant
import javax.sql.DataSource
/**
* v2 明细表读写(flight-state-design-v2 §3.2)。
* 与宽表双写;读路径优先明细表(完整顺序/源序号保真)。
*/
internal object FlightDetailTables {
/** 由明细表承载的集合键(v2 读写的权威来源)。 */
val DETAIL_COLLECTION_KEYS: Set<String> = setOf(
"GTDT", "CKDT", "CLDT", "PSDT", "CHDT", "DELY", "ABTM", "CHOT", "ROUT", "ERUT",
)
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 ->
spec.columns.associate { (attr, col) -> attr to rs.getString(col) }.toMutableMap().apply {
put(spec.seqAttr, rs.getString("source_seq"))
}
}
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 ->
mapOf(
"RTNO" to rs.getString("source_seq"),
"APCD" to rs.getString("apcd"),
"SCAT" to rs.getString("scat"),
"SCDT" to rs.getString("scdt"),
)
}
if (rows.isNotEmpty()) {
out[key] = mapper.writeValueAsString(rows)
}
}
return out
}
fun hasAnyDetailRows(ds: DataSource, flid: String): Boolean {
for (spec in SPECS) {
val count = ds.queryOne(
"SELECT 1 FROM ${spec.table} WHERE flid = ? LIMIT 1",
{ ps -> ps.setString(1, flid) },
) { 1 }
if (count != null) return true
}
return ds.queryOne(
"SELECT 1 FROM flight_route_point WHERE flid = ? LIMIT 1",
{ ps -> ps.setString(1, flid) },
) { 1 } != null
}
}
@@ -394,6 +394,9 @@ class JdbcFlightSchdRepository(
/** 无界集合紧凑 JSON 数组字串列(键名去 _TEXT 后缀即 legacy 视图键,直存直取)。 */ /** 无界集合紧凑 JSON 数组字串列(键名去 _TEXT 后缀即 legacy 视图键,直存直取)。 */
private val TEXT_COLUMNS: List<String> = listOf("SRVT_TEXT", "VIPF_TEXT", "MAFL_TEXT") private val TEXT_COLUMNS: List<String> = listOf("SRVT_TEXT", "VIPF_TEXT", "MAFL_TEXT")
/** v2 主表写入列:标量 + 异常 + 无界文本;集合由明细表承载,不经槽位/里程碑列。 */
private val SCALAR_WRITE_COLUMNS: List<String> = SCALAR_COLUMNS + EXCEPTION_COLUMNS + TEXT_COLUMNS
/** 库列名全集(读侧 SELECT 与视图重建的稳定顺序,与 V1.2.0 迁移一致)。 */ /** 库列名全集(读侧 SELECT 与视图重建的稳定顺序,与 V1.2.0 迁移一致)。 */
private val ALL_COLUMNS: List<String> = SCALAR_COLUMNS + EXCEPTION_COLUMNS + private val ALL_COLUMNS: List<String> = SCALAR_COLUMNS + EXCEPTION_COLUMNS +
OCCURRENCE_COLLECTIONS.flatMap { c -> c.slots.flatMap(Map<String, String>::values) } + OCCURRENCE_COLLECTIONS.flatMap { c -> c.slots.flatMap(Map<String, String>::values) } +
@@ -442,7 +445,22 @@ class JdbcFlightSchdRepository(
return out return out
} }
/** FDIV/FRET/FLAB1:0..1 单值异常对象 → 前缀标量列(自由文本取首个命中的文本键)。 */ /** v2:仅标量/异常/文本列 flatten;集合键由明细表读写,不经过槽位列。 */
private fun flattenScalarsOnly(scalars: Map<String, String>): Map<String, String?> {
val fields = linkedMapOf<String, String>()
scalars.forEach { (key, value) -> fields[key] = value }
val out = mutableMapOf<String, String?>()
scalars.forEach { (key, value) ->
when {
key in SCALAR_KEY_SET -> out[key] = value
key in setOf("SRVT", "VIPF", "MAFL") -> out["${key}_TEXT"] = value
}
}
flattenExceptions(fields, out)
return out
}
/** FDIV/FRET/FLAB1:0..1 单值异常对象 → 前缀标量列(自由文本取首个命中的文本键)。 */
private fun flattenExceptions(fields: FlightFields, out: MutableMap<String, String?>) { private fun flattenExceptions(fields: FlightFields, out: MutableMap<String, String?>) {
fun flatten(key: String, mappings: Map<String, String>, textTarget: String) { fun flatten(key: String, mappings: Map<String, String>, textTarget: String) {
val raw = fields[key] ?: return val raw = fields[key] ?: return
@@ -627,6 +645,7 @@ class JdbcFlightSchdRepository(
override fun deleteDiffByDay(day: String, delFlids: Collection<String>): Int { override fun deleteDiffByDay(day: String, delFlids: Collection<String>): Int {
if (delFlids.isEmpty()) return 0 if (delFlids.isEmpty()) return 0
FlightDetailTables.deleteForFlids(ds, delFlids)
var totalDeleted = 0 var totalDeleted = 0
val sqlDate = toSqlDate(day) val sqlDate = toSqlDate(day)
for (chunk in delFlids.chunked(200)) { for (chunk in delFlids.chunked(200)) {
@@ -660,12 +679,17 @@ class JdbcFlightSchdRepository(
?.let { fields["FRET"] = it } ?.let { fields["FRET"] = it }
exceptionView("FLAB", listOf("ARES" to "FLAB_ARES", "RSN" to "FLAB_RSN"), row) exceptionView("FLAB", listOf("ARES" to "FLAB_ARES", "RSN" to "FLAB_RSN"), row)
?.let { fields["FLAB"] = it } ?.let { fields["FLAB"] = it }
delayView(row)?.let { fields["DELY"] = it } val detailCollections = FlightDetailTables.loadCollections(ds, flid)
airbridgeView(row)?.let { fields["ABTM"] = it } val detailKeys = detailCollections.keys
chocksView(row)?.let { fields["CHOT"] = it } if ("DELY" !in detailKeys) delayView(row)?.let { fields["DELY"] = it }
row["ROUT_PATH"]?.let { fields["ROUT"] = routeView(it) } if ("ABTM" !in detailKeys) airbridgeView(row)?.let { fields["ABTM"] = it }
row["ERUT_PATH"]?.let { fields["ERUT"] = routeView(it) } if ("CHOT" !in detailKeys) chocksView(row)?.let { fields["CHOT"] = it }
OCCURRENCE_COLLECTIONS.forEach { c -> slotView(c, row)?.let { fields[c.key] = it } } if ("ROUT" !in detailKeys) row["ROUT_PATH"]?.let { fields["ROUT"] = routeView(it) }
if ("ERUT" !in detailKeys) row["ERUT_PATH"]?.let { fields["ERUT"] = routeView(it) }
OCCURRENCE_COLLECTIONS.forEach { c ->
if (c.key !in detailKeys) slotView(c, row)?.let { fields[c.key] = it }
}
detailCollections.forEach { (key, value) -> fields[key] = value }
return flid to fields return flid to fields
} }
@@ -740,6 +764,20 @@ class JdbcFlightSchdRepository(
::mapFlightRow, ::mapFlightRow,
)?.second )?.second
override fun findNextStateByFlid(flid: String): com.gzzn.omms.msgexchange.domain.flight.FlightNextState? =
ds.queryOne(
"SELECT flid, state_version, last_message_id, ${ALL_COLUMNS.joinToString(", ")} FROM flight_schd WHERE flid = ?",
{ ps -> ps.setString(1, flid) },
) { rs ->
val (id, fields) = mapFlightRow(rs)
com.gzzn.omms.msgexchange.domain.flight.FlightStateEngine.fromFlightFields(
id,
fields,
rs.getLong("state_version"),
rs.getString("last_message_id") ?: "",
)
}
override fun findByFlids(flids: Collection<String>): Map<String, FlightFields> { override fun findByFlids(flids: Collection<String>): Map<String, FlightFields> {
if (flids.isEmpty()) return emptyMap() if (flids.isEmpty()) return emptyMap()
val result = linkedMapOf<String, FlightFields>() val result = linkedMapOf<String, FlightFields>()
@@ -769,6 +807,7 @@ class JdbcFlightSchdRepository(
override fun deleteByFlids(flids: Set<String>): Int { override fun deleteByFlids(flids: Set<String>): Int {
if (flids.isEmpty()) return 0 if (flids.isEmpty()) return 0
FlightDetailTables.deleteForFlids(ds, flids)
var totalDeleted = 0 var totalDeleted = 0
for (chunk in flids.chunked(200)) { for (chunk in flids.chunked(200)) {
val placeholders = chunk.joinToString(",") { "?" } val placeholders = chunk.joinToString(",") { "?" }
@@ -782,9 +821,11 @@ class JdbcFlightSchdRepository(
override fun getGen(day: String): FlightSchdRepository.GenMeta? { override fun getGen(day: String): FlightSchdRepository.GenMeta? {
val meta = ds.queryOne( val meta = ds.queryOne(
"SELECT version, updated_at FROM schd_gen WHERE fday = ?", "SELECT version, last_message_id, updated_at FROM schd_gen WHERE fday = ?",
{ ps -> ps.setDate(1, toSqlDate(day)) }, { ps -> ps.setDate(1, toSqlDate(day)) },
) { rs -> rs.getLong("version") to (rs.getInstant("updated_at") ?: Instant.now()) } ) { rs ->
Triple(rs.getLong("version"), rs.getString("last_message_id"), rs.getInstant("updated_at") ?: Instant.now())
}
?: return null ?: return null
val flids = ds.query( val flids = ds.query(
"SELECT flid FROM schd_gen_flid WHERE fday = ? ORDER BY flid ASC", "SELECT flid FROM schd_gen_flid WHERE fday = ? ORDER BY flid ASC",
@@ -794,7 +835,8 @@ class JdbcFlightSchdRepository(
fday = day, fday = day,
version = meta.first, version = meta.first,
flids = flids, flids = flids,
updatedAt = meta.second, lastMessageId = meta.second,
updatedAt = meta.third,
) )
} }
@@ -832,14 +874,15 @@ class JdbcFlightSchdRepository(
if (expected == 0L) { if (expected == 0L) {
val inserted = ds.update( val inserted = ds.update(
""" """
INSERT INTO schd_gen (fday, version, updated_at) INSERT INTO schd_gen (fday, version, last_message_id, updated_at)
VALUES (?, ?, ?) VALUES (?, ?, ?, ?)
ON CONFLICT (fday) DO NOTHING ON CONFLICT (fday) DO NOTHING
""".trimIndent(), """.trimIndent(),
) { ps -> ) { ps ->
ps.setDate(1, sqlDate) ps.setDate(1, sqlDate)
ps.setLong(2, newGen.version) ps.setLong(2, newGen.version)
ps.setTimestamp(3, sqlTimestamp) ps.setString(3, newGen.lastMessageId)
ps.setTimestamp(4, sqlTimestamp)
} }
if (inserted == 1) { if (inserted == 1) {
replaceGenFlids(day, newGen.flids) replaceGenFlids(day, newGen.flids)
@@ -850,14 +893,15 @@ class JdbcFlightSchdRepository(
val updated = ds.update( val updated = ds.update(
""" """
UPDATE schd_gen UPDATE schd_gen
SET version = ?, updated_at = ? SET version = ?, last_message_id = ?, updated_at = ?
WHERE fday = ? AND version = ? WHERE fday = ? AND version = ?
""".trimIndent(), """.trimIndent(),
) { ps -> ) { ps ->
ps.setLong(1, newGen.version) ps.setLong(1, newGen.version)
ps.setTimestamp(2, sqlTimestamp) ps.setString(2, newGen.lastMessageId)
ps.setDate(3, sqlDate) ps.setTimestamp(3, sqlTimestamp)
ps.setLong(4, expected) ps.setDate(4, sqlDate)
ps.setLong(5, expected)
} }
if (updated == 1) { if (updated == 1) {
replaceGenFlids(day, newGen.flids) replaceGenFlids(day, newGen.flids)
@@ -866,6 +910,98 @@ class JdbcFlightSchdRepository(
return false return false
} }
/** v2 主表行存在性保障(增量路径新插 FDAY=NULL)。 */
private fun ensureFlightRow(flid: String, now: Instant) {
val ts = now.toSqlTimestamp()
ds.update(
"INSERT INTO flight_schd (flid, created_at, updated_at) VALUES (?, ?, ?) " +
"ON CONFLICT (flid) DO UPDATE SET updated_at = EXCLUDED.updated_at",
) { ps ->
ps.setString(1, flid)
ps.setTimestamp(2, ts)
ps.setTimestamp(3, ts)
}
}
/** v2 快照:标量列全量替换(集合不写槽位/里程碑列)。 */
private fun upsertSnapshotScalars(day: String, state: com.gzzn.omms.msgexchange.domain.flight.FlightNextState, now: Instant) {
val stored = flattenScalarsOnly(state.scalars)
val sqlDate = toSqlDate(day)
val sqlTimestamp = now.toSqlTimestamp()
val sql = buildString {
append("INSERT INTO flight_schd (flid, fday, ")
append(SCALAR_WRITE_COLUMNS.joinToString(", "))
append(", last_message_id, state_version, created_at, updated_at) VALUES (?, ?, ")
append(SCALAR_WRITE_COLUMNS.joinToString(", ") { "?" })
append(", ?, ?, ?, ?) ON CONFLICT (flid) DO UPDATE SET fday = EXCLUDED.fday, ")
append(SCALAR_WRITE_COLUMNS.joinToString(", ") { "$it = EXCLUDED.$it" })
append(", last_message_id = EXCLUDED.last_message_id, state_version = EXCLUDED.state_version, updated_at = EXCLUDED.updated_at")
}
ds.update(sql) { ps ->
var i = 1
ps.setString(i++, state.flid)
ps.setDate(i++, sqlDate)
for (column in SCALAR_WRITE_COLUMNS) {
ps.setString(i++, stored[column])
}
ps.setString(i++, state.lastMessageId)
ps.setLong(i++, state.stateVersion)
ps.setTimestamp(i++, sqlTimestamp)
ps.setTimestamp(i++, sqlTimestamp)
}
}
/** v2 增量:仅更新出现的标量列 + 追踪字段。 */
private fun upsertIncrementalScalars(state: com.gzzn.omms.msgexchange.domain.flight.FlightNextState, now: Instant) {
val stored = flattenScalarsOnly(state.scalars)
if (stored.isEmpty()) {
updateTrackingFields(state, now)
return
}
val assignments = stored.keys.map { "$it = ?" }.toMutableList()
assignments += listOf("last_message_id = ?", "state_version = ?", "updated_at = ?")
val sql = "UPDATE flight_schd SET ${assignments.joinToString(", ")} WHERE flid = ?"
val sqlTimestamp = now.toSqlTimestamp()
ds.update(sql) { ps ->
var i = 1
stored.values.forEach { ps.setString(i++, it) }
ps.setString(i++, state.lastMessageId)
ps.setLong(i++, state.stateVersion)
ps.setTimestamp(i++, sqlTimestamp)
ps.setString(i, state.flid)
}
}
private fun updateTrackingFields(state: com.gzzn.omms.msgexchange.domain.flight.FlightNextState, now: Instant) {
ds.update(
"UPDATE flight_schd SET last_message_id = ?, state_version = ?, updated_at = ? WHERE flid = ?",
) { ps ->
ps.setString(1, state.lastMessageId)
ps.setLong(2, state.stateVersion)
ps.setTimestamp(3, now.toSqlTimestamp())
ps.setString(4, state.flid)
}
}
override fun persistNextStates(
day: String?,
states: List<com.gzzn.omms.msgexchange.domain.flight.FlightNextState>,
snapshotReplace: Boolean,
now: Instant,
) {
if (states.isEmpty()) return
for (state in states) {
if (snapshotReplace && day != null) {
upsertSnapshotScalars(day, state, now)
} else {
ensureFlightRow(state.flid, now)
upsertIncrementalScalars(state, now)
}
FlightDetailTables.replaceAll(ds, state, now)
}
}
override fun deleteGenBefore(cutoffDay: String): Int = override fun deleteGenBefore(cutoffDay: String): Int =
ds.update( ds.update(
"DELETE FROM schd_gen WHERE fday < ?", "DELETE FROM schd_gen WHERE fday < ?",
@@ -203,15 +203,19 @@ class StubFlightSchd : FlightSchdRepository {
val flid: String, val flid: String,
val fday: String?, val fday: String?,
val fields: FlightFields, val fields: FlightFields,
val stateVersion: Long = 0L,
val lastMessageId: String = "",
val createdAt: Instant, val createdAt: Instant,
val updatedAt: Instant, val updatedAt: Instant,
) )
private val records = linkedMapOf<String, Record>() private val records = linkedMapOf<String, Record>()
private val detailCollections = mutableMapOf<String, Map<String, List<Map<String, String>>>>()
private val gens = mutableMapOf<String, FlightSchdRepository.GenMeta>() private val gens = mutableMapOf<String, FlightSchdRepository.GenMeta>()
fun clear() { fun clear() {
records.clear() records.clear()
detailCollections.clear()
gens.clear() gens.clear()
} }
@@ -245,28 +249,56 @@ class StubFlightSchd : FlightSchdRepository {
if (delFlids.isEmpty()) return 0 if (delFlids.isEmpty()) return 0
val delSet = delFlids.toSet() val delSet = delFlids.toSet()
val toRemove = records.filter { (flid, rec) -> rec.fday == day && flid in delSet }.keys val toRemove = records.filter { (flid, rec) -> rec.fday == day && flid in delSet }.keys
toRemove.forEach { records.remove(it) } toRemove.forEach { flid ->
records.remove(flid)
detailCollections.remove(flid)
}
return toRemove.size return toRemove.size
} }
override fun findByFlid(flid: String): FlightFields? = records[flid]?.fields 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> = override fun findByFlids(flids: Collection<String>): Map<String, FlightFields> =
flids.mapNotNull { flid -> records[flid]?.let { flid to it.fields } }.toMap() flids.mapNotNull { flid -> records[flid]?.let { flid to mergeFields(it) } }.toMap()
override fun findByDay(day: String): List<Pair<String, FlightFields>> = override fun findByDay(day: String): List<Pair<String, FlightFields>> =
records.values.filter { it.fday == day } records.values.filter { it.fday == day }
.sortedBy { it.flid } .sortedBy { it.flid }
.map { it.flid to it.fields } .map { it.flid to mergeFields(it) }
override fun findAll(): Map<String, FlightFields> = override fun findAll(): Map<String, FlightFields> =
records.mapValues { it.value.fields } records.mapValues { mergeFields(it.value) }
override fun deleteByFlids(flids: Set<String>): Int { override fun deleteByFlids(flids: Set<String>): Int {
if (flids.isEmpty()) return 0 if (flids.isEmpty()) return 0
var count = 0 var count = 0
for (flid in flids) { for (flid in flids) {
if (records.remove(flid) != null) count++ if (records.remove(flid) != null) count++
detailCollections.remove(flid)
} }
return count return count
} }
@@ -280,7 +312,48 @@ class StubFlightSchd : FlightSchdRepository {
return true return true
} }
override fun deleteGenBefore(cutoffDay: String): Int { 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 {
val mergedScalars = (existing?.fields?.filterKeys {
it == "FLID" || it !in com.gzzn.omms.msgexchange.infra.persistence.jdbc.FlightDetailTables.DETAIL_COLLECTION_KEYS
} ?: emptyMap()) + scalarFields
records[state.flid] = Record(
flid = state.flid,
fday = existing?.fday,
fields = mergedScalars,
stateVersion = state.stateVersion,
lastMessageId = state.lastMessageId,
createdAt = existing?.createdAt ?: now,
updatedAt = now,
)
val mergedCollections = (detailCollections[state.flid] ?: emptyMap()).toMutableMap()
state.collections.forEach { (key, items) -> mergedCollections[key] = items }
detailCollections[state.flid] = mergedCollections
}
}
}
override fun deleteGenBefore(cutoffDay: String): Int {
val toRemove = gens.keys.filter { it < cutoffDay } val toRemove = gens.keys.filter { it < cutoffDay }
toRemove.forEach { gens.remove(it) } toRemove.forEach { gens.remove(it) }
return toRemove.size return toRemove.size