2026-09-07 15:11:33 +08:00
|
|
|
|
package com.gzzn.omms.msgexchange.delivery
|
2026-09-06 16:13:50 +08:00
|
|
|
|
|
2026-09-06 22:12:23 +08:00
|
|
|
|
import com.fasterxml.jackson.databind.ObjectMapper
|
2026-09-07 15:11:33 +08:00
|
|
|
|
import com.gzzn.omms.msgexchange.config.PipelineProps
|
|
|
|
|
|
import com.gzzn.omms.msgexchange.domain.ErrorClass
|
|
|
|
|
|
import com.gzzn.omms.msgexchange.domain.EventStatus
|
|
|
|
|
|
import com.gzzn.omms.msgexchange.domain.MsgEvent
|
|
|
|
|
|
import com.gzzn.omms.msgexchange.domain.Targets
|
|
|
|
|
|
import com.gzzn.omms.msgexchange.infra.persistence.MsgEventRepository
|
|
|
|
|
|
import com.gzzn.omms.msgexchange.infra.retry.FailureScheduler
|
2026-09-06 16:13:50 +08:00
|
|
|
|
import jakarta.inject.Singleton
|
|
|
|
|
|
import java.time.Duration
|
|
|
|
|
|
import java.time.Instant
|
|
|
|
|
|
|
|
|
|
|
|
/** 对外投递端口(Kafka 同步确认;阶段 B 追加 ES/Redis 投影写入)。 */
|
|
|
|
|
|
interface DeliveryPort {
|
|
|
|
|
|
/** Kafka 发送(同步确认,at-least-once)。topic 由 target 映射:KAFKA:msg→"msg",KAFKA:schd→"schd"。 */
|
|
|
|
|
|
fun sendKafka(topic: String, payloadJson: String)
|
|
|
|
|
|
|
|
|
|
|
|
/** 阶段 B:ES flight_hts 写入。 */
|
|
|
|
|
|
fun indexFlightHts(payloadJson: String)
|
|
|
|
|
|
|
2026-09-06 22:12:23 +08:00
|
|
|
|
|
|
|
|
|
|
/** 连通性探测(健康检查用);默认 true,真实 Kafka 实装时覆写为 producer metadata 校验。 */
|
|
|
|
|
|
fun ping(): Boolean = true
|
2026-09-06 16:13:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-09-06 19:08:02 +08:00
|
|
|
|
* ACMA-8 流程 3:投递调度——每 target 严格 FIFO(I1 双层同策略)。
|
|
|
|
|
|
* N03(U06):KAFKA_SCHD 不走逐条循环(唯一出口是 flushSchd 批量聚合),逐条循环显式排除。
|
|
|
|
|
|
* U08 闭环:逐条与批量失败都在持有具体事件/批次的边界完成状态迁移——
|
|
|
|
|
|
* · 逐条:scheduleRetry(attempts+1, backoff) / 达上限 markDead(EXHAUSTED)(DLQ 保留行);
|
|
|
|
|
|
* · 批量(flushSchd):整批退避,队首 nextAttemptAt 未到不 claim;达上限整批 DEAD/DLQ;
|
|
|
|
|
|
* · loop 只作最后防线,不吞 InterruptedException(致命/中断错误不被普通恢复吞掉)。
|
2026-09-06 16:13:50 +08:00
|
|
|
|
*/
|
|
|
|
|
|
@Singleton
|
|
|
|
|
|
class Dispatcher(
|
|
|
|
|
|
private val msgEvents: MsgEventRepository,
|
|
|
|
|
|
private val port: DeliveryPort,
|
|
|
|
|
|
private val props: PipelineProps,
|
2026-09-06 19:08:02 +08:00
|
|
|
|
private val scheduler: FailureScheduler,
|
2026-09-06 16:13:50 +08:00
|
|
|
|
) {
|
2026-09-06 21:02:27 +08:00
|
|
|
|
private val log = org.slf4j.LoggerFactory.getLogger(Dispatcher::class.java)
|
|
|
|
|
|
|
2026-09-06 16:13:50 +08:00
|
|
|
|
@Volatile
|
|
|
|
|
|
private var running = true
|
|
|
|
|
|
|
|
|
|
|
|
private var lastFlush: Instant = Instant.EPOCH
|
|
|
|
|
|
|
2026-09-06 20:53:14 +08:00
|
|
|
|
/** 优雅停机:loop 收尾后退出;线程中断由 Runner 负责。 */
|
|
|
|
|
|
fun stop() {
|
|
|
|
|
|
running = false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-06 16:13:50 +08:00
|
|
|
|
fun loop() {
|
|
|
|
|
|
while (running) {
|
2026-09-06 18:19:04 +08:00
|
|
|
|
try {
|
|
|
|
|
|
tick()
|
2026-09-06 19:08:02 +08:00
|
|
|
|
} catch (e: InterruptedException) {
|
|
|
|
|
|
Thread.currentThread().interrupt()
|
|
|
|
|
|
return
|
2026-09-06 18:19:04 +08:00
|
|
|
|
} catch (e: Exception) {
|
2026-09-06 19:08:02 +08:00
|
|
|
|
// U08:投递循环只作最后防线;致命 Error 不吞。tick 内失败迁移已完成。
|
2026-09-06 18:19:04 +08:00
|
|
|
|
sleepQuietly(props.pipeline.pollInterval)
|
|
|
|
|
|
}
|
|
|
|
|
|
// N18:轮询间隔取参数表(下限 50ms,避免退避节律被吞)
|
|
|
|
|
|
sleepQuietly(props.pipeline.pollInterval.coerceAtLeast(Duration.ofMillis(50)))
|
2026-09-06 16:13:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
internal fun tick() {
|
|
|
|
|
|
val targets = if (props.phase == PipelineProps.Phase.A) Targets.phaseA else Targets.phaseB
|
|
|
|
|
|
for (t in targets) {
|
2026-09-06 18:19:04 +08:00
|
|
|
|
if (t == Targets.KAFKA_SCHD) continue // N03:schd 唯一出口 flushSchd
|
2026-09-06 16:13:50 +08:00
|
|
|
|
val head = msgEvents.headUnsent(t) ?: continue
|
2026-09-06 19:08:02 +08:00
|
|
|
|
if (head.state == EventStatus.PENDING && head.nextAttemptAt != null && head.nextAttemptAt > scheduler.now()) {
|
2026-09-06 18:19:04 +08:00
|
|
|
|
// 队头退避未到期:等待,不跳过(保序);超时升级归 U13(WP2)
|
2026-09-06 16:13:50 +08:00
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
|
|
|
deliver(t, head)
|
|
|
|
|
|
msgEvents.markSent(head.eventId!!)
|
2026-09-06 21:02:27 +08:00
|
|
|
|
log.debug("sent target={} eventId={}", t, head.eventId)
|
2026-09-07 16:12:00 +08:00
|
|
|
|
// ACM2-28:阶段 B Redis 投影废弃,读模型投递统一转由 Kafka 事件或 ES 处理
|
2026-09-06 16:13:50 +08:00
|
|
|
|
} catch (e: Exception) {
|
2026-09-06 19:08:02 +08:00
|
|
|
|
retryOrDead(head, e.message ?: "unknown")
|
2026-09-06 16:13:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-09-06 18:19:04 +08:00
|
|
|
|
if (flushDue()) {
|
2026-09-06 19:08:02 +08:00
|
|
|
|
flushSchd()
|
2026-09-06 16:13:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-06 18:19:04 +08:00
|
|
|
|
private fun flushDue(): Boolean =
|
2026-09-06 19:08:02 +08:00
|
|
|
|
Duration.between(lastFlush, scheduler.now()) >= props.schd.flushPeriod
|
|
|
|
|
|
|
|
|
|
|
|
/** 单条事件失败迁移:attempts+1;达上限 DEAD(EXHAUSTED)(DLQ,attempts 落库审计),否则退避重试。 */
|
|
|
|
|
|
private fun retryOrDead(e: MsgEvent, lastError: String) {
|
|
|
|
|
|
val attempts = e.attempts + 1
|
|
|
|
|
|
if (scheduler.exhausted(attempts)) {
|
2026-09-06 21:02:27 +08:00
|
|
|
|
log.error("event DEAD(DLQ) eventId={} attempts={} lastError={}", e.eventId, attempts, lastError)
|
2026-09-06 19:08:02 +08:00
|
|
|
|
msgEvents.markDead(e.eventId!!, ErrorClass.EXHAUSTED, lastError, attempts)
|
|
|
|
|
|
} else {
|
2026-09-06 21:02:27 +08:00
|
|
|
|
log.warn("event retry scheduled eventId={} attempts={} nextAttemptAt={}", e.eventId, attempts, scheduler.nextAttemptAt(attempts))
|
2026-09-06 19:08:02 +08:00
|
|
|
|
msgEvents.scheduleRetry(e.eventId!!, scheduler.nextAttemptAt(attempts), attempts)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-09-06 18:19:04 +08:00
|
|
|
|
|
2026-09-06 16:13:50 +08:00
|
|
|
|
private fun deliver(target: String, e: MsgEvent) = when (target) {
|
|
|
|
|
|
Targets.KAFKA_MSG -> port.sendKafka("msg", e.payloadJson)
|
2026-09-06 18:19:04 +08:00
|
|
|
|
Targets.KAFKA_SCHD -> error("KAFKA_SCHD must go through flushSchd (N03)")
|
2026-09-06 16:13:50 +08:00
|
|
|
|
Targets.ES_FLIGHT_HTS -> port.indexFlightHts(e.payloadJson)
|
|
|
|
|
|
else -> error("unknown target $target")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-06 19:08:02 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 流程 3 flushSchd:批上限 + 每 FLID 最新一态聚合(topic "schd",wire=FLTR JSON 数组)。
|
|
|
|
|
|
* U08 批量闭环:队首退避未到期不 claim;发送失败 → 整批 attempts+1(退避)或达上限整批 DEAD/DLQ;
|
|
|
|
|
|
* lastFlush 仅在成功(含空批)后推进。
|
|
|
|
|
|
*/
|
2026-09-06 16:13:50 +08:00
|
|
|
|
internal fun flushSchd() {
|
|
|
|
|
|
val pending = msgEvents.claimBatch(Targets.KAFKA_SCHD, limit = props.schd.flushLimit)
|
2026-09-06 18:19:04 +08:00
|
|
|
|
if (pending.isEmpty()) {
|
2026-09-06 19:08:02 +08:00
|
|
|
|
lastFlush = scheduler.now()
|
2026-09-06 18:19:04 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-09-06 19:08:02 +08:00
|
|
|
|
val due = pending.first()
|
|
|
|
|
|
if (due.nextAttemptAt != null && due.nextAttemptAt > scheduler.now()) {
|
|
|
|
|
|
return // 队首仍在退避:整批等待,不推进 lastFlush(到期再试)
|
|
|
|
|
|
}
|
2026-09-06 16:13:50 +08:00
|
|
|
|
val payload = SchdAggregation.latestPerFlight(pending).joinToString(",", "[", "]")
|
2026-09-06 21:02:27 +08:00
|
|
|
|
log.debug("flushSchd batch size={} payloadLen={}", pending.size, payload.length)
|
2026-09-06 19:08:02 +08:00
|
|
|
|
try {
|
|
|
|
|
|
port.sendKafka("schd", payload)
|
|
|
|
|
|
} catch (e: Exception) {
|
2026-09-06 21:02:27 +08:00
|
|
|
|
log.warn("flushSchd send failed batch={} -> batch retryOrDead: {}", pending.size, e.message)
|
2026-09-06 19:08:02 +08:00
|
|
|
|
pending.forEach { retryOrDead(it, "schd-send: ${e.message ?: "unknown"}") }
|
|
|
|
|
|
return // 不推进 lastFlush:整批退避(含 DEAD 出队)后到期重试
|
|
|
|
|
|
}
|
2026-09-06 16:13:50 +08:00
|
|
|
|
msgEvents.markAllSent(pending.mapNotNull { it.eventId })
|
2026-09-06 21:02:27 +08:00
|
|
|
|
log.info("flushSchd sent batch={}", pending.size)
|
2026-09-06 19:08:02 +08:00
|
|
|
|
lastFlush = scheduler.now()
|
2026-09-06 18:19:04 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private fun sleepQuietly(d: Duration) {
|
|
|
|
|
|
if (!d.isNegative && !d.isZero) Thread.sleep(d.toMillis().coerceAtLeast(1))
|
2026-09-06 16:13:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|