Skip to content

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

Description

@os-zhuang

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

现象

packages/metadata/src/loaders/database-loader.tsnextEventSeq():

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,值得一并想清楚 —— 它不是「字节没落盘」,而是「落盘的字节是错的」,可能需要词表之外的第二条规则。

参考

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions