Skip to content

单 id update 把同一行前置状态读了 3 次(engine 前置行门 + sys_fetch_previous_update + plugin-audit captureBefore),且后两次不受任何按对象需求门约束 #5846

Description

@baozhoutao

发现于 #5284(PR 见该单:把 update() 单 id 前置行门由全局改为按对象)。属 PD #10 的范围外发现,未在该 PR 内修改

事实(origin/main,已按文件逐处核对)

一次单 idupdate() 在一个装了 plugin-audit 的普通 kernel 上,同一行的前置状态被读 3 次:

  1. packages/objectql/src/plugin.ts — 内建 hook sys_fetch_previous_update(object: '*',events: ['beforeUpdate'],priority 5)在 hookCtx.input.id 存在时无条件ql.findOne,赋给 hookCtx.previous;
  2. packages/plugins/plugin-audit/src/audit-writers.tscaptureBefore(注册在 beforeUpdate/beforeDelete,object 选项 ⇒ 全局)对同一 id 再 ql.findOne 一次,赋给 ctx.__previous(注释自陈原因:「HookContext.previous 官方有类型但引擎不总是填」);
  3. packages/objectql/src/engine.tsupdate() 单 id 分支自己的前置行读(update() 的前置行门是全局的(hooks.get('afterUpdate').length > 0),任一对象注册 afterUpdate 就让所有对象的单 id update 多付一次读 #5284 之后按对象判定需求:校验规则 / 本对象 afterUpdate hook / roll-up 汇总),写进 priorRecord,并在写入后覆盖 hookContext.previous

其中 1、2 都是引擎读(ql.findOne,过完整读管线:中间件、RLS、字段掩码),3 是 driver.findOne。1、2 都不看任何需求门 —— 只要有 id 就读。

两条互相独立的后果

(a) 三读同一行。#5272 已经在 delete 侧把这件事做对了:引擎在派发 beforeDelete之前读一次前置行并绑定 previous,两个阶段共用一次读。update 侧仍是「先派发 beforeUpdate,写完才绑 previous」,于是 before 阶段的两个消费者只能各自去读。把 update 侧对齐 delete 侧的时序,1 和 2 都可以退休(2 的存在理由 —— 引擎不填 —— 届时不再成立)。

(b) 全局注册让「按对象」的需求门恒真。 plugin-audit 的 afterInsert/afterUpdate/afterDelete 三个 writeAudit 注册也不带 object,而 hasHooksFortriggerHooks 的语义把「无 object 的注册」正确地当作命中每个对象。于是只要 plugin-audit 启用:

writeAudit 自身是在 handler 里用 SKIP_OBJECTS 过滤的,也就是说「哪些对象要审计」这件事平台已经知道,只是没有表达在注册面上。把它表达出来(注册时给出 object 列表,或提供一个「本对象是否被审计」的谓词供门查询),按对象门才真正生效。

影响与严重度

不是正确性缺陷:previous 语义处处正确,只是同一行被读多次、按对象门被架空。严重度我判断不准(没有真实负载测量数据),按 #4949 平铺记录,交 PM 分诊定级。可达性没有疑问 —— 单 id update 是最常见的写路径,plugin-audit 是默认装配的一部分。

关联

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions