fix(processing): 落地 D1/D5/D6 三项裁决,删除 head-deadline 参数
D5(终态判据只保留尝试上限): - Pump.tick 内联 attempts 判定,删除 head-deadline 相关的毒丸分支与滞留告警代码 - 删除配置项 head-deadline(PipelineProps / application.yml)与 PumpDeadlineTest - PROCESSING_STARTED_AT 变为只写,注释如实说明当前无判据消费它 D1(回填放弃判据改为时间): - 暂时性故障在 R 之前只退避重试,不再按尝试次数放弃;到 R 才放弃并记 TRANSIENT_DEADLINE - backfill-max-attempts 降级为单行重试的告警阈值 D6(超期判据改用本地入队时间): - 新增 V6 迁移:PROC_STATE 加 ENQUEUED_AT(回填存量后置为非空 + 默认) - findBackfillDue 的谓词与 overdue 标记改比较 enqueued_at,不再用库方时钟的 received_at - BackfillDue 增加 overdue;收报与兼容入口显式写入本地入队时间 文档同步: - 清理 4 处 message-lifecycle.md 章节号死链(Pump/InboxService/PipelineProps/application.yml) - 关闭 G-HEAD-DEADLINE、G-BACKFILL-ABANDON-BYTIME、G-ENQUEUED-AT 三条缺口登记 - reference/user-stories/README 与实现对齐 验证:./gradlew test ⇒ 122 tests, 0 failures, 1 skipped Refs: ACM2-45
This commit is contained in:
@@ -45,10 +45,11 @@ interface ProcStateRepository {
|
||||
*
|
||||
* 幂等:同一个消息 ID 重复登记既不报错、也不会建第二行(收报重扫和兼容入口并发调用都安全)。
|
||||
*
|
||||
* @param receivedAt 信箱里的接收时间,用于判断超期未回填和统计最老信龄
|
||||
* @param receivedAt 信箱里的接收时间,来自库方时钟、可能为 NULL;只作对账与展示
|
||||
* @param enqueuedAt 本地入队时间;不传时由实现用自己注入的时钟填(列非空)
|
||||
* @return true 表示这次真的新建了一行
|
||||
*/
|
||||
fun insertIfAbsent(msgId: Long, receivedAt: Instant?): Boolean
|
||||
fun insertIfAbsent(msgId: Long, receivedAt: Instant?, enqueuedAt: Instant? = null): Boolean
|
||||
|
||||
fun find(msgId: Long): ProcState?
|
||||
|
||||
@@ -99,11 +100,13 @@ interface ProcStateRepository {
|
||||
fun recordBackfillFailure(msgId: Long, error: String?, attempts: Int, nextAttemptAt: Instant, now: Instant)
|
||||
|
||||
/**
|
||||
* 放弃回填:判定该行不必再自动尝试(信箱行不存在,或达到尝试上限)。
|
||||
* 放弃回填:判定该行不必再自动尝试(信箱行不存在,或暂时性故障持续到 `R` 仍未打标)。
|
||||
*
|
||||
* **abandoned ≠ 标记已确认**:`BACKFILL_AT` 仍为空,因此**不**满足"边界内全部行已打标"的
|
||||
* 清除前提;放弃只是停止自动重试并把事实留痕,供人工对账。
|
||||
*
|
||||
* 放弃判据是**时间**(`R` 超期)而不是尝试次数:一次小时级的共享库故障不该把待回填行成批判死。
|
||||
*
|
||||
* @return false 表示该行不存在
|
||||
*/
|
||||
fun markBackfillAbandoned(msgId: Long, reason: String, now: Instant): Boolean
|
||||
@@ -117,8 +120,9 @@ interface ProcStateRepository {
|
||||
/**
|
||||
* 找出现在该回填的记录:已经到终态、还没确认回填,并且退避时间已到。
|
||||
*
|
||||
* [overdueBefore] 是兜底:消息接收时间早于它的(已经等了很久)无视退避直接补写。
|
||||
* [overdueBefore] 是兜底:入队时间早于它的(已经等了超过 `R`)无视退避直接补写。
|
||||
* 没有这条兜底,退避一直失败的话这些行就永远打不上标记,库方也没法清理信箱。
|
||||
* 判据比较的是**本地** `ENQUEUED_AT`,不是库方时钟的 `RECEIVED_AT`(`PRE-4`)。
|
||||
*/
|
||||
fun findBackfillDue(now: Instant, overdueBefore: Instant, limit: Int): List<BackfillDue>
|
||||
|
||||
@@ -132,8 +136,12 @@ interface ProcStateRepository {
|
||||
fun hasAny(): Boolean
|
||||
}
|
||||
|
||||
/** 扫描到的待回填记录。 */
|
||||
data class BackfillDue(val msgId: Long, val attempts: Int)
|
||||
/**
|
||||
* 扫描到的待回填记录。
|
||||
* @param overdue 入队时间早于 `NOW − R`:本轮已经进入强补写窗口
|
||||
* (调用方据此决定"暂时性故障是否到放弃期限")
|
||||
*/
|
||||
data class BackfillDue(val msgId: Long, val attempts: Int, val overdue: Boolean = false)
|
||||
|
||||
/**
|
||||
* 处理侧积压快照。
|
||||
|
||||
+24
-12
@@ -76,16 +76,20 @@ class JdbcProcStateRepository(
|
||||
private val clock: Clock,
|
||||
) : ProcStateRepository {
|
||||
/** 入队(幂等):主键冲突时什么都不做,所以重复扫描和兼容入口并发调用都安全。 */
|
||||
override fun insertIfAbsent(msgId: Long, receivedAt: Instant?): Boolean =
|
||||
ds.update(
|
||||
"INSERT INTO proc_state (msg_id, state, received_at, updated_at) VALUES (?, 'PENDING', ?, ?) " +
|
||||
"ON CONFLICT (msg_id) DO NOTHING",
|
||||
override fun insertIfAbsent(msgId: Long, receivedAt: Instant?, enqueuedAt: Instant?): Boolean {
|
||||
val now = clock.instant()
|
||||
return ds.update(
|
||||
"INSERT INTO proc_state (msg_id, state, received_at, enqueued_at, updated_at) " +
|
||||
"VALUES (?, 'PENDING', ?, ?, ?) ON CONFLICT (msg_id) DO NOTHING",
|
||||
{ ps ->
|
||||
ps.setLong(1, msgId)
|
||||
ps.setTimestamp(2, receivedAt?.toSqlTimestamp())
|
||||
ps.setTimestamp(3, clock.instant().toSqlTimestamp())
|
||||
// 入队时间必须由本系统时钟给出:它同时是超期判据 R 的比较对象(PRE-4)。
|
||||
ps.setTimestamp(3, (enqueuedAt ?: now).toSqlTimestamp())
|
||||
ps.setTimestamp(4, now.toSqlTimestamp())
|
||||
},
|
||||
) == 1
|
||||
}
|
||||
|
||||
override fun find(msgId: Long): ProcState? =
|
||||
ds.queryOne("$SELECT_PROC WHERE msg_id = ?", { ps -> ps.setLong(1, msgId) }, ::mapProcState)
|
||||
@@ -254,22 +258,29 @@ class JdbcProcStateRepository(
|
||||
* 排序用**公平轮转**:先按已尝试次数升序,再按 msg_id。若只按 msg_id 升序,
|
||||
* 最旧的一批永久失败行会持续占满批次,后面的记录永远轮不到(全局回填饥饿)。
|
||||
*/
|
||||
/**
|
||||
* 超期判据用 `enqueued_at`(本地时钟、非空),不用 `received_at`:
|
||||
* 后者来自库方时钟,前偏会让 `NOW − R` 提前成立(`PRE-4`)。
|
||||
*/
|
||||
override fun findBackfillDue(now: Instant, overdueBefore: Instant, limit: Int): List<BackfillDue> =
|
||||
ds.query(
|
||||
"""
|
||||
SELECT msg_id, backfill_attempts FROM proc_state
|
||||
SELECT msg_id, backfill_attempts, (enqueued_at < ?) AS overdue FROM proc_state
|
||||
WHERE backfill_at IS NULL
|
||||
AND backfill_abandoned_at IS NULL
|
||||
AND state IN ('SUCCEEDED', 'SKIPPED', 'DEAD')
|
||||
AND (backfill_next_at IS NULL OR backfill_next_at <= ? OR (received_at IS NOT NULL AND received_at < ?))
|
||||
AND (backfill_next_at IS NULL OR backfill_next_at <= ? OR enqueued_at < ?)
|
||||
ORDER BY backfill_attempts ASC, msg_id ASC LIMIT ?
|
||||
""".trimIndent(),
|
||||
{ ps ->
|
||||
ps.setTimestamp(1, now.toSqlTimestamp())
|
||||
ps.setTimestamp(2, overdueBefore.toSqlTimestamp())
|
||||
ps.setInt(3, limit)
|
||||
ps.setTimestamp(1, overdueBefore.toSqlTimestamp())
|
||||
ps.setTimestamp(2, now.toSqlTimestamp())
|
||||
ps.setTimestamp(3, overdueBefore.toSqlTimestamp())
|
||||
ps.setInt(4, limit)
|
||||
},
|
||||
) { rs -> BackfillDue(rs.getLong("msg_id"), rs.getInt("backfill_attempts")) }
|
||||
) { rs ->
|
||||
BackfillDue(rs.getLong("msg_id"), rs.getInt("backfill_attempts"), rs.getBoolean("overdue"))
|
||||
}
|
||||
|
||||
override fun requeueByErrorClasses(errorClasses: List<ErrorClass>): Int {
|
||||
if (errorClasses.isEmpty()) return 0
|
||||
@@ -321,6 +332,7 @@ class JdbcProcStateRepository(
|
||||
errorClass = rs.getString("error_class")?.let(ErrorClass::valueOf),
|
||||
lastError = rs.getString("last_error"),
|
||||
receivedAt = rs.getInstant("received_at"),
|
||||
enqueuedAt = rs.getInstant("enqueued_at"),
|
||||
backfillAt = rs.getInstant("backfill_at"),
|
||||
backfillNextAt = rs.getInstant("backfill_next_at"),
|
||||
backfillAttempts = rs.getInt("backfill_attempts"),
|
||||
@@ -334,7 +346,7 @@ class JdbcProcStateRepository(
|
||||
private companion object {
|
||||
const val SELECT_PROC =
|
||||
"SELECT msg_id, state, identity_key, attempts, next_attempt_at, error_class, last_error, " +
|
||||
"received_at, backfill_at, backfill_next_at, backfill_attempts, backfill_error, " +
|
||||
"received_at, enqueued_at, backfill_at, backfill_next_at, backfill_attempts, backfill_error, " +
|
||||
"backfill_abandoned_at, backfill_abandoned_reason, processing_started_at, updated_at FROM proc_state"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,9 +63,17 @@ class StubProcState : ProcStateRepository {
|
||||
rows.clear(); bound.clear()
|
||||
}
|
||||
|
||||
override fun insertIfAbsent(msgId: Long, receivedAt: Instant?): Boolean {
|
||||
/**
|
||||
* 内存版没有独立时钟,入队时间默认沿用 `receivedAt`(测试可显式传入以区分两者);
|
||||
* 生产实现由注入的 Clock 填,`ENQUEUED_AT` 列非空。
|
||||
*/
|
||||
override fun insertIfAbsent(msgId: Long, receivedAt: Instant?, enqueuedAt: Instant?): Boolean {
|
||||
if (rows.containsKey(msgId)) return false
|
||||
rows[msgId] = ProcState(msgId, ProcStatus.PENDING, receivedAt = receivedAt)
|
||||
rows[msgId] = ProcState(
|
||||
msgId, ProcStatus.PENDING,
|
||||
receivedAt = receivedAt,
|
||||
enqueuedAt = enqueuedAt ?: receivedAt,
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -182,14 +190,13 @@ class StubProcState : ProcStateRepository {
|
||||
override fun findBackfillDue(now: Instant, overdueBefore: Instant, limit: Int): List<BackfillDue> =
|
||||
rows.values
|
||||
.filter { it.state.isTerminal() && it.backfillAt == null && it.backfillAbandonedAt == null }
|
||||
.filter {
|
||||
it.backfillNextAt == null || it.backfillNextAt <= now ||
|
||||
(it.receivedAt != null && it.receivedAt < overdueBefore)
|
||||
}
|
||||
// 超期判据是本地入队时间,与 JDBC 实现保持一致(不用库方时钟的 receivedAt)。
|
||||
.map { it to (it.enqueuedAt?.let { e -> e < overdueBefore } ?: false) }
|
||||
.filter { (row, overdue) -> overdue || row.backfillNextAt == null || row.backfillNextAt <= now }
|
||||
// 公平轮转:先按已尝试次数,再按 msg_id。只按 msg_id 会让最旧的一批永久失败行占满批次。
|
||||
.sortedWith(compareBy({ it.backfillAttempts }, { it.msgId }))
|
||||
.sortedWith(compareBy({ it.first.backfillAttempts }, { it.first.msgId }))
|
||||
.take(limit)
|
||||
.map { BackfillDue(it.msgId, it.backfillAttempts) }
|
||||
.map { (row, overdue) -> BackfillDue(row.msgId, row.backfillAttempts, overdue) }
|
||||
|
||||
override fun hasAny(): Boolean = rows.isNotEmpty()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user