Skip to content

fix(events): Node 保持不可重入,快速路径上的 emit 排在被扣住的同名调用之后;等待图记入口,修复可重入运行时上查不出的卡死 - #527

Merged
eric8810 merged 2 commits into
mainfrom
fix/node-emit-hold-order
Oct 21, 2026

Conversation

@eric8810

@eric8810 eric8810 commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

#520 的故障评审发现的回退(阻塞 0.15):#520 让 Node 在同步等待期间就地执行 emit 方式的 events.call,Node 因此变得可重入,带来两个问题。

  1. 宿主的环检测看不到的卡死:C 同步调用 A.x,x 同步调用 B.slow;这时一个无关的 emit 让 A 的监听器压在 x 的等待上面执行,监听器同步调用 c.y;C 不可重入,扣住 c.y,谁也动不了,也不报 SyncWaitCycle。
  2. 破坏 JS 的 run-to-completion:监听器可能在同步调用中途看到改了一半的插件状态,与“不可重入”的设计(多语言设计 §9、TLA+ 模型、开发者无感)矛盾。

修正

Node 保持不可重入,§5.5 照样成立

  • 撤回 fix(events): Node 按帧序就地开始 emit 方式的监听器调用,补同步等待用例、回复原语测试与文档 #520 对 session.mjs / runner.mjs 的改动:同步等待期间,emit 方式的 events.call 和其他不在等待链上的调用一样被扣住,等待结束后按帧的顺序执行。
  • 会话新增 behind(key, run) 和选项 holds:运行时把 emit 方式的 events.call 标上事件名。总线在快速路径上发出 emit 时,如果同名的调用还被扣着没执行,就把这次本地分发排在它们后面(#queued 里的一项,等待期间同样扣住)。所以 emit/order-across-fast-path-sync-wait 仍是 a:1 先于 a:2,并且走快速路径(viaHost: false)。
  • 总线文件三处同一份(Node 运行时、Node SDK、Bun),一起改。

Bun / Python:同样的卡死确实存在,已修正

它们按设计在同步等待期间执行所有调用,并声明 reentrant-sync 与 sync-stack。但宿主的等待图只记录“为同步等待的调用方转发的调用”。宿主自己发出的 events.call(以及任何异步调用)不在图里,所以监听器被压在 x 上面这件事,栈顺序那类边看不到。集成测试在 main 上对 Python、Bun 都会卡住(10 s 超时)。

  • waits.rs:有调用被标记时,没有调用方同步等待、又进入 sync-stack 运行时的调用,记为入口。栈顺序对入口成立:c 等在它之后进入、并已开始执行的入口。入口等它之下发出的同步调用。没有谁按调用链等入口,因为它的调用方不等它。这样上面的环 c.y → x → 入口 → c.y 能被查到,回调得到 SyncWaitCycle,其余照常完成。

  • TLA+ 模型(specs/cross-runtime-sync)加入 Rust 的投递(MaxPosts、posts,即入口),TrueSucc / RustSucc 把入口算进图。新增三个配置:

    • current-posts:卡死(反例 5)
    • policy-b-posts:通过(253,235 个状态);不记入口时,同一配置在策略 B 下卡死
    • policy-b-posts-node:p 不可重入(Node),通过(152,438 个状态)

    已有配置加 MaxPosts = 0。本地用 tla2tools v1.7.4(sha256 与 README 一致)跑了全部配置,结果都与 expected.txt 一致,通过的配置状态数不变。

测试

  • node/rutis-runtime/test/session.test.mjs:同步等待期间 emit 方式的调用不在等待上面执行(监听器看到的是完成后的状态,回复顺序正确);behind 按帧的顺序排在被扣住的同名调用之后,没有被扣住的调用时由调用方直接执行。

  • waits.rs::tests::a_listener_run_on_top_of_a_wait:可重入时成环;不可重入时不成环;没有谁按调用链等入口。

  • crates/rutis-loader/tests/sync_cycles.rs::an_emit_listener_run_while_a_call_waits,A 分别是:

    • Python、Bun:监听器的回调得到 SyncWaitCycle,其余完成;
    • Node:监听器在 x 返回后才执行,回调成功。

    已核实:在 main 上 Python、Bun 卡住;用 fix(events): Node 按帧序就地开始 emit 方式的监听器调用,补同步等待用例、回复原语测试与文档 #520 的 Node 代码时,Node 那一项的断言失败。

  • 一致性套件:emit/order* 在 node、node-leaf 两列(全部摆放)通过;整套(sampled)见下方 CI。

文档

  • 事件协议 §5.5(中英):撤回“Node 在同步等待期间对监听器就地处理”的说法,改为“Node 扣住,快速路径上的同名 emit 排在其后”(2026-10-21 修订)。
  • 迁移说明 0.15(中英)、runtime-contract.md:同上。
  • 多语言设计 §9(中英):等待图的入口。
  • 保证表(中英):bridge/sync-wait-cycle-refused 补两个测试。
  • 模型 README(中英):投递与入口、反例 5、新配置。

@eric8810 eric8810 added this to the 0.15 milestone Oct 21, 2026
- Node 保持不可重入:emit 方式的 events.call 在同步等待期间照旧被扣住;会话新增 behind,本进程快速路径上的同名 emit 排在被扣住的调用之后,按帧的顺序执行(§5.5)
- 等待图:没有调用方同步等待、进入 sync-stack 运行时的调用记为入口,栈顺序对它成立;修复可重入的 Python / Bun 把监听器压在等待上面、回调不可重入运行时时查不出的卡死
- TLA+ 模型加入 Rust 的投递(入口),新增 current-posts 等四个配置
- 测试:Node 会话单元测试、waits 单元测试、sync_cycles 集成测试(Python / Node / Bun)
- 文档:§5.5 撤回 Node 对监听器可重入的说法;迁移说明、运行时约定、多语言设计 §9、保证表
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant