fix: 修正生成失败错误码被终态码覆盖 #63

Closed
opened 2026-08-25 21:24:26 +08:00 by ila · 1 comment
Owner

基本信息

  • 类型:缺陷
  • 所属 Epic:#3
  • 所属 MVP / 版本:#35 / MVP-2
  • 阶段:已完成

依赖与并行

  • 前置工单:无
  • 是否允许与前置工单并行:是
  • 原因:与超时配置工单相互独立。本工单只修正错误码的记录与归类,不改变任何超时值;两者可并行,但本工单修好后能让超时工单的验证证据更准确,建议优先。

子项目影响

  • 仅影响的子项目 / 交付单元:portal(worker)、internal/core/queue
  • 是否跨子项目:否
  • 是否修改共享接口或契约:否;不改表结构,只改写入 generations.attempts 与 error_code 的取值逻辑
  • 各子项目需要执行的验证:go test ./internal/core/queue/... ./portal/worker/...,以及全量 go build ./... && go vet ./... && go test ./...

原始需求

  • 来源:用户对话
  • 提出时间:2026-08-25
  • 关键原话或脱敏摘要:用户报告"最新代码,portal 端 test 用户登录后文生图失败",要求分析具体原因。诊断过程中发现真实错误码被覆盖,导致无法从记录判断失败原因;用户确认就此单独建单。

要解决什么

现象

上游超时(upstream_timeout)的失败被记录并展示为 route_unavailable,用户端提示"当前没有可用的图片生成路由"。但此时路由配置完全正常:active_routes.image_generate → route_pool 2,成员与 provider、model 三级 enabled 均为 1。错误信息把使用者引向错误的排查方向。

复现

以 generation 13 为例,其 attempts 中的 provider 尝试记录自相矛盾:

{"type": "provider", "retryable": true, "error_code": "route_unavailable",
 "latency_ms": 300009, "route_member_id": 5, "provider_model_id": 4,
 "error_message": "upstream request failed"}

route_unavailable 表示"没有可用成员",但同一条记录却有 300 秒的 provider 调用耗时和 retryable: true —— 说明调用真实发生过并超时,错误码事后被改写。

期望结果

  • 终态 error_code 反映真实失败原因(超时即 upstream_timeout,候选耗尽即 failover_exhausted),只有真正选不出成员时才是 route_unavailable。
  • attempts 中每条 provider 尝试保留该次尝试自身的真实错误码,不被终态码覆盖。

这两点是 AGENTS.md 红线 9(rendered_prompt 与 attempts 必须落库,失败必须写 error_code 与 error_message)以及架构文档中"attempts 是排障的唯一依据"的直接要求。

做什么 / 不做什么

  • 做:修正终态错误码的判定依据;停止用终态码覆盖已有 provider 尝试的错误码。
  • 不做:
    • 不修改 generations.status 状态机(仍是 pending → running → succeeded | failed)。
    • 不修改 retryable 判定、熔断参数、选路算法或 SSRF 校验。
    • 不修改数据库表结构,不新增迁移。
    • 不修改任何超时值。
    • 不顺手调整用户端错误文案(如需调整另开工单)。

已确认方案

根因一:终态判定使用了过期的内存快照

// portal/worker/worker.go:290
if claim.Generation.ProviderAttemptCount > 0 {
    return w.fail(ctx, claim, provider.ErrorCode(router.CodeFailoverExhausted))
}
return w.fail(ctx, claim, provider.ErrorCode(router.CodeRouteUnavailable))

BeginProviderAttempt 只在数据库里递增:

-- internal/core/queue/mysql_repository.go:246
SET provider_model_id = ?, provider_attempt_count = provider_attempt_count + 1,

它没有回写 claim.Generation.ProviderAttemptCount。该字段在 ClaimNext 时读取一次后再不更新,所以循环结束时仍是 0,落入 route_unavailable 分支。数据库中 generation 13 的 provider_attempt_count = 1,与内存值不一致,可作为佐证。

同一个过期值还被 worker.go:160、:175、:182 使用。:182 的 remaining 计算在单次 process 内只算一次,当前不受影响,但依赖的是同一个易错前提,需一并核对。

修法:在 worker 内维护本次 process 的真实尝试计数(以 BeginProviderAttempt 成功返回为准),终态判定和 remaining 计算都使用它,而不是 claim 时的快照。

根因二:Fail() 覆盖最后一条尝试的错误码

// internal/core/queue/mysql_repository.go:394 Fail()
attempt.ErrorCode = code
// ...
attempts = JSON_SET(attempts, CONCAT('$[', attempt_count - 1, ']'),
    JSON_MERGE_PATCH(JSON_EXTRACT(attempts, CONCAT('$[', attempt_count - 1, ']')),
                     CAST(? AS JSON)))

终态码被 JSON_MERGE_PATCH 合并进最后一条 attempt,抹掉了 finishFailure 已经写入的真实 upstream_timeout。

修法:Fail() 不得覆盖已有 error_code 非空的 attempt。终态码只写 generations.error_code;仅当最后一条 attempt 尚无错误码(例如租约、快照解码等非 provider 阶段的失败)时才填充。

修复后该场景的正确记录

同样是单成员池、max_failover=0、provider 超时:

  • generations.error_code = failover_exhausted(可重试失败但已无候选)
  • attempts[1].error_code = upstream_timeout,retryable: true,latency_ms 保留

预计修改文件:

  • portal/worker/worker.go
  • internal/core/queue/mysql_repository.go
  • portal/worker/worker_test.go
  • internal/core/queue/mysql_integration_test.go

需求变化记录

日期 变化内容 原因 用户确认
无 是

设计与原型门禁

  • 修改类型:恢复既有行为的 Bug
  • 所需设计证据:原设计或复现证据
  • 可编辑设计源链接、版本或事实来源:AGENTS.md 红线 9;架构与代码地图"不可破坏的边界";本工单"要解决什么"中的 generation 13 记录
  • 本地 HTML 审核快照路径和版本(不适用时说明原因):不适用,非 UI 修改,不改变页面结构、流程、状态或异常处理路径
  • 本地浏览方式和资源完整性检查:不适用
  • 版本、revision 或确认日期:2026-08-25
  • 状态:已确认
  • 确认人、确认时间和覆盖范围:ila,2026-08-25,覆盖上述两处错误码写入逻辑
  • 无需 UI 原型或无需任何原型的原因:只恢复"失败记录必须反映真实错误"这一既有约定,界面无变化

文档影响

  • 更新常见修改或故障排查

Troubleshooting 中"status=failed,error_code 是 400/401 → 不应发生故障转移"一类的判读表,需要补上修复后 failover_exhausted 与 route_unavailable 的区分含义。

交付文档影响

  • 无交付文档影响,原因:内部失败记录语义修正,无外部受众。

验收标准

  • 单成员池、max_failover=0、provider 返回可重试失败(超时)时,generations.error_code 为 failover_exhausted,不再是 route_unavailable。
  • 同一场景下 attempts 中该次 provider 尝试保留 upstream_timeout,retryable 与 latency_ms 不被改写。
  • 真正选不出可用成员(成员禁用或熔断打开)时,error_code 仍为 route_unavailable。
  • 非 provider 阶段的失败(租约、快照解码)仍能正确写入终态码与错误信息,不出现空错误码。
  • generations.status 流转未改变,仍只允许 pending → running → succeeded | failed。
  • go build ./...、go vet ./...、go test ./... 全部通过。
  • 新增测试覆盖上述前四条,且包含失败分支而不只是成功路径。

验证方式

go build ./...
go vet ./...
go test ./internal/core/queue/... ./portal/worker/... -v
go test ./...

需要 MySQL 的集成测试按既有约定使用本机测试库,不使用生产数据、不调用真实上游。

完成证据

最终差异

portal/worker/worker.go —— 引入本次 process 的真实尝试计数 providerAttemptCount:

  • 初始化为 claim.Generation.ProviderAttemptCount;
  • 每次 BeginProviderAttempt 成功后更新为 attempt.ProviderOrdinal(该值由仓储层按数据库的 provider_attempt_count + 1 计算,是权威值);
  • 原先使用 claim 快照的四处(:160 上限判断、:175 选不出候选时的分支、:182 remaining 计算、:290 终态判定)全部改用该变量。

internal/core/queue/mysql_repository.go —— Fail() 不再无条件覆盖尾条尝试:

