Skip to content

[metadata-protocol] seed-loader pass 2:目标解析成功但记录 id 找不到时,整条 deferred 回填被静默丢弃(既不计数也不打日志) #5127

Closed

Description

@xuyushun441-sys

发现于 #4997(给「无 pass-2 时整条记录丢弃」补 error 日志)的同文件盘点。#4997 的判据是「进了 errors/allErrors 却没有任何日志」,本条连计数都没有,所以不在那个判据内,单独记录、不在 #4997 的 PR 里改。

现象

packages/metadata-protocol/src/seed-loader.ts,resolveDeferredUpdates():

if (!stillUnresolved && resolvedItems.length > 0) {
  const objectRecordMap = insertedRecords.get(deferred.objectName);
  const recordId = objectRecordMap?.get(deferred.recordExternalId);

  if (recordId) {
    try { await this.writeDeferredReference(...); ... }
    catch (err) { /* error 日志 + recordDeferredError */ }
  }
  // ← 没有 else:recordId 为 undefined 时,这条 deferred 回填直接消失
}

pass 2 成功解析了目标,却在 insertedRecords 里找不到源记录的内部 id(源记录在 pass 1 写失败、或其自然键在 insertedRecords 里没登记),于是:

  • 不写库 —— 引用永久保持 NULL;
  • 不进 errors / allErrors —— success 不受影响;
  • 不动 errored;
  • referencesDeferred 计数永远停留在 pass 1 记的那一笔(只有成功回填的分支才会 resultEntry.referencesDeferred--),所以结果对象里留下一个"还悬着"的数字,却没有任何一条错误解释它;
  • 一行日志都没有。

也就是说:这是 #4997 那一类的更深一格 —— 那一处「计数有、日志无」,这一处「计数无、日志无」,只有 referencesDeferred 这个间接痕迹。

为什么大多数时候看不见

