test(jobs): verify purge rollback on real postgres
新增 HistorySweepPurgePgTest(Testcontainers 本地 PG):归档确认后删除阶段注入失败,验证同事务登记的 tombstone 随事务回滚、当前态保留(INV-17/INV-18)。
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
package com.gzzn.omms.msgexchange.jobs
|
||||
|
||||
import com.gzzn.omms.msgexchange.config.HistoryProps
|
||||
import com.gzzn.omms.msgexchange.config.OperationDayProps
|
||||
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.infra.persistence.FlightStateRepository
|
||||
import com.gzzn.omms.msgexchange.infra.persistence.jdbc.JdbcFlightStateRepository
|
||||
import com.gzzn.omms.msgexchange.infra.persistence.jdbc.JdbcMsgEventRepository
|
||||
import com.gzzn.omms.msgexchange.infra.persistence.jdbc.JdbcPipelineLockRepository
|
||||
import com.gzzn.omms.msgexchange.infra.persistence.jdbc.JdbcPipelineTransactionManager
|
||||
import com.gzzn.omms.msgexchange.support.PgTestSupport
|
||||
import com.zaxxer.hikari.HikariConfig
|
||||
import com.zaxxer.hikari.HikariDataSource
|
||||
import org.flywaydb.core.Flyway
|
||||
import org.junit.jupiter.api.Assertions.assertNotNull
|
||||
import org.junit.jupiter.api.Assertions.assertThrows
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Assumptions.assumeTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.time.Clock
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.time.ZoneOffset
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* 在真实 PostgreSQL 上验证历史清退的原子性:归档确认后若在删当前态之前失败,
|
||||
* 同事务登记的 tombstone 必须一起回滚(`INV-17`、`INV-18`)。
|
||||
*/
|
||||
class HistorySweepPurgePgTest {
|
||||
|
||||
private val t0: Instant = Instant.parse("2026-09-12T00:00:00Z")
|
||||
|
||||
@Test
|
||||
fun `a failure after the tombstone insert rolls back the whole purge transaction`() {
|
||||
val ds = dataSource()
|
||||
val clock = Clock.fixed(t0, ZoneOffset.UTC)
|
||||
val flights = JdbcFlightStateRepository(ds, clock)
|
||||
val events = JdbcMsgEventRepository(ds, clock)
|
||||
val flid = "PG-" + UUID.randomUUID().toString().take(8)
|
||||
flights.persistFullState(
|
||||
FlightSnapshot(flid, LocalDate.of(2026, 8, 1), FlightState.ACTIVE, 1, emptyMap(), emptyMap()),
|
||||
msgId = 1,
|
||||
now = t0.minusSeconds(30L * 86400), // 静默超过 idle-hours,命中生命周期清退
|
||||
)
|
||||
|
||||
// 删除阶段注入失败:tombstone 已登记,删除抛错 → 整个事务必须回滚。
|
||||
val failing = object : FlightStateRepository by flights {
|
||||
override fun purgeArchived(candidates: List<HistoryCandidate>): Int =
|
||||
throw IllegalStateException("purge-failed")
|
||||
}
|
||||
val job = HistorySweepJob(
|
||||
failing, events, HistoryProps().apply { historyStoreEnabled = true },
|
||||
OperationDayProps(), JdbcPipelineTransactionManager(ds), JdbcPipelineLockRepository(ds),
|
||||
historyStore = { setOf(flid) },
|
||||
)
|
||||
|
||||
assertThrows(IllegalStateException::class.java) { job.run(t0) }
|
||||
|
||||
assertNotNull(flights.findMainRow(flid), "当前态不得被删")
|
||||
assertTrue(
|
||||
events.mergePendingSchd(t0, 1000).none { it.partitionKey == flid },
|
||||
"tombstone 必须随事务回滚,不留半状态",
|
||||
)
|
||||
}
|
||||
|
||||
private fun dataSource(): javax.sql.DataSource {
|
||||
assumeTrue(PgTestSupport.canConnect(), PgTestSupport.skipMessage())
|
||||
Flyway.configure()
|
||||
.dataSource(PgTestSupport.jdbcUrl, PgTestSupport.user, PgTestSupport.password)
|
||||
.locations("classpath:db/migration")
|
||||
.load()
|
||||
.migrate()
|
||||
return HikariDataSource(
|
||||
HikariConfig().apply {
|
||||
jdbcUrl = PgTestSupport.jdbcUrl
|
||||
username = PgTestSupport.user
|
||||
password = PgTestSupport.password
|
||||
maximumPoolSize = 2
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user