attempts = IF(
    attempt_count > 0
    AND JSON_EXTRACT(attempts, CONCAT('$[', GREATEST(attempt_count - 1, 0), '].finished_at')) IS NULL,
    JSON_SET(attempts, CONCAT('$[', GREATEST(attempt_count - 1, 0), ']'),
        JSON_MERGE_PATCH(JSON_EXTRACT(attempts, CONCAT('$[', GREATEST(attempt_count - 1, 0), ']')),
                         CAST(? AS JSON))),
    attempts)

判定条件从"是否有错误码"改为**"该尝试是否已结束"**,语义更清晰:已结束的尝试记录不可变,终态码只写 generations.error_code;只有尚未结束的尾条尝试(租约、快照解码等非 provider 阶段失败)才补写终态信息。GREATEST 保证 attempt_count = 0 时 JSON 路径仍然合法。

顺带覆盖的一个额外场景:saveOutputs 失败时(provider 调用已成功),该次尝试现在保持成功记录不变,generations.error_code 记 upstream_unknown——如实反映"上游成功、本地落盘失败"。

测试结果

新增 5 个测试,全部先验证在未修复代码上会失败:

测试 覆盖
TestWorkerReportsFailoverExhaustedAfterRetryableFailure 单成员池 + 超时 → failover_exhausted,且该尝试保留 upstream_timeout
TestWorkerReportsFailoverExhaustedAfterAllCandidatesFail 两成员都可重试失败 → failover_exhausted
TestWorkerReportsRouteUnavailableWhenNoMemberIsEligible 成员禁用 → 仍为 route_unavailable,且 begun 为空
TestWorkerKeepsNonRetryableProviderCodeAsTerminal 400 → 终态仍是 upstream_bad_request,只尝试一次
TestMySQLFailKeepsFinishedAttemptErrorCode 集成测试:Fail() 后尾条尝试的 error_code / retryable / latency_ms 均未被改写

反向验证(临时还原修复后运行):

--- FAIL: TestWorkerReportsFailoverExhaustedAfterRetryableFailure
    terminal code = route_unavailable, want failover_exhausted
--- FAIL: TestWorkerReportsFailoverExhaustedAfterAllCandidatesFail
    terminal code = route_unavailable
--- FAIL: TestMySQLFailKeepsFinishedAttemptErrorCode
    provider attempt error_code = "failover_exhausted", want upstream_timeout (被终态码覆盖)

全量验证(含 MySQL 集成测试,CHORUS_TEST_DSN 指向本机 3308 测试库):

go build ./...   通过
go vet ./...     通过
go test ./...    28 个包全部 ok(含 internal/core/queue、portal/worker)
python dev_scripts/harness.py sync --check   Wiki 镜像检查通过
python dev_scripts/harness.py check --strict DevHarness 检查通过
python -m unittest discover -s tests         OK

未验证部分

  • 未做真实上游端到端复现:修复行为由单元与集成测试覆盖,没有再造一次真实超时来观察线上记录。下一次真实失败时应核对 generations.error_code 是否为 failover_exhausted、attempts 尾条是否保留 upstream_*。
  • 历史记录未回溯修正:generation 6/7/9/11/12/13 的 route_unavailable 保持原样作为审计证据,不修改。
  • 并发下的 Fail() 竞争未专门验证:依赖既有的 lease_token CAS 谓词,本次未改动该谓词,也未新增并发测试。
  • worker.go:182 的 remaining 计算:改用新变量后行为与原先一致(单次 process 内只算一次),但没有专门构造"claim 时已有尝试记录"的场景来验证跨轮次的正确性。

