fix(ingress): preserve compat receipt semantics
接收结果边界改为 insertRaw 是否返回 ID:接收时间读取失败回退注入 Clock 并留痕,PG insertIfAbsent 失败只记日志与 pgEnqueueFailures 计数,不再冒泡成 HTTP 失败(避免客户端重试多写信箱行)。缺的 PROC_STATE 由收报轮询按 US-01 补建且只建一条(INV-1),入口高 ID 仍等水位追平(INV-4)。 验证:./gradlew test 135 tests / 0 fail / 0 skipped(新增 InboxServiceTest 4 例:读时间失败+入队失败仍返回同一 ID、接收时间 NULL 回退时钟、真实插入失败仍失败且无伪造 ID、轮询对失败入队补建且仅一条)。
This commit is contained in:
@@ -5,6 +5,7 @@ import com.gzzn.omms.msgexchange.infra.persistence.ProcStateRepository
|
||||
import jakarta.inject.Singleton
|
||||
import java.time.Clock
|
||||
import java.time.Instant
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
|
||||
/**
|
||||
* 兼容 HTTP 入口:把一条报文写进共享信箱,再在自有 PG 里入队,供联调和影子对拍使用。
|
||||
@@ -24,21 +25,34 @@ class InboxService(
|
||||
) {
|
||||
private val log = org.slf4j.LoggerFactory.getLogger(InboxService::class.java)
|
||||
|
||||
/** 落信成功但 PG 入队失败的次数:由收报轮询按 `US-01` 补建,这里只做可观测计数。 */
|
||||
val pgEnqueueFailures = AtomicLong(0)
|
||||
|
||||
data class Receipt(val msgId: Long, val receivedAt: Instant)
|
||||
|
||||
fun accept(rawXml: String): Receipt {
|
||||
// 接收结果边界 = insertRaw 是否成功返回 ID(C-28 只承诺到落信)。只有这一步失败才返回失败;
|
||||
// 之后的读取/入队失败若冒泡成 HTTP 失败,客户端重试会在信箱里多写一行。
|
||||
val id = inbox.insertRaw(rawXml)
|
||||
// 信箱接收时间缺失不能让"已经落信"的写入对外报失败(那会与"确认落信才返回 ID"的
|
||||
// 契约矛盾,并让客户端重试产生额外重复行)。回退到入队时间并留痕;
|
||||
// 时间源用注入的 Clock,保证与收报/回填一致、测试可确定。
|
||||
val mailboxReceivedAt = inbox.receivedAtOf(id)
|
||||
val now = clock.instant()
|
||||
val mailboxReceivedAt = try {
|
||||
inbox.receivedAtOf(id)
|
||||
} catch (e: Exception) {
|
||||
log.warn("mailbox receive time read failed msgId={}: {}", id, e.message)
|
||||
null
|
||||
}
|
||||
if (mailboxReceivedAt == null) {
|
||||
log.warn("mailbox receive time missing msgId={}, falling back to enqueue time", id)
|
||||
}
|
||||
val now = clock.instant()
|
||||
val receivedAt = mailboxReceivedAt ?: now
|
||||
// 入队时间单独传本地时钟:它是超期判据 R 的比较对象,不能借用库方时间(PRE-4)。
|
||||
procState.insertIfAbsent(id, receivedAt, enqueuedAt = now)
|
||||
try {
|
||||
// 入队时间单独传本地时钟:它是超期判据 R 的比较对象,不能借用库方时间(PRE-4)。
|
||||
procState.insertIfAbsent(id, receivedAt, enqueuedAt = now)
|
||||
} catch (e: Exception) {
|
||||
// PG 入队失败不回退接收结果:原文已在信箱,轮询会按 ID 补建,且只建一条(INV-1、US-01)。
|
||||
pgEnqueueFailures.incrementAndGet()
|
||||
log.error("compat accepted but PG enqueue failed msgId={}; poller will re-create", id, e)
|
||||
}
|
||||
log.info("compat-accepted msgId={}", id)
|
||||
return Receipt(id, receivedAt)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user