fix(config): fail fast on invalid operation-day zone
运营日时区只认 PARAM:msgx.operation-day.zone:JobRunner、HistorySweepJob、JdbcSnapshotLogRepository.purgeBefore 改走注入的 OperationDayProps;ScheduleProcessor/AdftProcessor 删除「非法值回退字面量」;新增 @Context 启动自检 OperationDayZoneCheck,配置错位在启动时暴露。 验证:./gradlew test 126 tests / 0 fail(新增 OperationDayPropsTest 覆盖合法解析与非法即失败)。
This commit is contained in:
@@ -1,6 +1,9 @@
|
|||||||
package com.gzzn.omms.msgexchange.config
|
package com.gzzn.omms.msgexchange.config
|
||||||
|
|
||||||
import io.micronaut.context.annotation.ConfigurationProperties
|
import io.micronaut.context.annotation.ConfigurationProperties
|
||||||
|
import io.micronaut.context.annotation.Context
|
||||||
|
import jakarta.inject.Singleton
|
||||||
|
import java.time.ZoneId
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 算航班运营保障日(OPERATION_DAY)要用的两个参数,对应配置里的 `msgx.operation-day.*`:
|
* 算航班运营保障日(OPERATION_DAY)要用的两个参数,对应配置里的 `msgx.operation-day.*`:
|
||||||
@@ -16,4 +19,22 @@ class OperationDayProps {
|
|||||||
|
|
||||||
/** 切日边界小时:SODT 的本地时刻早于这个点就归属前一个运营日。取值 0–23。 */
|
/** 切日边界小时:SODT 的本地时刻早于这个点就归属前一个运营日。取值 0–23。 */
|
||||||
var cutoffHour: Int = 0
|
var cutoffHour: Int = 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析机场时区。非法值直接失败,**不回退到代码字面量**:
|
||||||
|
* `PARAM:msgx.operation-day.zone` 是运营日时区的唯一出处,回退会把配置错位藏起来。
|
||||||
|
*/
|
||||||
|
fun zoneId(): ZoneId =
|
||||||
|
try {
|
||||||
|
ZoneId.of(zone)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw IllegalStateException("msgx.operation-day.zone is not a valid IANA zone: '$zone'", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 启动自检:时区非法必须在启动时失败,而不是等第一条报文进来才炸;`@Context` 使其急切创建。 */
|
||||||
|
@Singleton
|
||||||
|
@Context
|
||||||
|
class OperationDayZoneCheck(props: OperationDayProps) {
|
||||||
|
private val zone: ZoneId = props.zoneId()
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -1,5 +1,6 @@
|
|||||||
package com.gzzn.omms.msgexchange.infra.persistence.jdbc
|
package com.gzzn.omms.msgexchange.infra.persistence.jdbc
|
||||||
|
|
||||||
|
import com.gzzn.omms.msgexchange.config.OperationDayProps
|
||||||
import com.gzzn.omms.msgexchange.domain.ErrorClass
|
import com.gzzn.omms.msgexchange.domain.ErrorClass
|
||||||
import com.gzzn.omms.msgexchange.domain.EventStatus
|
import com.gzzn.omms.msgexchange.domain.EventStatus
|
||||||
import com.gzzn.omms.msgexchange.domain.EventType
|
import com.gzzn.omms.msgexchange.domain.EventType
|
||||||
@@ -818,6 +819,7 @@ class JdbcFlightStateRepository(
|
|||||||
class JdbcSnapshotLogRepository(
|
class JdbcSnapshotLogRepository(
|
||||||
private val ds: DataSource,
|
private val ds: DataSource,
|
||||||
private val clock: Clock,
|
private val clock: Clock,
|
||||||
|
private val operationDayProps: OperationDayProps,
|
||||||
) : SnapshotLogRepository, SnapshotLogPurge {
|
) : SnapshotLogRepository, SnapshotLogPurge {
|
||||||
/** 只追加写;这里不抛异常,写失败由调用方捕获并记为指标。 */
|
/** 只追加写;这里不抛异常,写失败由调用方捕获并记为指标。 */
|
||||||
override fun append(entry: SnapshotLogEntry) {
|
override fun append(entry: SnapshotLogEntry) {
|
||||||
@@ -847,7 +849,7 @@ class JdbcSnapshotLogRepository(
|
|||||||
ds.update(
|
ds.update(
|
||||||
"DELETE FROM schd_snap_log WHERE scope_end < ? AND recv_at < ?",
|
"DELETE FROM schd_snap_log WHERE scope_end < ? AND recv_at < ?",
|
||||||
{ ps ->
|
{ ps ->
|
||||||
val cutoffDay = java.time.LocalDate.ofInstant(instant, java.time.ZoneId.of("Asia/Shanghai"))
|
val cutoffDay = java.time.LocalDate.ofInstant(instant, operationDayProps.zoneId())
|
||||||
ps.setDate(1, java.sql.Date.valueOf(cutoffDay))
|
ps.setDate(1, java.sql.Date.valueOf(cutoffDay))
|
||||||
ps.setTimestamp(2, instant.toSqlTimestamp())
|
ps.setTimestamp(2, instant.toSqlTimestamp())
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.gzzn.omms.msgexchange.jobs
|
package com.gzzn.omms.msgexchange.jobs
|
||||||
|
|
||||||
import com.gzzn.omms.msgexchange.config.HistoryProps
|
import com.gzzn.omms.msgexchange.config.HistoryProps
|
||||||
|
import com.gzzn.omms.msgexchange.config.OperationDayProps
|
||||||
import com.gzzn.omms.msgexchange.domain.EventType
|
import com.gzzn.omms.msgexchange.domain.EventType
|
||||||
import com.gzzn.omms.msgexchange.domain.MsgEvent
|
import com.gzzn.omms.msgexchange.domain.MsgEvent
|
||||||
import com.gzzn.omms.msgexchange.domain.Targets
|
import com.gzzn.omms.msgexchange.domain.Targets
|
||||||
@@ -12,7 +13,6 @@ import com.gzzn.omms.msgexchange.infra.persistence.SnapshotLogPurge
|
|||||||
import jakarta.inject.Singleton
|
import jakarta.inject.Singleton
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.time.ZoneId
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 历史归档与物理清除:把到期航班搬进历史存储,再从当前态删掉。五步顺序不能颠倒:
|
* 历史归档与物理清除:把到期航班搬进历史存储,再从当前态删掉。五步顺序不能颠倒:
|
||||||
@@ -31,6 +31,7 @@ class HistorySweepJob(
|
|||||||
private val flightState: FlightStateRepository,
|
private val flightState: FlightStateRepository,
|
||||||
private val msgEvents: MsgEventRepository,
|
private val msgEvents: MsgEventRepository,
|
||||||
private val props: HistoryProps,
|
private val props: HistoryProps,
|
||||||
|
private val operationDayProps: OperationDayProps,
|
||||||
/** 历史存储端口:返回确认归档成功的 FLID 集合;部署侧没接通时是 null。 */
|
/** 历史存储端口:返回确认归档成功的 FLID 集合;部署侧没接通时是 null。 */
|
||||||
private val historyStore: HistoryStore? = null,
|
private val historyStore: HistoryStore? = null,
|
||||||
/** 留痕清理端口:删掉超过保留期(默认 90 天)的日计划留痕行;不依赖历史存储开关,没接通时为 null。 */
|
/** 留痕清理端口:删掉超过保留期(默认 90 天)的日计划留痕行;不依赖历史存储开关,没接通时为 null。 */
|
||||||
@@ -54,7 +55,7 @@ class HistorySweepJob(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val rules = HistoryRules(props.cancelledHours, props.terminalHours, props.deletedHours, props.idleHours)
|
val rules = HistoryRules(props.cancelledHours, props.terminalHours, props.deletedHours, props.idleHours)
|
||||||
val zone = ZoneId.of("Asia/Shanghai") // 保留期窗口按机场时区算,不用 UTC
|
val zone = operationDayProps.zoneId() // 保留期窗口按 PARAM:msgx.operation-day.zone 算,不用 UTC
|
||||||
val candidates = flightState.findHistoryCandidates(rules, zone, now)
|
val candidates = flightState.findHistoryCandidates(rules, zone, now)
|
||||||
if (candidates.isEmpty()) return SweepOutcome(0, 0, 0, snapLogPurged = snapLogPurged)
|
if (candidates.isEmpty()) return SweepOutcome(0, 0, 0, snapLogPurged = snapLogPurged)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.gzzn.omms.msgexchange.jobs
|
package com.gzzn.omms.msgexchange.jobs
|
||||||
|
|
||||||
|
import com.gzzn.omms.msgexchange.config.OperationDayProps
|
||||||
import com.gzzn.omms.msgexchange.processing.BackfillService
|
import com.gzzn.omms.msgexchange.processing.BackfillService
|
||||||
import jakarta.inject.Singleton
|
import jakarta.inject.Singleton
|
||||||
import java.time.Clock
|
import java.time.Clock
|
||||||
@@ -22,9 +23,10 @@ class JobRunner(
|
|||||||
private val backfill: BackfillService,
|
private val backfill: BackfillService,
|
||||||
private val historySweep: HistorySweepJob,
|
private val historySweep: HistorySweepJob,
|
||||||
private val clock: Clock,
|
private val clock: Clock,
|
||||||
|
operationDayProps: OperationDayProps,
|
||||||
) {
|
) {
|
||||||
private val log = org.slf4j.LoggerFactory.getLogger(JobRunner::class.java)
|
private val log = org.slf4j.LoggerFactory.getLogger(JobRunner::class.java)
|
||||||
private val zone: ZoneId = ZoneId.of("Asia/Shanghai")
|
private val zone: ZoneId = operationDayProps.zoneId()
|
||||||
|
|
||||||
@Volatile
|
@Volatile
|
||||||
private var running = true
|
private var running = true
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ class AdftProcessor(
|
|||||||
private val clock: Clock,
|
private val clock: Clock,
|
||||||
) {
|
) {
|
||||||
private val opDay = OperationDayCalculator(
|
private val opDay = OperationDayCalculator(
|
||||||
zone = runCatching { ZoneId.of(operationDayProps.zone) }.getOrElse { ZoneId.of("Asia/Shanghai") },
|
zone = operationDayProps.zoneId(),
|
||||||
cutoffHour = operationDayProps.cutoffHour,
|
cutoffHour = operationDayProps.cutoffHour,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ class ScheduleProcessor(
|
|||||||
private val mapper: ObjectMapper,
|
private val mapper: ObjectMapper,
|
||||||
private val clock: Clock,
|
private val clock: Clock,
|
||||||
) {
|
) {
|
||||||
private val zone: ZoneId = runCatching { ZoneId.of(operationDayProps.zone) }.getOrElse { ZoneId.of("Asia/Shanghai") }
|
private val zone: ZoneId = operationDayProps.zoneId()
|
||||||
private val opDay = OperationDayCalculator(
|
private val opDay = OperationDayCalculator(
|
||||||
zone = zone,
|
zone = zone,
|
||||||
cutoffHour = operationDayProps.cutoffHour,
|
cutoffHour = operationDayProps.cutoffHour,
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package com.gzzn.omms.msgexchange.config
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Assertions.assertThrows
|
||||||
|
import org.junit.jupiter.api.Assertions.assertTrue
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import java.time.ZoneId
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运营日时区只有一个出处(`PARAM:msgx.operation-day.zone`):能解析就解析,
|
||||||
|
* 解析不了必须让启动失败,**不允许回退到代码字面量**——回退会把配置错位藏起来,
|
||||||
|
* 让运营日按错误的时区计算。
|
||||||
|
*/
|
||||||
|
class OperationDayPropsTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `configured zone resolves to the IANA zone`() {
|
||||||
|
val props = OperationDayProps().apply { zone = "Asia/Shanghai" }
|
||||||
|
|
||||||
|
assertEquals(ZoneId.of("Asia/Shanghai"), props.zoneId())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `invalid zone fails fast instead of falling back to a literal`() {
|
||||||
|
val props = OperationDayProps().apply { zone = "Not/AZone" }
|
||||||
|
|
||||||
|
val failure = assertThrows(IllegalStateException::class.java) { props.zoneId() }
|
||||||
|
|
||||||
|
assertTrue(failure.message!!.contains("msgx.operation-day.zone"))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.gzzn.omms.msgexchange.jobs
|
package com.gzzn.omms.msgexchange.jobs
|
||||||
|
|
||||||
import com.gzzn.omms.msgexchange.config.HistoryProps
|
import com.gzzn.omms.msgexchange.config.HistoryProps
|
||||||
|
import com.gzzn.omms.msgexchange.config.OperationDayProps
|
||||||
import com.gzzn.omms.msgexchange.domain.EventType
|
import com.gzzn.omms.msgexchange.domain.EventType
|
||||||
import com.gzzn.omms.msgexchange.domain.flight.FlightState
|
import com.gzzn.omms.msgexchange.domain.flight.FlightState
|
||||||
import com.gzzn.omms.msgexchange.domain.flight.HistoryCandidate
|
import com.gzzn.omms.msgexchange.domain.flight.HistoryCandidate
|
||||||
@@ -50,7 +51,7 @@ class HistorySweepJobTest {
|
|||||||
fun `history store not connected must delete zero rows`() {
|
fun `history store not connected must delete zero rows`() {
|
||||||
val f = seededFlight("F1", deleted = true, idleDays = 30)
|
val f = seededFlight("F1", deleted = true, idleDays = 30)
|
||||||
val events = StubMsgEvents()
|
val events = StubMsgEvents()
|
||||||
val job = HistorySweepJob(f, events, HistoryProps().apply { historyStoreEnabled = false })
|
val job = HistorySweepJob(f, events, HistoryProps().apply { historyStoreEnabled = false }, OperationDayProps())
|
||||||
|
|
||||||
val outcome = job.run(now)
|
val outcome = job.run(now)
|
||||||
|
|
||||||
@@ -71,7 +72,7 @@ class HistorySweepJobTest {
|
|||||||
val events = StubMsgEvents()
|
val events = StubMsgEvents()
|
||||||
val store = RecordingHistoryStore().apply { confirmed.add("F1") } // 只有 F1 归档确认成功
|
val store = RecordingHistoryStore().apply { confirmed.add("F1") } // 只有 F1 归档确认成功
|
||||||
val job = HistorySweepJob(
|
val job = HistorySweepJob(
|
||||||
f, events, HistoryProps().apply { historyStoreEnabled = true }, historyStore = store,
|
f, events, HistoryProps().apply { historyStoreEnabled = true }, OperationDayProps(), historyStore = store,
|
||||||
)
|
)
|
||||||
|
|
||||||
val outcome = job.run(now)
|
val outcome = job.run(now)
|
||||||
@@ -90,7 +91,7 @@ class HistorySweepJobTest {
|
|||||||
val events = StubMsgEvents()
|
val events = StubMsgEvents()
|
||||||
val store = RecordingHistoryStore()
|
val store = RecordingHistoryStore()
|
||||||
val job = HistorySweepJob(
|
val job = HistorySweepJob(
|
||||||
f, events, HistoryProps().apply { historyStoreEnabled = true }, historyStore = store,
|
f, events, HistoryProps().apply { historyStoreEnabled = true }, OperationDayProps(), historyStore = store,
|
||||||
)
|
)
|
||||||
|
|
||||||
job.run(now)
|
job.run(now)
|
||||||
@@ -108,7 +109,7 @@ class HistorySweepJobTest {
|
|||||||
var cutoff: Instant? = null
|
var cutoff: Instant? = null
|
||||||
val purge = com.gzzn.omms.msgexchange.infra.persistence.SnapshotLogPurge { instant -> cutoff = instant; 7 }
|
val purge = com.gzzn.omms.msgexchange.infra.persistence.SnapshotLogPurge { instant -> cutoff = instant; 7 }
|
||||||
val job = HistorySweepJob(
|
val job = HistorySweepJob(
|
||||||
f, StubMsgEvents(), HistoryProps().apply { historyStoreEnabled = false }, snapLogPurge = purge,
|
f, StubMsgEvents(), HistoryProps().apply { historyStoreEnabled = false }, OperationDayProps(), snapLogPurge = purge,
|
||||||
)
|
)
|
||||||
|
|
||||||
val outcome = job.run(now)
|
val outcome = job.run(now)
|
||||||
|
|||||||
Reference in New Issue
Block a user