Skip to content

[metadata] nextEventSeq() 把驱动读失败也当成「表还没建」,静默从 1 重新发号 —— #4632 同形,机械检查覆盖不到 #4825

Closed

Description

@os-zhuang

发现于 #4728(修 database-loader.ts 的 ensureSchema() 静默吞 DDL 失败)。仅记录,未在该 PR 中修改 —— 与本单无关,按 Prime Directive #10 单开。

现象

packages/metadata/src/loaders/database-loader.ts 的 nextEventSeq():

private async nextEventSeq(): Promise< number > {
  const where: Record< string, unknown > = this.organizationId
    ? { organization_id: this.organizationId }
    : {};
  try {
    const rows = await this._find(this.historyTableName, { where });
    let max = 0;
    for (const row of rows as Array< { event_seq?: number | null } >) {
      const v = typeof row.event_seq === 'number' ? row.event_seq : 0;
      if (v > max) max = v;
    }
    return max + 1;
  } catch {
    // Table not provisioned yet or driver error — start at 1.
    return 1;
  }
}

为什么值得单开

这是 #4728 刚修掉的同一种形状,只是在下一层,而且 #4632 的机械检查看不到它(_find 不在 DURABILITY_CRITICAL_CALLEES 词表里):

  • 注释同时点名了两种原因 —— 「表还没建」(良性:此时确实该从 1 开始)和「驱动错误」(不良性),然后用同一个 return 1 对待两者;
  • 历史表里已经有 N 行时,一次瞬时读失败(连接抖动、超时、权限)会让下一条历史记录拿到 event_seq = 1,与既有行直接撞号;
  • event_seq 是历史排序/回滚定位的依据,撞号之后版本顺序就是错的,而写入本身成功、日志一行没有 —— 系统对外完全正常。

nextEventSeq 的 TSDoc 已经承认它是 legacy、非事务、并发会撞号(canonical 是 SysMetadataRepository),所以这里不是「必须马上修」,但「并发撞号」和「读失败静默重置到 1」是两回事:前者被记录过,后者没有。

建议

按 #4632 的判定问句处理,任选其一:

  1. 只在确实是表不存在时返回 1(按错误类型判别 —— [metadata] database-loader 吞掉 sys_metadata 的 DDL 失败后仍置 schemaReady=true —— 第二类降级(#4632 规则),本轮因包冻结未修 #4728 落地的 packages/metadata/src/utils/schema-sync-errors.ts 是同类判别的现成范式),其余读失败以 error 上报后果(历史序号将从 1 重新发号,与既有行撞号,版本顺序不可信)并抛出,让调用方决定;
  2. 或者确认该 legacy 路径已无生产调用方,直接删除,把历史写入统一收敛到 SysMetadataRepository。

顺带:如果要动这个词表,_find 这类「读出来决定后续写什么」的调用是否该进 DURABILITY_CRITICAL_CALLEES,值得一并想清楚 —— 它不是「字节没落盘」,而是「落盘的字节是错的」,可能需要词表之外的第二条规则。

参考

Activity

  1. os-zhuang commented on Aug 3, 2026

    @os-zhuang
    ContributorAuthor

    PM 分诊:pm:queue + domain:engine(落点 packages/metadata)。本会话不派发 —— 维护者已指示本波在飞任务收完即暂停,本单留给下一个 engine 域会话。

    分诊结论:这条比它的来源单 #4728 更值得注意,理由是失败方向不同。

    #4728 是「声称持久化、实际没落盘」—— 坏在缺失。本条是「落盘了,但落的字节是错的」:历史表已有 N 行时一次瞬时读失败让下一条拿到 event_seq = 1,与既有行撞号,而 event_seq 正是历史排序与回滚定位的依据。写入成功、日志无痕、系统对外完全正常 —— 版本顺序从此不可信,却没有任何东西会说出来。

    正文里那句提醒尤其准,我原样转给接手者:

    如果要动这个词表,_find 这类「读出来决定后续写什么」的调用是否该进 DURABILITY_CRITICAL_CALLEES,值得一并想清楚 —— 它不是「字节没落盘」,而是「落盘的字节是错的」,可能需要词表之外的第二条规则。

    这是今晚 #4632 那条规则第一次被它自己的应用者指出边界:词表建在「持久化调用」上,而这一类的危害发生在读上,读的结果决定了写什么。#4777(把词表扩到启动期注册表判断)是同一个方向的另一个延伸,两条可以一起考虑 —— 但 #4777 在 domain:devx 车道,本条在 engine,跨域协调请走 #4604。

    修法方向按正文的两条:优先方案 1(按错误类型判别,packages/metadata/src/utils/schema-sync-errors.ts 是 #4728 刚落地的现成范式),或方案 2(确认无生产调用方后直接删除,收敛到 SysMetadataRepository)。先确认这条 legacy 路径还有没有生产调用方 —— 若没有,方案 2 更彻底,别花力气修一段该删的代码。


    Generated by Claude Code

  2. claude commented on Aug 3, 2026

    @claude
    Contributor

    认领:PM 循环第 7 轮
    会话:session_015Br2xsJsczFsTR9bvbh2Ny
    分支:claude/issue-4825-next-event-seq-loud
    Worktree:objectstack-issue-4825

    域:engine
    文件面:packages/metadata/src/loaders/database-loader.ts

    并行批隔离:本轮同批的另三条落在互不相交的包 —— #4825 packages/metadata、#4813 packages/core、#4808 packages/plugins/plugin-auth、#4820 packages/services/service-analytics。⚠️ #4821(mergeByDimensions 分隔符)与 #4820 同文件,未同批派发,串到下一轮以避开撞车。


    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