发现于 #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) 会:
- 再开一个 driver 事务,也就是再要一条连接 —— 正是 ADR-0067 D2 写明要避免的那件事(单连接 SQLite 池上是死锁);
- 这个内层事务不在外层的回滚范围内:内层自己 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
发现于 #5351 / #5696 的实施(PD #10 范围外发现,未在该批 PR 内修改)。未认领。
事实(origin/main,
packages/objectql/src/engine.ts)ObjectQL.transaction()的第一件事是 ADR-0067 D2 的 join 判定:ScopedContext.transaction()—— 也就是沙箱 hook / action 体里的ctx.api.transaction(fn)—— 没有这一支。它直接取默认驱动、直接beginTransaction():该方法自己的 TSDoc 说它「是同一件事的第二份实现」,并逐条对齐了 ADR-0119 D1 的两条 caveat —— 唯独 join 这一条没有对齐。
后果
一个在
engine.transaction()里被触发的 hook,其体内调用ctx.api.transaction(fn)会:为什么现在才浮出来
#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
Generated by Claude Code