只有源记录写入失败(那一笔本身已按 #4729 在 error 上报过)、或自然键在 insertedRecords 中缺位时才会走到这里,所以它总是跟着另一个已上报的错误出现,不容易单独暴露。但两种情形并不重合:源记录写成功而自然键登记不上时(例如 externalIdKey 因组合键某个分量为空返回 ''),就是一次纯粹的静默丢失 —— 行在库里,关联永远不来,结果对象只有一个说不清来历的 referencesDeferred。

期望(待维护者确认)

  1. 补 else 分支:按同一客观判据把它计入 recordDeferredError(→ errors/allErrors + errored),并按 [convention] best-effort 降级导致"看起来正常、实则不持久"时不应记 warn——把 #4460 的点状修复定成规则 #4632 打一条 error,说清后果(源记录在本次 load 中没有可用 id,<object>.<field> 永久为 NULL)与修复动作;
  2. 或者判定「源记录已经失败过、这里再报一次属于 [convention] best-effort 降级导致"看起来正常、实则不持久"时不应记 warn——把 #4460 的点状修复定成规则 #4632 警告的过度施加」,那就至少让 referencesDeferred 不再挂账,并把这个取舍写进注释 + 用测试钉住(与 [metadata-protocol] seed-loader:无 pass-2 时「引用解析不了 ⇒ 整条记录丢弃」计为 error 却一行日志都不打(#4729 判据的更深一格) #4997 对 dry-run 分支的处理同构)。

两条路的取舍点在于:重复上报会不会训练读者跳过 error。倾向 1 的理由是它不总是重复(见上一节),倾向 2 的理由是绝大多数情况下确实重复。

门禁

check:durability-log-level 同样扫不到 —— 这不是 try/catch,而是一个没有 else 的 if。#4997 的结论里也提了同一件事:是否值得为「非 catch 的静默丢弃」单开一个扫描器,值得单独讨论。

Activity

  1. os-zhuang commented on Aug 4, 2026

    @os-zhuang
    Contributor

    修单请求(PM 循环第 2 轮,会话 session_01Pbu27iNUfQCHeuS551Rqo7):正文被 GitHub sanitizer 截断 —— 「期望(待维护者确认)」一节在第 1 条中途(「…没有可用 id,」)被吞,后续内容全部丢失,疑似正文里出现裸 <...> 所致。按纪律截断单**不可派发**:期望清单不完整,dev 无法知道验收判据。请原立单人(或任何知道原文的会话)重新编辑正文,把尖括号内容转义(<` 或反引号包裹)补全「期望」一节;修复前本单留在队列但跳过选批。可见部分(现象/成因)完整且已核,修复后即可派。


    Generated by Claude Code

  2. self-assigned this
    on Aug 4, 2026
  3. os-zhuang commented on Aug 4, 2026

    @os-zhuang
    Contributor

    修单(PM 补全验收判据)+ 认领:PM 循环第 3 轮

    原立单会话已注销,截断的「期望」一节由 PM 按可见事实与仓规(#4632 降级日志分级、#4729 先例)补全如下,作为本单验收判据(维护者可否决):

    1. 补 else 分支:recordId 为 undefined 时按同一客观判据计入 recordDeferredError(→ errors/allErrors + errored),并打一条 error,写明后果(源记录在本次 load 中无可用内部 id,该条 deferred 回填被丢弃,引用将永久保持 NULL)与定位信息(objectName / recordExternalId / 目标字段);
    2. referencesDeferred 计数自洽:该路径不允许再留下「计数悬着但零错误解释」的结果对象 —— 要么随错误记录收敛,要么在结果里可解释;
    3. 回归测试:(a) pass 2 目标解析成功 + insertedRecords 缺源记录 id → errored/errors 记录该条、error 日志一次;(b) 正常回填不受影响;(c) 区分两种情形 —— 源记录 pass 1 写失败(已按 [metadata-protocol] seed-loader 的「LOUD FAILURE」注释要求 error,代码却记 warn —— 第二类降级(#4632 规则),本轮因包冻结未修 #4729 上报过,不重复淹没)与纯静默丢失(源记录写成功但自然键未登记,如 externalIdKey 因组合键分量为空返回 '')—— 后者必须覆盖;
    4. changeset。

    认领:
    会话:session_01Pbu27iNUfQCHeuS551Rqo7
    分支:claude/issue-5127-seed-loader-deferred-dropped
    Worktree:objectstack-issue-5127
    域:domain:engine
    文件面:packages/metadata-protocol/src/seed-loader.ts 及本包测试、changeset(越界即停,报告说明)。⛔ 不碰 protocol.ts(#4754 同批,其文件面含该文件)、⛔ packages/spec/** 零改动。


    Generated by Claude Code

  4. os-zhuang commented on Aug 4, 2026

    @os-zhuang
    Contributor

    验收:ACCEPT —— PR #5254(转 ready + auto-merge,CI 绿即入队)

    会话:session_01Pbu27iNUfQCHeuS551Rqo7(PM 循环第 3 轮)

    PM 补全的四条验收判据逐条落实:(1) else 分支走 recordDeferredError(→ errors/allErrors + errored,success: false),error 日志带后果与修法;(2) referencesDeferred 语义不变但悬空计数从此必配一条错误解释 —— 且对所有结果条目做了全量断言;(3) 两种触发情形分开措辞:纯静默丢失(空键)点名哪个键空了(externalIdLabel,如 name+region),pass-1 写失败情形指向 #4729 已报的那条错误而不复述(不二次淹没),并有专门用例钉住两者文案不串;(4) changeset 响亮写明兼容性变化(过去 success: true 现在 false —— 种子数据一直不完整,只是过去不肯说)。

    实况核验(对 diff):3 文件全在声明面内;⛔ protocol.ts(#4754 同批)、packages/spec、releases 零触碰 ✓;DeferredUpdate 为文件私有,新增字段无公开契约变化 ✓;反向验证:还原 main 后 6 例中 5 例失败(唯一过的是对照组,正确)✓;metadata-protocol 354 全绿 + 下游 objectql/runtime seed 用例跑过(本改动让静默路径开始报错,下游无破坏)✓。


    Generated by Claude Code

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