发现于 #6168 的实施(PD #10 范围外发现,未在该 PR 内修改)。未认领。
事实
#6168 / PR #6403 给 ScopedContext.transaction()(回调式)补上了 ADR-0067 D2 的 ambient join。实施中实读发现:QuickJS 沙箱面的 ctx.api.transaction(fn) 根本不走那个方法,因此不在该修复的覆盖范围内。
packages/runtime/src/sandbox/quickjs-runner.ts:639-660 里,VM 侧的 ctx.api.transaction 是一段 JS 糖,底下驱动三个 host leaf:__txBegin / __txCommit / __txRollback。__txBegin(:566-579)调的是离散三件套:
const begin = apiTx?.beginTransaction;
if (typeof begin === 'function') {
const r = await begin.call(apiTx) ?? null;
if (r) { txState.api = r.ctx; txState.handle = r.handle; }
}
而 ScopedContext.beginTransaction()(packages/objectql/src/engine.ts,三件套区)与修复前的回调面一样,没有 ambient 判定:直接 txDriver() 再 driver.beginTransaction()。
于是 #6168 描述的两条后果,在 VM 侧 body 上原样存在:
- 再要一条连接 —— 单连接池(knex/SQLite)上就是死锁;
- 内层由
__txCommit 自行 commit,写入存活过外层回滚 —— 外层 engine.transaction() 回滚后,VM 里写的行仍在,无报错无日志。
为什么不能顺手在 #6168 里一起修
三件套刻意不用 ALS:body 跨很多 host event-loop turn 执行(deferred promise + pump),setImmediate 边界上 AsyncLocalStorage 不存活 —— 这是三件套存在的全部理由,engine.ts 的三件套 TSDoc 与 ADR-0119 的「declared LIMIT」段都写明了。所以 __txBegin 里直接 txStore.getStore() 未必读得到外层 ambient(leaf 在 deferred 里跑,多半已不在那个 ALS 上下文里),不是加一个 if 就能了事。
至少需要决定:
- ambient 句柄在哪一刻捕获并显式穿进
txState(建 sandbox api 时?还是 hook dispatch 时?);
- join 之后
__txCommit / __txRollback 必须弃权(不能 commit 自己不拥有的事务),txState 得带 owned 位;
- body 显式 rollback 一个 joined 事务时的语义(标记外层必回滚?还是报错?)—— 这一条是契约级决定,不是实现细节。
这些都超出 #6168 的范围(该单锁定回调面),故另开。
可达性(诚实说明)
与 #6168 同款,且更窄一层:需要「沙箱 hook/action body 显式调 ctx.api.transaction()」+「该 body 由一次 engine.transaction() 触发」+「该 body 走 QuickJS 而非进程内」三者同时成立。示例 app 里没有可指的用例。方向上是持久性一类,不是功能一类:失败时看起来一切正常,回滚后留下不该留的行。
注:quickjs-runner.ts:567 已挡住 body 自己的嵌套(nested ctx.api.transaction is not supported),但那只看 txState.open,看不见宿主侧的外层事务。
不是同一件事,别合并。#6167 是同源校验对不可归属句柄弃权(transactionCoversDriverFor 判不了引擎没开过的句柄);本单是嵌套时不 join。两者都落在三件套这片面上,#6167 的收口方向(让句柄属主在 IDataDriver 上可查)不解决本单,反之亦然。
Refs:#6168(回调面已修,PR #6403)、#6167、ADR-0067 D2、ADR-0119 D1 的 declared-LIMIT 段、#5696(owned 信号)。
发现于 #6168 的实施(PD #10 范围外发现,未在该 PR 内修改)。未认领。
事实
#6168 / PR #6403 给
ScopedContext.transaction()(回调式)补上了 ADR-0067 D2 的 ambient join。实施中实读发现:QuickJS 沙箱面的ctx.api.transaction(fn)根本不走那个方法,因此不在该修复的覆盖范围内。packages/runtime/src/sandbox/quickjs-runner.ts:639-660里,VM 侧的ctx.api.transaction是一段 JS 糖,底下驱动三个 host leaf:__txBegin/__txCommit/__txRollback。__txBegin(:566-579)调的是离散三件套:而
ScopedContext.beginTransaction()(packages/objectql/src/engine.ts,三件套区)与修复前的回调面一样,没有 ambient 判定:直接txDriver()再driver.beginTransaction()。于是 #6168 描述的两条后果,在 VM 侧 body 上原样存在:
__txCommit自行 commit,写入存活过外层回滚 —— 外层engine.transaction()回滚后,VM 里写的行仍在,无报错无日志。为什么不能顺手在 #6168 里一起修
三件套刻意不用 ALS:body 跨很多 host event-loop turn 执行(deferred promise + pump),
setImmediate边界上 AsyncLocalStorage 不存活 —— 这是三件套存在的全部理由,engine.ts的三件套 TSDoc 与 ADR-0119 的「declared LIMIT」段都写明了。所以__txBegin里直接txStore.getStore()未必读得到外层 ambient(leaf 在 deferred 里跑,多半已不在那个 ALS 上下文里),不是加一个 if 就能了事。至少需要决定:
txState(建 sandbox api 时?还是 hook dispatch 时?);__txCommit/__txRollback必须弃权(不能 commit 自己不拥有的事务),txState得带owned位;这些都超出 #6168 的范围(该单锁定回调面),故另开。
可达性(诚实说明)
与 #6168 同款,且更窄一层:需要「沙箱 hook/action body 显式调
ctx.api.transaction()」+「该 body 由一次engine.transaction()触发」+「该 body 走 QuickJS 而非进程内」三者同时成立。示例 app 里没有可指的用例。方向上是持久性一类,不是功能一类:失败时看起来一切正常,回滚后留下不该留的行。注:
quickjs-runner.ts:567已挡住 body 自己的嵌套(nested ctx.api.transaction is not supported),但那只看txState.open,看不见宿主侧的外层事务。与 #6167 的关系
不是同一件事,别合并。#6167 是同源校验对不可归属句柄弃权(
transactionCoversDriverFor判不了引擎没开过的句柄);本单是嵌套时不 join。两者都落在三件套这片面上,#6167 的收口方向(让句柄属主在IDataDriver上可查)不解决本单,反之亦然。Refs:#6168(回调面已修,PR #6403)、#6167、ADR-0067 D2、ADR-0119 D1 的 declared-LIMIT 段、#5696(
owned信号)。