Skip to content

fix(tests): 两个反复出现的偶发失败——rutis-dev 的套接字替换、Windows 上运行时被杀后的重启 - #501

Merged
eric8810 merged 1 commit into
mainfrom
fix/flakes-dev-socket-crash
Oct 20, 2026
Merged

eric8810 merged 1 commit into
mainfrom
fix/flakes-dev-socket-crash

Conversation

@eric8810

Copy link
Copy Markdown
Contributor

问题

  1. crates/rutis-dev/tests/channel.rs::a_live_socket_is_not_replaced_but_a_stale_one_is 偶发失败(feat(swift): Swift SDK 的会话层与值(#386) #491、feat(swift): 远程运行时 listen: 与 dial:,一致性的 Swift 列(#387) #496;run 39245891413 / job 118037687992,自托管 Linux):第二次 DevChannel::start 报 a dev channel already listens on /tmp/.tmpXXXX/dev.sock(AddrInUse),路径在测试自己的临时目录里,不是别的测试或别的作业占了路径。
  2. tests/e2e runtime_crash.rs::a_killed_runtime_fails_its_calls_and_a_restart_recovers 在 Windows 偶发失败(同一 run,job 118037688593)。日志里的 panic 是 process 2812 is not one of 16416's(tests/e2e/src/lib.rs 的 Scenario::kill),发生在杀 Python 运行时这一步,与重启后复用端口无关。

根因

  1. 测试用 std::os::unix::net::UnixListener::bind 绑定后立刻丢弃,来造“崩溃留下的旧套接字”。同一测试二进制里 the_cli_keeps_the_row_id 并行地启动 rutis-dev 子进程;子进程从 fork 到 exec 之间持有父进程所有描述符的副本(CLOEXEC 要到 exec 才关),刚丢弃的监听套接字在这段时间仍在监听,于是 connect 成功,DevChannel 正确地判定“有人在监听”。产品逻辑没有问题,是测试造出的“旧套接字”并不旧。
    • 复现:Linux 容器里一个最小程序(两个线程不停 spawn /bin/true,主线程 bind→drop→connect)20000 次中 673 次连上;同样循环改用 UnixDatagram 为 0 次。
  2. Windows 上 CI 用 python -m venv,venv 的 python.exe 是启动器,真正的解释器是它的子进程,放在一个 JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE 的作业里:杀掉启动器,子进程随之被杀;先杀子进程,启动器随后自己退出。所以 Python 运行时在 Windows 上是两个进程。场景逐个调用 Scenario::kill,每次重新列举主机的进程并断言目标在其中;第二个进程可能已随第一个结束,断言就失败。是否失败取决于这段时序,所以是偶发的。a_killed_runtime_restarted_on_failure_brings_its_rows_back 和 soak 的同一写法有相同的竞争。

改法

  1. 测试用 UnixDatagram::bind 造旧套接字(stale_socket 辅助函数,注释写明原因):同样是没人接受连接的套接字文件,但永远不会接受流连接,子进程短暂持有副本也不影响。另一个测试里指向旧套接字的符号链接也用它。
  2. Scenario::kill(host, pid) 改为 Scenario::kill(host, &[pid]):一次列举,核对所有 pid 都属于主机,再逐个杀;轮到时已经结束的进程不算错(Unix:ESRCH;Windows:OpenProcess 报进程不存在,或 TerminateProcess 失败但进程已有退出码)。其他错误仍然 panic。runtime_crash.rs 两个场景和 soak.rs 改用它。场景意图不变:仍是 kill -9 运行时的全部进程、在途调用失败、其余运行时不受影响、主机停后以第一台主机拿到的端口重启。

验证

  • Linux 容器(rust:1.98.1-bookworm)循环 cargo test -p rutis-dev --test channel 300 次:改前 14 次失败,改后 0 次。
  • macOS:cargo test -p rutis-dev 循环 100 次通过;cargo test -p rutis-e2e --no-fail-fast 除 cordis 外全部通过(cordis 本机失败是本机 Node 22.13 没有 zlib.createZstdCompress,与本改动无关);runtime_crash 多次通过。
  • cargo check -p rutis-e2e --tests --target x86_64-pc-windows-msvc 通过。
  • 完整 CI(含 runtimes-windows)在本分支手动触发两次,见下方评论。

rutis-dev channel.rs::a_live_socket_is_not_replaced_but_a_stale_one_is:
测试用 std UnixListener 绑定后立即丢弃来造“崩溃留下的旧套接字”。同一测试
进程里 the_cli_keeps_the_row_id 并行地启动 rutis-dev 子进程;子进程在 fork
到 exec 之间持有父进程全部描述符的副本(CLOEXEC 只在 exec 时关闭),被丢弃
的监听套接字在这段时间里仍在监听,connect 成功,DevChannel 判它“仍有人
监听”而报 AddrInUse。产品逻辑没错。改为用 UnixDatagram 绑定造旧套接字:同样
是没人接受连接的套接字文件,但永远不会接受流连接。Linux 容器里循环 300 次:
旧测试失败 14 次,新测试 0 次。

e2e runtime_crash.rs::a_killed_runtime_fails_its_calls_and_a_restart_recovers
(Windows):失败与端口无关,是 "process N is not one of M's"。Windows 上 venv
的 python.exe 是启动器,真正的解释器是它的子进程,放在一个 KILL_ON_JOB_CLOSE
的作业里;杀掉启动器,子进程随之结束(反之亦然,启动器在子进程退出后自己
退出)。场景逐个杀 Python 进程,每次重新列一遍主机的进程,第二个可能已经
随第一个结束而不在列表里。Scenario::kill 改为接收一组 pid:一次列举核对全部
属于主机,然后逐个杀;轮到时已经结束的进程(ESRCH;Windows 上进程已不存在或
已有退出码)不算错。runtime_crash 的两个场景与 soak 都改用它,场景意图不变。
@eric8810 eric8810 added this to the 0.14 milestone Oct 19, 2026
@eric8810
eric8810 merged commit 4d989c4 into main Oct 20, 2026
121 of 125 checks passed
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