Skip to content

check:durability-log-level 分不清「故障已答给调用方」和「故障被吞掉」—— 词表加 saveMetaItem 后逼出 2 条基线,记的全是正确代码 #5241

Description

@os-zhuang

发现于 #4754 的实现期(PR 分支 claude/issue-4754-savemetaitem-durability-wordlist)。未认领 —— 只是记录。观察类:今天没有用户会撞到,代价是一份本不该存在的基线。

现象

#4754 把 saveMetaItem 加进 DURABILITY_CRITICAL_CALLEES。修掉闸门自身两个精度缺陷后,命中从 8 降到 4,其中:

  • 1 处是真丢失(packages/runtime/src/domains/packages.ts 的 ADR-0045 可见性翻转:HTTP 200 + unhideError 埋在 body 里,零日志)—— 已按范本改成 error;
  • 3 处不是降级,它们把故障原样答给了调用方:
    • packages/runtime/src/domains/meta.ts — catch → deps.errorFromThrown(e, 400),调用方拿到带 issues 的 4xx/422;
    • packages/metadata-protocol/src/protocol.ts ×2 — migrateStoredItems 的 report.failed++ + 逐项 outcome:'failed';复制入包的 failed.push() + 聚合 success:false / failedCount。

按 AGENTS.md 的判据原问 ——「降级之后系统从外面看还正常吗?」—— 这 3 处答案都是否:请求方被明确告知这次写入没有落盘。它们根本不是降级,而是错误传播。但闸门的模型只认「loud 日志 or rethrow」,表达不了「已答给调用方」,于是这 3 处只能进 scripts/durability-degradation.baseline.json。

为什么这是个问题,而不是「基线就是干这个的」

基线文件自己的表头写着「Every entry names WHY it is still here and WHAT closes it」,而且是 shrink-only。给正确代码写基线条目有两个后果:

  1. 这些条目永远关不掉(代码没毛病),shrink-only 的账本从此有了一批不会缩的行,「基线 = 待还的债」这个语义被稀释;
  2. 更糟的是它给下一个作者立了个范例:这个闸门会误报,遇红先加基线。而闸门脚本自己的注释恰恰写着,误报率高到让人绕过的闸门「worth less than no gate, because it also reports success」。

还有第三个后果,是这次差点踩到的:面对 meta.ts 那处误报,最省事的「修法」是补一句 logger.error —— 而那条路径最常见的情况是作者提交了不合 spec 的 body,于是每一次校验拒绝都会打一条持久性 error。这正是 AGENTS.md 点名的镜像错误(「trains everyone to skim error」),也正是 #4420 那条 warn 当初没人读的成因。一个闸门,最省力的满足方式是有害的,那闸门的形状就有问题。

根因:saveMetaItem 和词表里其他条目不是一类

现有词表条目(syncSchema、writeRecord、writeDeferredReference、rearmSuspendedWaitTimers、dropPromotedDraftRow、deliverPersistedRow…)清一色是背景副作用:调用方并不在等这一次写的结果,某个更外层的操作会照常报成功 —— 所以「catch 必须响」对它们无一例外地成立。

saveMetaItem 不同:8 处命中里 7 处是调用方直面的主操作(HTTP 写元数据、批量迁移/复制),只有 1 处是搭别人便车的副作用。#4669 那个真事故也正是副作用型。词表按 callee 名字匹配,而这个 callee 横跨两类,精度就掉了。

建议方向(裁决留给维护者,本卡不预设)

  1. 给闸门加一份「故障传播词汇」(和 DURABILITY_CRITICAL_CALLEES 一样是显式声明的,不猜):一个 catch 若每条路径都把故障交了出去,就等价于 rethrow。HTTP 形状好办 —— errorFromThrown / sendError 是专名;难的是 protocol.ts 那种结构化逐项结果报告(report.failed++、failed.push({...})),按名字识别就退回成脚本注释明确拒绝的启发式了;
  2. 收窄词表:承认这一族要区分「副作用型写」和「主操作型写」,而 callee 名字做不到这个区分 —— 那就接受 saveMetaItem 只能靠基线管住,并把基线表头的语义从「待还的债」放宽成「已复核的例外」,明说这个取舍;
  3. 维持现状(3 处基线),只把这份取舍记在案。

我倾向 1,但它对第二种形状(结构化结果报告)是否可机械判,我没有把握 —— 这正是本卡不自作主张的原因。

现状

#4754 的 PR 已经落了这些,本卡不阻塞它:

本卡关闭时,应能删掉那 2 条基线条目(键是 file::callee,3 处站点合成 2 个键)。

关联

#4632(立规矩)、#4669(副作用型的那次真事故)、#4754(加词表 + 清账)、#5186(同一闸门的另一个盲区:读接缝的漏报;本卡是反方向的误报)、AGENTS.md「Degradation log levels — warn vs error」。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions