Skip to content

ScopedContext.transaction 不 join ambient 事务 —— 沙箱 hook 体里的 ctx.api.transaction() 会在事务中再开一个事务(违反 ADR-0067 D2) #6168

Description

@baozhoutao

发现于 #5351 / #5696 的实施(PD #10 范围外发现,未在该批 PR 内修改)。未认领。

事实(origin/main,packages/objectql/src/engine.ts)

ObjectQL.transaction() 的第一件事是 ADR-0067 D2 的 join 判定:

const ambient = this.txStore.getStore();
if (ambient?.transaction) {
  return callback({ ...(baseContext ?? {}), transaction: ambient.transaction }, { owned: false });
}

ScopedContext.transaction() —— 也就是沙箱 hook / action 体里的 ctx.api.transaction(fn) —— 没有这一支。它直接取默认驱动、直接 beginTransaction():

async transaction(callback, opts?) {
  const engine = this.engine as any;
  const driver = engine.defaultDriver ? engine.drivers?.get(engine.defaultDriver) : undefined;
  if (!driver?.beginTransaction) { /* 降级 */ }
  const trx = await driver.beginTransaction();   // ← 没有先看 ambient
  ...
}

该方法自己的 TSDoc 说它「是同一件事的第二份实现」,并逐条对齐了 ADR-0119 D1 的两条 caveat —— 唯独 join 这一条没有对齐。

后果

一个在 engine.transaction() 里被触发的 hook,其体内调用 ctx.api.transaction(fn) 会:

  1. 再开一个 driver 事务,也就是再要一条连接 —— 正是 ADR-0067 D2 写明要避免的那件事(单连接 SQLite 池上是死锁);
  2. 这个内层事务不在外层的回滚范围内:内层自己 commit,外层随后回滚,内层的写留下 —— D2 的原话是外层批量事务必须拥有唯一的一次 commit/rollback。

为什么现在才浮出来

#5696 给回调加了 owned 信号。引擎面按 D2 如实报告(join 时 false),而这一面永远报 true —— 它确实每次都自己开。信号是诚实的,被它诚实描述的行为不是。

可达性(诚实说明)

需要「沙箱 hook/action 体内显式调用 ctx.api.transaction()」+「该 hook 由一次 engine.transaction() 触发」同时成立。前者在示例 app 里没有用例可指;后者极常见(元数据发布、batchData 的 atomic 路径都开事务)。我没有真实部署里的调用证据,所以按 #4949 平铺记录、给 finding 标签,严重度交 PM 分诊 —— 但方向上它是持久性一类,不是功能一类:失败时看起来一切正常,回滚后留下不该留的行。

可能的修法(交分诊定,本单不预设)

ScopedContext.transaction 顶部加上与引擎面同形的 join 分支,join 时向回调报 { owned: false }⚠️ 需要先确认它与离散 beginTransaction/commit/rollback 三件套的关系 —— 三件套刻意不走 txStore(见 #6167),join 判定必须不把三件套的显式句柄误当成 ambient。

Refs:ADR-0067 D2、ADR-0119 D1、#4619#5696(owned 信号)、#5351#6167(同源校验对不可归属句柄弃权 —— 同一片沙箱事务面的另一半)。

Blocked-by: #6171

Serialization, added at triage: packages/objectql/src/engine.ts carries two stacked open drafts on the transaction paths — #6165 (#5696, base main) and #6171 (#5351, stacked on #6165). Selection skips this issue until #6171 merges; re-verify the join branch against the merged code before implementing.


Generated by Claude Code

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions