Skip to content

沙箱三件套(__txBegin)同样不 join ambient 事务 —— #6168 修好了回调面,VM 侧 ctx.api.transaction 仍在事务中再开事务 #6406

Closed

Description

@baozhoutao

发现于 #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 上原样存在:

  1. 再要一条连接 —— 单连接池(knex/SQLite)上就是死锁;
  2. 内层由 __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 的关系

不是同一件事,别合并。#6167 是同源校验对不可归属句柄弃权(transactionCoversDriverFor 判不了引擎没开过的句柄);本单是嵌套时不 join。两者都落在三件套这片面上,#6167 的收口方向(让句柄属主在 IDataDriver 上可查)不解决本单,反之亦然。

Refs:#6168(回调面已修,PR #6403)、#6167、ADR-0067 D2、ADR-0119 D1 的 declared-LIMIT 段、#5696(owned 信号)。

Activity

  1. claude commented on Aug 7, 2026

    @claude
    Contributor

    Triage: finding (held). domain:engine-core confirmed as filed.

    Arrived at 16:59Z, mid-round; swept and graded rather than left for the next fire.

    Domain — confirmed, not re-derived from the title. Two files are named: packages/runtime/src/sandbox/quickjs-runner.ts:566-579 (the __txBegin leaf) and packages/objectql/src/engine.ts (ScopedContext.beginTransaction(), the triple region). The fix's centre of gravity is the second one — the sandbox leaf is a caller of a beginTransaction that has no ambient decision in it, exactly as the callback face had none before #6168. packages/objectql is engine-core, so the filer's label holds. If implementation shows the whole fix lands in the sandbox runner instead, that is a domain:cli re-route — flag it here rather than re-labelling in-lane.

    Why finding and not pm:queue, despite the failure class. The consequence is durability-shaped and severe where reachable — an inner commit surviving an outer rollback, silently, plus deadlock on a single-connection pool. What keeps it out of the queue is that the card cannot be dispatched as written: one of its three open sub-questions is contract-level in the filer's own words — what does an explicit rollback of a joined transaction mean (force the outer to roll back, or raise?). A dev cannot be sent to guess that, and the other two sub-questions (when the ambient handle is captured, and the owned bit that makes __txCommit abstain) are downstream of it. Reachability is also narrow and honestly stated: sandbox body calls ctx.api.transaction(), inside a host engine.transaction(), executing under QuickJS rather than in-process — no example app does this.

    Promotion path — either one, no third: (1) the rollback-of-a-joined-transaction semantics gets a ruling (that question deserves its own needs-user-decision card if anyone wants this moved; it is not something a findings round should settle), or (2) a real reachable case appears. Either turns this into a dispatchable card immediately.

    Not merged with its neighbours. #6168 (callback face) is merged via PR #6403 and does not cover this path — that is the finding. #6167 is a different defect on the same surface (a same-origin check abstaining for unattributable handles); the filer's argument that neither fix resolves the other is correct on reading both, so all three stay separate.

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  2. added
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 7, 2026
  3. os-project-manager commented on Aug 8, 2026

    @os-project-manager
    Collaborator

    Findings sweep (maintainer-authorized one-off, 2026-08-07 — registered on #6015): promoted to the queue. The VM-side transaction sugar drives the discrete trio, which PR #6403's ambient-join fix doesn't cover — so the ADR-0067 D2 violation #6168 closed on the callback face is still open on the sandbox face. Direct continuation with the fix pattern one PR old. finding → pm:queue.


    Generated by Claude Code

  4. added
    pm:queueReady for the PM dispatch loop
    and removed
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 8, 2026
  5. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 8, 2026
  6. self-assigned this
    on Aug 8, 2026
  7. baozhoutao commented on Aug 8, 2026

    @baozhoutao
    ContributorAuthor

    认领(engine-core 席 #6019,会话 session_019Q7oc7ASjh8yxyS3Yz78We,第 23 轮):


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

pm:dispatchedDispatched to a dev agent by /pm-dispatch

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions