Files
msgexchange-v2/src/main/kotlin/com/gzzn/omms/msgexchange/domain/ProcState.kt
T

88 lines
3.5 KiB
Kotlin
Raw Normal View History

package com.gzzn.omms.msgexchange.domain
import java.time.Instant
/**
* 一条入站消息的处理记录,一行对应共享信箱 CMINMSGS 里的一条报文。
*
* 这张表同时承担四件事:
* 1. 防重复入队——主键是信箱 ID,同一条报文只会有一行;
* 2. 防业务重复——IDENTITY_KEY 唯一,同一条业务报文只处理一次;
* 3. 记录处理进度——状态、重试次数、下次重试时间和失败原因;
* 4. 记录回填进度——处理完要把"已处理"标记写回共享信箱,写成功之前一直留着待办意图。
*
* 第 4 条和终态写在同一条 UPDATE 里,所以不会出现"业务处理完了,却没人记得去回填"。
*/
enum class ProcStatus {
/** 已入队,等待处理。 */
PENDING,
/** 处理失败,等退避时间到了再重试。 */
FAILED,
/** 处理成功。 */
SUCCEEDED,
/** 判定为业务重复,跳过不处理。 */
SKIPPED,
/** 处理失败且不再重试,等人工处置。 */
DEAD,
}
/** 失败原因分类,决定失败后是重试还是直接进死信。 */
enum class ErrorClass {
/** 报文本身不合法,重试也没用,直接进死信。 */
MALFORMED,
/** 整包被拒绝(运营日冲突、声明条数不符等),整包不落地,直接进死信。 */
PROTOCOL,
/** 解码逻辑的问题;修好 codec 之后可以重放。 */
CODEC_ERROR,
/** 重试次数用尽或队头滞留超时;人工复核后可以重放。 */
EXHAUSTED,
/** 数据库、网络等基础设施抖动,重试通常就能过。 */
INFRA,
/** 报文类型还没有对应处理器;属于能力未实现,先退避重试等补齐。 */
UNSUPPORTED,
}
data class ProcState(
/** 信箱 CMINMSGS_ID,也是本表主键。 */
val msgId: Long,
val state: ProcStatus,
/** 业务身份 SNDR|TYPE|STYP|SEQN;解码成功后绑定一次,重试不会重绑。 */
val identityKey: String? = null,
/** 处理失败次数,用来算退避档位和判断是否已到上限。 */
val attempts: Int = 0,
/** FAILED 状态下,下次可以重试的时刻。 */
val nextAttemptAt: Instant? = null,
val errorClass: ErrorClass? = null,
/** 最近一次失败的原因(截断后落库,供排查)。 */
val lastError: String? = null,
/** 信箱里的接收时间:用来判断"超期仍未回填",也是最老未处理信龄的计算依据。 */
val receivedAt: Instant? = null,
/** 非空表示已确认信箱行带上了处理标记。 */
val backfillAt: Instant? = null,
/** 非空表示还欠一次回填:写终态时置为当前时间,失败后退避推后。 */
val backfillNextAt: Instant? = null,
val backfillAttempts: Int = 0,
val backfillError: String? = null,
/**
* 非空表示已判定"不必再回填"(信箱行不存在,或达到尝试上限)。
*
* **它不等于标记已确认**`backfillAt` 仍为空,所以不满足"边界内全部行已打标"的清除条件。
* 停止重试与"已满足清除前提"是两件事,不能互相替代。
*/
val backfillAbandonedAt: Instant? = null,
/** 放弃原因(`MISSING_ROW` / `MAX_ATTEMPTS`),供人工对账与恢复判断。 */
val backfillAbandonedReason: String? = null,
/** 首次被主泵取得的时刻;重试不刷新,用作 HOL deadline 的稳定起点。 */
val processingStartedAt: Instant? = null,
val updatedAt: Instant = Instant.now(),
)