修复双实例 owner-scoped 结果文件 job 被非归属实例误杀
Build Backend JAR / build (push) Has been cancelled

线上现象:货源查询任务 26402/26400(归属 server-121)在归属机正常推进,
却被 server-110 判为「结果生成失败」,错误为「该任务已绑定到另一台服务实例处理」。

两个缺陷叠加:
1. 两台实例共用一个 RocketMQ consumer group,dispatch 消息被轮询投递;
   worker 对"PENDING + owner 不匹配"的 job 选择本地抢跑,模块层
   ensureTaskOwnedByCurrentInstance 直接抛 TaskOwnerMismatchException,
   异常落进通用 catch 被当成生成失败,5 次抢跑即耗尽 retryCount 并终态失败。
2. runPendingJobs 双重 claim:claimRunnableJobsForOwner 已把 job 翻成 RUNNING,
   随后又走 claimRunning(条件 PENDING/FAILED)必然失败 → job 被静默丢弃,
   归属机的定时兜底从未真正生效。

修复:
- worker:非归属实例一律跳过 owner-scoped job;抽出 processClaimed 供已持有
  claim 的调用方使用;processInternal 单独捕获 TaskOwnerMismatchException,
  只 deferRunning 退回 PENDING,不计入重试。
- TaskFileJobLocalDispatcher 新增 dispatchClaimed。
- dispatch 事件带上 owner,coordinator 按归属路由:本机 owner 直接本地派发不发 MQ,
  他机 owner 不发不跑交由归属机轮询接手,owner 为空维持原有 MQ 行为。
- 新增 owner 路由回归测试两组,回滚配合"本地抢跑"的旧断言。

同时提交此前工作区内已随 JAR 上线的 backend-java 改动:SimilarAsin 解析载荷
groups 内嵌 items(Python 旧链路兼容)与 chunk 类型不匹配跳过、
PermissionMenuSchemaInitializer 与 V103 两级菜单分组迁移、相关测试。
This commit is contained in:
2026-09-01 21:07:25 +08:00
parent 55517f996b
commit 5f4fcad2ef
16 changed files with 679 additions and 44 deletions
@@ -27,6 +27,7 @@ import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
@@ -268,6 +269,7 @@ class ExternalCallMetricsRecorderTest {
}
} finally {
pool.shutdown();
assertTrue(pool.awaitTermination(60, TimeUnit.SECONDS), "并发任务在超时前全部结束");
}
awaitMetric("aiimage.external-call.total", "client", "llm", "result", "success");
@@ -275,6 +277,12 @@ class ExternalCallMetricsRecorderTest {
Thread.sleep(10);
}
assertEquals(20, llmSubmitCount.get(), "并发 20 请求全部真实发出");
// 指标在响应处理完成之后才落,submit 计数到 20 不代表指标已收敛:
// 直接断言会读到中间值(实测 18.0)。等指标计数自身收敛再断言。
for (int i = 0; i < 2000 && counterCount("aiimage.external-call.total",
"client", "llm", "result", "success") < 20.0; i++) {
Thread.sleep(10);
}
assertEquals(20.0, counterCount("aiimage.external-call.total",
"client", "llm", "result", "success"), "20 次成功全部记录,无重复");
}