提交哈希

  • 52ebca2 fix: 修正生成失败错误码被终态码覆盖 (#63)
  • f4a17af docs: 区分 route_unavailable 与 failover_exhausted 的判读 (#63)

均已推送至 origin/main。

长期 Wiki 页面与 revision

  • Troubleshooting → 9803ea981876455054c5598a38b21fd030211d4c

新增终态码判读表,明确区分 route_unavailable(一次上游都没调过)、failover_exhausted(调过且可重试但无候选可换)与 upstream_*(不可重试),并说明 attempts 中每条尝试的 error_code 不会被终态码覆盖——若看到 error_code=route_unavailable 却带 retryable=true 和真实 latency_ms,说明跑的是修复前的版本。

风险和回退

  • 触及 worker 终态判定与队列失败写入,属高风险区域:写错会让所有失败记录的错误码失真,比现状更难排查。
  • 缓解:不改状态机、不改表结构、不改 retryable 判定;每一条验收标准都必须有对应单元测试,包含失败分支。
  • 已产生的历史记录(generation 6/7/9/11/12/13)不回溯修正,保留原样作为审计证据。
  • 回退:直接 revert 本工单提交,不涉及数据迁移。
## 基本信息 - 类型:缺陷 - 所属 Epic:#3 - 所属 MVP / 版本:#35 / MVP-2 - 阶段:已完成 ## 依赖与并行 - 前置工单:无 - 是否允许与前置工单并行:是 - 原因:与超时配置工单相互独立。本工单只修正错误码的记录与归类,不改变任何超时值;两者可并行,但本工单修好后能让超时工单的验证证据更准确,建议优先。 ## 子项目影响 - 仅影响的子项目 / 交付单元:`portal`(worker)、`internal/core/queue` - 是否跨子项目:否 - 是否修改共享接口或契约:否;不改表结构,只改写入 `generations.attempts` 与 `error_code` 的取值逻辑 - 各子项目需要执行的验证:`go test ./internal/core/queue/... ./portal/worker/...`,以及全量 `go build ./... && go vet ./... && go test ./...` ## 原始需求 - 来源:用户对话 - 提出时间:2026-08-25 - 关键原话或脱敏摘要:用户报告"最新代码,portal 端 test 用户登录后文生图失败",要求分析具体原因。诊断过程中发现真实错误码被覆盖,导致无法从记录判断失败原因;用户确认就此单独建单。 ## 要解决什么 ### 现象 上游超时(`upstream_timeout`)的失败被记录并展示为 `route_unavailable`,用户端提示"当前没有可用的图片生成路由"。但此时路由配置完全正常:`active_routes.image_generate → route_pool 2`,成员与 provider、model 三级 `enabled` 均为 1。错误信息把使用者引向错误的排查方向。 ### 复现 以 generation 13 为例,其 `attempts` 中的 provider 尝试记录自相矛盾: ```json {"type": "provider", "retryable": true, "error_code": "route_unavailable", "latency_ms": 300009, "route_member_id": 5, "provider_model_id": 4, "error_message": "upstream request failed"} ``` `route_unavailable` 表示"没有可用成员",但同一条记录却有 300 秒的 provider 调用耗时和 `retryable: true` —— 说明调用真实发生过并超时,错误码事后被改写。 ### 期望结果 - 终态 `error_code` 反映真实失败原因(超时即 `upstream_timeout`,候选耗尽即 `failover_exhausted`),只有真正选不出成员时才是 `route_unavailable`。 - `attempts` 中每条 provider 尝试保留该次尝试自身的真实错误码,不被终态码覆盖。 这两点是 `AGENTS.md` 红线 9(`rendered_prompt` 与 `attempts` 必须落库,失败必须写 `error_code` 与 `error_message`)以及架构文档中"`attempts` 是排障的唯一依据"的直接要求。 ## 做什么 / 不做什么 - 做:修正终态错误码的判定依据;停止用终态码覆盖已有 provider 尝试的错误码。 - 不做: - 不修改 `generations.status` 状态机(仍是 `pending → running → succeeded | failed`)。 - 不修改 `retryable` 判定、熔断参数、选路算法或 SSRF 校验。 - 不修改数据库表结构,不新增迁移。 - 不修改任何超时值。 - 不顺手调整用户端错误文案(如需调整另开工单)。 ## 已确认方案 ### 根因一:终态判定使用了过期的内存快照 ```go // portal/worker/worker.go:290 if claim.Generation.ProviderAttemptCount > 0 { return w.fail(ctx, claim, provider.ErrorCode(router.CodeFailoverExhausted)) } return w.fail(ctx, claim, provider.ErrorCode(router.CodeRouteUnavailable)) ``` `BeginProviderAttempt` 只在数据库里递增: ```sql -- internal/core/queue/mysql_repository.go:246 SET provider_model_id = ?, provider_attempt_count = provider_attempt_count + 1, ``` 它**没有回写 `claim.Generation.ProviderAttemptCount`**。该字段在 `ClaimNext` 时读取一次后再不更新,所以循环结束时仍是 0,落入 `route_unavailable` 分支。数据库中 generation 13 的 `provider_attempt_count = 1`,与内存值不一致,可作为佐证。 同一个过期值还被 `worker.go:160`、`:175`、`:182` 使用。`:182` 的 `remaining` 计算在单次 `process` 内只算一次,当前不受影响,但依赖的是同一个易错前提,需一并核对。 修法:在 worker 内维护本次 `process` 的真实尝试计数(以 `BeginProviderAttempt` 成功返回为准),终态判定和 `remaining` 计算都使用它,而不是 claim 时的快照。 ### 根因二:`Fail()` 覆盖最后一条尝试的错误码 ```go // internal/core/queue/mysql_repository.go:394 Fail() attempt.ErrorCode = code // ... attempts = JSON_SET(attempts, CONCAT('$[', attempt_count - 1, ']'), JSON_MERGE_PATCH(JSON_EXTRACT(attempts, CONCAT('$[', attempt_count - 1, ']')), CAST(? AS JSON))) ``` 终态码被 `JSON_MERGE_PATCH` 合并进最后一条 attempt,抹掉了 `finishFailure` 已经写入的真实 `upstream_timeout`。 修法:`Fail()` 不得覆盖已有 `error_code` 非空的 attempt。终态码只写 `generations.error_code`;仅当最后一条 attempt 尚无错误码(例如租约、快照解码等非 provider 阶段的失败)时才填充。 ### 修复后该场景的正确记录 同样是单成员池、`max_failover=0`、provider 超时: - `generations.error_code` = `failover_exhausted`(可重试失败但已无候选) - `attempts[1].error_code` = `upstream_timeout`,`retryable: true`,`latency_ms` 保留 预计修改文件: - `portal/worker/worker.go` - `internal/core/queue/mysql_repository.go` - `portal/worker/worker_test.go` - `internal/core/queue/mysql_integration_test.go` ## 需求变化记录 | 日期 | 变化内容 | 原因 | 用户确认 | |---|---|---|---| | | 无 | | 是 | ## 设计与原型门禁 - 修改类型:恢复既有行为的 Bug - 所需设计证据:原设计或复现证据 - 可编辑设计源链接、版本或事实来源:`AGENTS.md` 红线 9;架构与代码地图"不可破坏的边界";本工单"要解决什么"中的 generation 13 记录 - 本地 HTML 审核快照路径和版本(不适用时说明原因):不适用,非 UI 修改,不改变页面结构、流程、状态或异常处理路径 - 本地浏览方式和资源完整性检查:不适用 - 版本、revision 或确认日期:2026-08-25 - 状态:已确认 - 确认人、确认时间和覆盖范围:ila,2026-08-25,覆盖上述两处错误码写入逻辑 - 无需 UI 原型或无需任何原型的原因:只恢复"失败记录必须反映真实错误"这一既有约定,界面无变化 ## 文档影响 - [x] 更新常见修改或故障排查 Troubleshooting 中"`status=failed`,`error_code` 是 400/401 → 不应发生故障转移"一类的判读表,需要补上修复后 `failover_exhausted` 与 `route_unavailable` 的区分含义。 ## 交付文档影响 - [x] 无交付文档影响,原因:内部失败记录语义修正,无外部受众。 ## 验收标准 - [x] 单成员池、`max_failover=0`、provider 返回可重试失败(超时)时,`generations.error_code` 为 `failover_exhausted`,不再是 `route_unavailable`。 - [x] 同一场景下 `attempts` 中该次 provider 尝试保留 `upstream_timeout`,`retryable` 与 `latency_ms` 不被改写。 - [x] 真正选不出可用成员(成员禁用或熔断打开)时,`error_code` 仍为 `route_unavailable`。 - [x] 非 provider 阶段的失败(租约、快照解码)仍能正确写入终态码与错误信息,不出现空错误码。 - [x] `generations.status` 流转未改变,仍只允许 `pending → running → succeeded | failed`。 - [x] `go build ./...`、`go vet ./...`、`go test ./...` 全部通过。 - [x] 新增测试覆盖上述前四条,且包含失败分支而不只是成功路径。 ## 验证方式 ```powershell go build ./... go vet ./... go test ./internal/core/queue/... ./portal/worker/... -v go test ./... ``` 需要 MySQL 的集成测试按既有约定使用本机测试库,不使用生产数据、不调用真实上游。 ## 完成证据 ### 最终差异 **`portal/worker/worker.go`** —— 引入本次 `process` 的真实尝试计数 `providerAttemptCount`: - 初始化为 `claim.Generation.ProviderAttemptCount`; - 每次 `BeginProviderAttempt` 成功后更新为 `attempt.ProviderOrdinal`(该值由仓储层按数据库的 `provider_attempt_count + 1` 计算,是权威值); - 原先使用 claim 快照的四处(`:160` 上限判断、`:175` 选不出候选时的分支、`:182` `remaining` 计算、`:290` 终态判定)全部改用该变量。 **`internal/core/queue/mysql_repository.go`** —— `Fail()` 不再无条件覆盖尾条尝试: ```sql attempts = IF( attempt_count > 0 AND JSON_EXTRACT(attempts, CONCAT('$[', GREATEST(attempt_count - 1, 0), '].finished_at')) IS NULL, JSON_SET(attempts, CONCAT('$[', GREATEST(attempt_count - 1, 0), ']'), JSON_MERGE_PATCH(JSON_EXTRACT(attempts, CONCAT('$[', GREATEST(attempt_count - 1, 0), ']')), CAST(? AS JSON))), attempts) ``` 判定条件从"是否有错误码"改为**"该尝试是否已结束"**,语义更清晰:已结束的尝试记录不可变,终态码只写 `generations.error_code`;只有尚未结束的尾条尝试(租约、快照解码等非 provider 阶段失败)才补写终态信息。`GREATEST` 保证 `attempt_count = 0` 时 JSON 路径仍然合法。 顺带覆盖的一个额外场景:`saveOutputs` 失败时(provider 调用已成功),该次尝试现在保持成功记录不变,`generations.error_code` 记 `upstream_unknown`——如实反映"上游成功、本地落盘失败"。 ### 测试结果 新增 5 个测试,全部先验证**在未修复代码上会失败**: | 测试 | 覆盖 | |---|---| | `TestWorkerReportsFailoverExhaustedAfterRetryableFailure` | 单成员池 + 超时 → `failover_exhausted`,且该尝试保留 `upstream_timeout` | | `TestWorkerReportsFailoverExhaustedAfterAllCandidatesFail` | 两成员都可重试失败 → `failover_exhausted` | | `TestWorkerReportsRouteUnavailableWhenNoMemberIsEligible` | 成员禁用 → 仍为 `route_unavailable`,且 `begun` 为空 | | `TestWorkerKeepsNonRetryableProviderCodeAsTerminal` | 400 → 终态仍是 `upstream_bad_request`,只尝试一次 | | `TestMySQLFailKeepsFinishedAttemptErrorCode` | 集成测试:`Fail()` 后尾条尝试的 `error_code` / `retryable` / `latency_ms` 均未被改写 | 反向验证(临时还原修复后运行): ``` --- FAIL: TestWorkerReportsFailoverExhaustedAfterRetryableFailure terminal code = route_unavailable, want failover_exhausted --- FAIL: TestWorkerReportsFailoverExhaustedAfterAllCandidatesFail terminal code = route_unavailable --- FAIL: TestMySQLFailKeepsFinishedAttemptErrorCode provider attempt error_code = "failover_exhausted", want upstream_timeout (被终态码覆盖) ``` 全量验证(含 MySQL 集成测试,`CHORUS_TEST_DSN` 指向本机 3308 测试库): ``` go build ./... 通过 go vet ./... 通过 go test ./... 28 个包全部 ok(含 internal/core/queue、portal/worker) python dev_scripts/harness.py sync --check Wiki 镜像检查通过 python dev_scripts/harness.py check --strict DevHarness 检查通过 python -m unittest discover -s tests OK ``` ### 未验证部分 - **未做真实上游端到端复现**:修复行为由单元与集成测试覆盖,没有再造一次真实超时来观察线上记录。下一次真实失败时应核对 `generations.error_code` 是否为 `failover_exhausted`、`attempts` 尾条是否保留 `upstream_*`。 - **历史记录未回溯修正**:generation 6/7/9/11/12/13 的 `route_unavailable` 保持原样作为审计证据,不修改。 - **并发下的 `Fail()` 竞争未专门验证**:依赖既有的 lease_token CAS 谓词,本次未改动该谓词,也未新增并发测试。 - **`worker.go:182` 的 `remaining` 计算**:改用新变量后行为与原先一致(单次 `process` 内只算一次),但没有专门构造"claim 时已有尝试记录"的场景来验证跨轮次的正确性。 ### 提交哈希 - `52ebca2 fix: 修正生成失败错误码被终态码覆盖 (#63)` - `f4a17af docs: 区分 route_unavailable 与 failover_exhausted 的判读 (#63)` 均已推送至 `origin/main`。 ### 长期 Wiki 页面与 revision - `Troubleshooting` → `9803ea981876455054c5598a38b21fd030211d4c` 新增终态码判读表,明确区分 `route_unavailable`(一次上游都没调过)、`failover_exhausted`(调过且可重试但无候选可换)与 `upstream_*`(不可重试),并说明 `attempts` 中每条尝试的 `error_code` 不会被终态码覆盖——若看到 `error_code=route_unavailable` 却带 `retryable=true` 和真实 `latency_ms`,说明跑的是修复前的版本。 ## 风险和回退 - 触及 worker 终态判定与队列失败写入,属高风险区域:写错会让所有失败记录的错误码失真,比现状更难排查。 - 缓解:不改状态机、不改表结构、不改 `retryable` 判定;每一条验收标准都必须有对应单元测试,包含失败分支。 - 已产生的历史记录(generation 6/7/9/11/12/13)不回溯修正,保留原样作为审计证据。 - 回退:直接 revert 本工单提交,不涉及数据迁移。
Author
Owner

验收通过(2026-08-26)

用户于 2026-08-26 明确验收通过。

补充:真实运行证据(原列为未验证项)

工单"未验证部分"第一条"未做真实上游端到端复现",在 #64 的实施过程中被意外补齐。

2026-08-26 09:35,因 extra_body 白名单拒绝 resolution,generation 16 / 17 / 18 连续三次在毫秒级失败,运行的正是本工单修复后的二进制。以 generation 18 为例:

{"type": "provider", "retryable": false, "error_code": "upstream_unknown",
 "latency_ms": 2, "route_member_id": 5, "provider_ordinal": 1,
 "error_message": "upstream request failed"}

error_code / retryable / latency_ms 三者自洽:不可重试的本地配置错误、2 毫秒、未发出上游请求。终态 generations.error_code 同为 upstream_unknown,未覆盖该尝试自身的记录。

修复前的版本在这条路径上会把终态码合并进尾条尝试,产生与实际不符的记录。因此本条未验证项可以划掉:修复在真实运行中生效。

其余三条未验证项(历史记录未回溯、Fail() 并发竞争未专门验证、worker.go:182 跨轮次场景未构造测试)仍然有效,不因验收而消除。

提交

  • 52ebca2 fix: 修正生成失败错误码被终态码覆盖 (#63)
  • f4a17af docs: 区分 route_unavailable 与 failover_exhausted 的判读 (#63)

均已推送至 origin/main。部署二进制已于 2026-08-26 10:30 重新编译,包含本修复。

本工单关闭。

## 验收通过(2026-08-26) 用户于 2026-08-26 明确验收通过。 ### 补充:真实运行证据(原列为未验证项) 工单"未验证部分"第一条"未做真实上游端到端复现",在 #64 的实施过程中被意外补齐。 2026-08-26 09:35,因 `extra_body` 白名单拒绝 `resolution`,generation 16 / 17 / 18 连续三次在毫秒级失败,运行的正是本工单修复后的二进制。以 generation 18 为例: ```json {"type": "provider", "retryable": false, "error_code": "upstream_unknown", "latency_ms": 2, "route_member_id": 5, "provider_ordinal": 1, "error_message": "upstream request failed"} ``` `error_code` / `retryable` / `latency_ms` 三者自洽:不可重试的本地配置错误、2 毫秒、未发出上游请求。终态 `generations.error_code` 同为 `upstream_unknown`,未覆盖该尝试自身的记录。 修复前的版本在这条路径上会把终态码合并进尾条尝试,产生与实际不符的记录。因此本条未验证项可以划掉:修复在真实运行中生效。 其余三条未验证项(历史记录未回溯、`Fail()` 并发竞争未专门验证、`worker.go:182` 跨轮次场景未构造测试)仍然有效,不因验收而消除。 ### 提交 - `52ebca2 fix: 修正生成失败错误码被终态码覆盖 (#63)` - `f4a17af docs: 区分 route_unavailable 与 failover_exhausted 的判读 (#63)` 均已推送至 `origin/main`。部署二进制已于 2026-08-26 10:30 重新编译,包含本修复。 本工单关闭。
ila closed this issue 2026-08-26 10:34:27 +08:00
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: OPC/chorus#63