feat(ingress): 实现 JDBC 信箱轮询、入站持久化与对拍测试 (U05)
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
package com.gzzn.omms.msgexchange.infra.retry
|
||||
|
||||
import com.gzzn.omms.msgexchange.config.PipelineProps
|
||||
import io.micronaut.context.annotation.Factory
|
||||
import jakarta.inject.Singleton
|
||||
import java.time.Clock
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
* 统一重试策略(ACM2-10 U08):供 MessageProcessor / SnapshotFlow(ProcState 侧)与
|
||||
* Dispatcher(MsgEvent 侧)共用——attempts 递增后按 backoff 表给 nextAttemptAt;
|
||||
* exhausted 判定与两侧同源(maxAttempts)。时间一律经可注入 Clock(测试用固定钟,避免脆弱睡眠)。
|
||||
*/
|
||||
@Singleton
|
||||
class FailureScheduler(
|
||||
private val props: PipelineProps,
|
||||
private val clock: Clock,
|
||||
) {
|
||||
/** 便捷构造:默认系统时钟(生产路径)。 */
|
||||
constructor(props: PipelineProps) : this(props, Clock.systemUTC())
|
||||
|
||||
fun now(): Instant = clock.instant()
|
||||
|
||||
fun exhausted(attempts: Int): Boolean = attempts >= props.pipeline.maxAttempts
|
||||
|
||||
/** attempts 指递增后的值;N28:attempt ≤ 0 由 backoffFor 兜底为首档。 */
|
||||
fun nextAttemptAt(attemptsAfterIncrement: Int): Instant =
|
||||
now().plusMillis(props.pipeline.backoffFor(attemptsAfterIncrement))
|
||||
}
|
||||
|
||||
/** 提供可注入 Clock(java.time.Clock);测试可用 Clock.fixed(...) 或自定义可变钟覆盖。 */
|
||||
@Factory
|
||||
class TimeFactory {
|
||||
@Singleton
|
||||
fun systemClock(): Clock = Clock.systemUTC()
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.gzzn.omms.msgexchange.infra.retry
|
||||
|
||||
import com.gzzn.omms.msgexchange.domain.ErrorClass
|
||||
import com.gzzn.omms.msgexchange.domain.ProcState
|
||||
import com.gzzn.omms.msgexchange.domain.ProcStatus
|
||||
import com.gzzn.omms.msgexchange.infra.persistence.ProcStateRepository
|
||||
import jakarta.inject.Singleton
|
||||
|
||||
/**
|
||||
* ProcState 侧统一失败迁移(U08/U10):处理/快照路径共用——
|
||||
* attempts+1 后若 exhausted → DEAD(EXHAUSTED)(终态,errorClass 规范化,原因保留在 lastError);
|
||||
* 否则 FAILED + attempts + nextAttemptAt(退避)(可重放)。任何“失败”都不得在无退避下直接终态化。
|
||||
*/
|
||||
@Singleton
|
||||
class ProcFailure(
|
||||
private val procState: ProcStateRepository,
|
||||
val scheduler: FailureScheduler,
|
||||
) {
|
||||
fun fail(head: ProcState, ec: ErrorClass, reason: String) {
|
||||
val attempts = head.attempts + 1
|
||||
if (scheduler.exhausted(attempts)) {
|
||||
procState.update(
|
||||
head.cminmsgsId, ProcStatus.DEAD,
|
||||
attempts = attempts,
|
||||
errorClass = ErrorClass.EXHAUSTED,
|
||||
lastError = "$reason; attempts=$attempts",
|
||||
)
|
||||
} else {
|
||||
procState.update(
|
||||
head.cminmsgsId, ProcStatus.FAILED,
|
||||
attempts = attempts,
|
||||
nextAttemptAt = scheduler.nextAttemptAt(attempts),
|
||||
errorClass = ec,
|
||||
lastError = reason,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.gzzn.omms.msgexchange.infra.retry
|
||||
|
||||
import com.gzzn.omms.msgexchange.domain.ErrorClass
|
||||
import com.gzzn.omms.msgexchange.infra.persistence.ProcStateRepository
|
||||
import jakarta.inject.Singleton
|
||||
|
||||
/**
|
||||
* U11 显式重放入口:只允许“可恢复”的错误类从 FAILED/DEAD 回到 PENDING(主泵重领)。
|
||||
* 不可恢复类(MALFORMED——报文非法,重放必再失败)与未知类一律不在白名单内。
|
||||
*/
|
||||
@Singleton
|
||||
class ReplayService(
|
||||
private val procState: ProcStateRepository,
|
||||
) {
|
||||
private val log = org.slf4j.LoggerFactory.getLogger(ReplayService::class.java)
|
||||
/** 可恢复错误类:codec 修复可重放 / 未实装补齐可重放 / 基础设施抖动可重放 / 重试耗尽后人工复核可重放。 */
|
||||
val replayableErrorClasses: Set<ErrorClass> =
|
||||
setOf(ErrorClass.CODEC_ERROR, ErrorClass.UNSUPPORTED, ErrorClass.INFRA, ErrorClass.EXHAUSTED)
|
||||
|
||||
/** 只重放白名单内的类;请求含 MALFORMED 等非法类时静默忽略该类。 */
|
||||
fun replay(requested: Collection<ErrorClass>): Int {
|
||||
val allowed = requested.filter { it in replayableErrorClasses }
|
||||
if (allowed.isEmpty()) {
|
||||
log.warn("replay requested only non-replayable classes: {}", requested)
|
||||
return 0
|
||||
}
|
||||
val n = procState.requeueByErrorClasses(allowed)
|
||||
log.info("replayed rows={} classes={}", n, allowed)
|
||||
return n
|
||||
}
|
||||
|
||||
/** 默认入口:重放全部可恢复类。 */
|
||||
fun replayAll(): Int = replay(replayableErrorClasses)
|
||||
}
|
||||
Reference in New Issue
Block a user