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

Labels

bugSomething isn't workingpm:dispatchedDispatched to a dev agent by /pm-dispatchpm:queueReady for the PM dispatch loop

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions