2026-09-07 15:11:33 +08:00
|
|
|
|
package com.gzzn.omms.msgexchange.config
|
2026-09-06 16:13:50 +08:00
|
|
|
|
|
|
|
|
|
|
import io.micronaut.context.annotation.ConfigurationProperties
|
|
|
|
|
|
import java.time.Duration
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-09-10 11:05:55 +08:00
|
|
|
|
* 管道运行参数,对应配置文件里的 `msgx.*`。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 注意:嵌套的配置类也必须标 `@ConfigurationProperties`,否则 Micronaut 不会绑定
|
|
|
|
|
|
* 里面的键,配置会静默失效、悄悄用回代码里的默认值。
|
2026-09-06 16:13:50 +08:00
|
|
|
|
*/
|
|
|
|
|
|
@ConfigurationProperties("msgx")
|
|
|
|
|
|
class PipelineProps {
|
|
|
|
|
|
var serviceName: String = "msgexchangeapi"
|
|
|
|
|
|
var registerEureka: Boolean = true
|
|
|
|
|
|
var pipeline: Pipeline = Pipeline()
|
|
|
|
|
|
var schd: Schd = Schd()
|
|
|
|
|
|
var identity: Identity = Identity()
|
|
|
|
|
|
|
2026-09-06 17:58:39 +08:00
|
|
|
|
@ConfigurationProperties("pipeline")
|
2026-09-06 16:13:50 +08:00
|
|
|
|
class Pipeline {
|
|
|
|
|
|
var pollInterval: Duration = Duration.ofSeconds(1) // KEEP 现役节奏
|
|
|
|
|
|
var claimBatch: Int = 50
|
|
|
|
|
|
var maxAttempts: Int = 5 // 处理/投递同值
|
2026-09-11 08:08:35 +08:00
|
|
|
|
// 档位数必须等于 max-attempts − 1:attempts 达到上限即转 DEAD,不再计算下次重试,
|
|
|
|
|
|
// 因此最多只用得到 max-attempts − 1 个档位。多出来的档位永远走不到(会被 validate() 拦下)。
|
|
|
|
|
|
var backoffMs: List<Long> = listOf(1000, 2000, 4000, 8000)
|
2026-09-06 16:13:50 +08:00
|
|
|
|
var backoffCapMs: Long = 60_000
|
|
|
|
|
|
|
2026-09-10 10:59:58 +08:00
|
|
|
|
/**
|
2026-09-10 11:05:55 +08:00
|
|
|
|
* 缺口等待时长:水位后面缺了一个 ID 时,等这么久还没出现就认定它永远不会来了,
|
|
|
|
|
|
* 跳过缺口继续推进水位。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 取值应该等于库方承诺的"上游提交到消息可见的最长时间"。设太小,可能把一条
|
|
|
|
|
|
* 迟到的消息误判成永久缺失,导致它排到后面的消息之后;设太大,收报会在缺口上白等。
|
2026-09-10 10:59:58 +08:00
|
|
|
|
*/
|
|
|
|
|
|
var maxCommitDelay: Duration = Duration.ofMinutes(5)
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-09-11 14:26:44 +08:00
|
|
|
|
* 超期补写期限 R:一条消息到达终态后,过了这么久处理标记仍未写回信箱
|
|
|
|
|
|
* (比如回填一直失败),扫描谓词的超期分支成立,无视退避强制补写(只会提前、从不推迟打标)。
|
2026-09-10 11:05:55 +08:00
|
|
|
|
*
|
2026-09-11 14:26:44 +08:00
|
|
|
|
* 唯一约束是 R ≤ R_keep,**不保护重放窗口**(打标时刻与 R 解耦,完整论证见
|
2026-09-11 20:44:30 +08:00
|
|
|
|
* design「回填」)。取值待 Q6 定案,确认之前不要为提速下调。
|
2026-09-10 10:59:58 +08:00
|
|
|
|
*/
|
|
|
|
|
|
var overdueBackfill: Duration = Duration.ofDays(30)
|
|
|
|
|
|
|
2026-09-10 11:05:55 +08:00
|
|
|
|
/** 每次回填扫描最多处理多少条。 */
|
2026-09-10 10:59:58 +08:00
|
|
|
|
var backfillBatch: Int = 100
|
|
|
|
|
|
|
2026-09-11 08:08:35 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 回填的自动重试次数上限。达到上限后**停止自动重试**(置为 abandoned 并告警),
|
|
|
|
|
|
* 但保留人工恢复能力——暂时性故障不该变成永久失去补偿,也不该永久占满扫描批次。
|
|
|
|
|
|
*/
|
|
|
|
|
|
var backfillMaxAttempts: Int = 100
|
|
|
|
|
|
|
2026-09-13 16:26:03 +08:00
|
|
|
|
/** 回填独立退避的起步间隔 `[G-BACKFILL-BACKOFF]`。 */
|
|
|
|
|
|
var backfillBackoffMs: Long = 30_000
|
|
|
|
|
|
|
|
|
|
|
|
/** 回填独立退避的封顶间隔 `[G-BACKFILL-BACKOFF]`。 */
|
|
|
|
|
|
var backfillBackoffCapMs: Long = 900_000
|
|
|
|
|
|
|
2026-09-11 08:08:35 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 切流水位播种(一次性、显式)。取值:
|
|
|
|
|
|
* `min` = `W:MIN(ID)−1`(读当前全部现存行)、`zero` = `W:0`(按空洞规则从 0 扫)、
|
|
|
|
|
|
* `max` = `W:MAX(ID)`(跳过当前可见存量)、或一个具体 ID。
|
|
|
|
|
|
*
|
|
|
|
|
|
* **默认 null = 不播种**,保持既有行为。是否跳过存量属于切流决策,必须由人显式配置:
|
|
|
|
|
|
* 代码不做默认选择,也不会自动退化成 `max`;升级实例(已有水位或已有处理记录)会拒绝重新播种。
|
|
|
|
|
|
*/
|
|
|
|
|
|
var cutoverWatermark: String? = null
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 普通事件(`KAFKA:msg`)每轮向一个目标领取的条数上限。
|
|
|
|
|
|
* 逐条领取会让投递吞吐被"每条一次 DB 往返 + 一轮一次 sleep"压到每秒 1 条。
|
|
|
|
|
|
*/
|
|
|
|
|
|
var deliveryBatch: Int = 200
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 每轮最多连续领取多少批,之后让出一次循环去跑 `schd` flush,
|
|
|
|
|
|
* 避免长积压把状态通知饿死。
|
|
|
|
|
|
*/
|
|
|
|
|
|
var deliveryDrainRounds: Int = 10
|
|
|
|
|
|
|
2026-09-10 11:14:11 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 服务启动后是否自动拉起收报、主泵、投递三个循环。
|
|
|
|
|
|
* 默认关闭:只有接了真实仓储、或者明确用内存 stub 跑的时候才安全。
|
|
|
|
|
|
*/
|
2026-09-06 20:53:14 +08:00
|
|
|
|
var autostart: Boolean = false
|
|
|
|
|
|
|
2026-09-13 15:48:16 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* MSG_EVENT 已确认投递行(`STATE='SENT'`)的保留期。
|
|
|
|
|
|
* 清理作业删除 `SENT_AT < now - eventRetention` 的行;
|
|
|
|
|
|
* 取值应大于最长可能的投递重试周期,避免误删刚发出的事件。
|
|
|
|
|
|
*/
|
|
|
|
|
|
var eventRetention: Duration = Duration.ofDays(7)
|
|
|
|
|
|
|
2026-09-06 18:19:04 +08:00
|
|
|
|
/** N28:attempt ≤ 0(如 FAILED 未递增 attempts 的行)不得抛异常,取下界=首档退避。 */
|
|
|
|
|
|
fun backoffFor(attempt: Int): Long {
|
|
|
|
|
|
val index = (attempt - 1).coerceAtLeast(0)
|
|
|
|
|
|
return backoffMs.getOrNull(index)?.coerceAtMost(backoffCapMs) ?: backoffCapMs
|
|
|
|
|
|
}
|
2026-09-11 08:08:35 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 配置自检:退避表档位数必须等于 `max-attempts − 1`。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 因为 attempts 一达到 `max-attempts` 就转 `DEAD`、不再计算下次重试,能真正用到的档位
|
2026-09-11 14:26:44 +08:00
|
|
|
|
* 只有 `max-attempts − 1` 个。表更长会有一段**永远走不到**,
|
2026-09-11 08:08:35 +08:00
|
|
|
|
* 表更短则会提前封顶。两种错位都应该在启动时暴露,而不是静默生效。
|
|
|
|
|
|
*/
|
|
|
|
|
|
fun validate() {
|
|
|
|
|
|
require(backoffMs.size == maxAttempts - 1) {
|
|
|
|
|
|
"msgx.pipeline.backoff-ms has ${backoffMs.size} slots, " +
|
|
|
|
|
|
"but max-attempts=$maxAttempts implies exactly ${maxAttempts - 1}"
|
|
|
|
|
|
}
|
|
|
|
|
|
// 切流播种只接受四种取值;非法值必须在启动时挡掉,而不是每轮轮询刷错误日志。
|
|
|
|
|
|
val cutover = cutoverWatermark
|
|
|
|
|
|
require(
|
|
|
|
|
|
cutover == null ||
|
|
|
|
|
|
cutover.equals("min", ignoreCase = true) ||
|
|
|
|
|
|
cutover.equals("zero", ignoreCase = true) ||
|
|
|
|
|
|
cutover.equals("max", ignoreCase = true) ||
|
|
|
|
|
|
cutover.toLongOrNull() != null,
|
|
|
|
|
|
) {
|
|
|
|
|
|
"msgx.pipeline.cutover-watermark must be one of min|zero|max|<id>, got '$cutover'"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-09-06 16:13:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-06 17:58:39 +08:00
|
|
|
|
@ConfigurationProperties("schd")
|
2026-09-06 16:13:50 +08:00
|
|
|
|
class Schd {
|
|
|
|
|
|
var flushPeriod: Duration = Duration.ofSeconds(3) // KEEP 现役节律
|
|
|
|
|
|
var flushLimit: Int = 500
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-06 17:58:39 +08:00
|
|
|
|
@ConfigurationProperties("identity")
|
2026-09-06 16:13:50 +08:00
|
|
|
|
class Identity {
|
|
|
|
|
|
/** CONFIRM(矩阵 #11):SEQN 重置作用域确认前保持 false,计算集中此处(I3)。 */
|
|
|
|
|
|
var includeDayBoundary: Boolean = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|