fix(core): stderr drain 不再读共享字段,时序注入补上"调度得晚"这条轴 - #30
Merged
Merged
Conversation
固定秒数的 sleep 断言的是“这台机器够快”而不是被测行为:慢机器上它随机红,快机器上它 永远绿——两种结果都不回答“被测代码对不对”。 - 新增 tests/_waits.py:wait_for / wait_for_values / join_thread。为什么跨文件共用一个 实现:每处自己发明“等一会儿”时,余量都会被写成某个具体秒数(本仓曾同时存在 0.05 / 0.15 / 0.3),而谁也说不清哪个是真约束。 - test_health.py 用它替换本地实现;“停下来”改用 join 证明线程已死。 - test_retry.py 去掉“先睡 0.3 秒再 join”——sleep 的 join 前缀什么也没保证,join 才是 这段真正要的等待,而且它顺带证明了驱动线程会自己退出。 - pyproject 里显式声明 tests/ 下的内部助手为 first-party,否则 ruff 会把它们归到 第三方段。 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>
connect() 把 SSH 子进程的 stderr 交给守护线程去读(否则 stderr=PIPE 的缓冲区一旦填满 就会把 SSH 别住),而那个线程是在**被调度之后**才从 self.process 取进程引用的;同时 connect() 在会话结束时(finally)就把该字段清成了 None。于是线程只要晚一点跑到第一行 ——忙机器上的常态——它拿到的就是 None,直接返回,什么都没读:它存在的理由恰好被它自己 绕过了。改成创建线程时把进程交给它。 这个竞态不是读代码发现的:它是新的“线程启动延迟”注入逼出来的(本机空闲时永远绿,注入 延迟 0.15s 后每次都红)。所以同时加上一条不依赖注入的契约测试:self.process 已经是 None 时,交给该线程的进程照样得被读完。 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>
**第二条轴(PONTE_TEST_THREAD_START_DELAY)。** 原来的注入只把工作线程的 sleep/wait 拉长,而“线程已经 start() 却迟迟跑不到第一行”它够不到:还没开始执行的线程没有任何等待 可以被拉长。就这一条缝隙里藏着真 bug——它让 TunnelManager.connect 的 stderr drain 竞态 从“偶尔”变成“每次”(见上一个提交),也复现了当初把 main 弄红的那类断言。 **被否决的第三条轴,连同实测数字。** “工作线程缺 CPU”试过了:一个 100% 忙等的 Python 线程让 pytest 的收集阶段从 0.43s 变成 30s(两个线程 68s),而且它**抓不到**上面那条 竞态——竞争者的 sleep 会释放 GIL,被反调度的线程照样能在 50ms 的 nap 里跑起来。GIL 争抢加的是“每次重新获取最多 5ms 的延迟”,不是吞吐;那是前两条轴已经能做的事,且是免费 和确定性的。数字写在 tests/_injection.py 里,免得下一个人重新掏这笔钱。 **守卫的自检从“测量机制”升级成“证明它能把绿变红”。** 子进程里跑一条故意依赖时序的 探针:注入开着时必须失败、关掉时必须通过。只验前一半是不够的——一个把所有东西都弄红的 坏注入同样满足它。 **CI 的三个新维度**(都在同一批里,因为第一条腿已经证明了“环境差异”值得单独跑): - Python 3.13 / 3.14 进矩阵:版本差异不是装饰,v4-mapped 回环判定就在 3.11 与 3.12 之间 变过,而那直接决定这个工具要不要令牌。 - 新腿:C locale(非 UTF-8 stdio,需 PYTHONCOERCECLOCALE=0 才不是空跑)+ 半时区偏移 (UTC+5:30)+ 弃用告警当错误。 - 顺带在本地就把这条腿该发现的东西找到了:我的新自检在 ASCII stderr 上写不出中文报错, 于是它自己成了它想找的那类环境依赖——探针的文本已改成 ASCII。 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
新的 env-edges 腿在 CI 上抓到这条自检自己:
os.posix_spawn(...) → UnicodeEncodeError: 'ascii' codec can't encode
characters in position 83-104
那 22 个字符正是探针里的一句中文注释。C locale 下文件系统编码就是 ASCII,所以
- 中文写不进 stderr(本地就撞过:PYTHONIOENCODING=ascii 下这条测试直接红);
- 作为 `-c` 的参数更彻底:argv 根本进不了子进程。
改成全 ASCII 并把原因写进注释——不然下一个人会把它当成风格问题改回去。这不是“猜到的
风险”:两条都是真实运行出来的,且报的是同一个位置。
🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
#29 修掉了那条抖动的断言,但根因不止那一条:守卫只覆盖了"等待被推迟"这一种慢。
这条 PR 把缺的那条轴补上——它立刻抓到一个产品竞态,顺手把 CI 里一直没被覆盖的环境维度也补了。
一、守卫缺的那条轴(也是真 bug 的来源)
原来的注入把工作线程的
sleep/Event.wait拉长。但还没开始执行的线程没有任何等待可以被拉长:线程
start()之后第一行什么时候跑,是调度决定的。空闲机器上它永远绿,忙机器上随机红——正是当初把 main 弄红的那类断言。
所以加了第二条独立轴
PONTE_TEST_THREAD_START_DELAY:把每个线程体延后固定秒数再执行。它单独就把旧写法抓红了(对照见下),于是我用它去扫
test_core.py里那处"睡 0.05 秒等 drain线程"……
二、它抓到的产品竞态(这是这条 PR 里最重要的部分)
一注入就每次都红,而且不是测试的问题:
connect()用守护线程读 SSH 子进程的 stderr(否则stderr=PIPE缓冲区填满会把 SSH 别住),而那个线程是被调度之后才从
self.process取引用;connect()的finally早已把该字段清成
None。于是线程只要晚一点跑到第一行,它就拿到None、直接返回——它存在的理由恰好被它自己绕过。忙机器上的窗口就是从"极小"变成"常态"。
修法是创建线程时把进程交给它(
args=(proc,))。另外加了一条不依赖注入的契约测试:self.process已经是None时,被交给该线程的进程照样得被读完。两条测试分工明确——test_connect_logs_stderr(睡 0.05s)(第三行是关键:注入不是"让它红",而是"让真相提前暴露"。)
三、被否决的第三条轴,连同实测数字
"工作线程缺 CPU"我试了:一个 100% 忙等的 Python 线程抢 GIL。结果两头都不划算,数字写在
tests/_injection.py里,免得下一个人重新掏这笔钱:获取都加上最多
sys.getswitchinterval()的延迟,而一套测试里有几千次。sleep会释放 GIL,被反调度的线程照样能在 50ms 的 nap 里跑起来。
GIL 争抢加的是"延迟",而延迟恰恰是前两条轴已经能做的事,且是确定性的、免费的。真正"工作线程
一点 CPU 都没拿到"的失败形态,就是第二条轴。
四、顺带补上的两个"测试卫生"问题
tests/test_retry.py里"先睡 0.3 秒再join()":那个 0.3 既不是约束也不是事实,join才是这段真正要的等待(而且它顺带证明了驱动线程会自己退出)。
哪个是约束。统一成
tests/_waits.py的wait_for/wait_for_values/join_thread:等谓词,带截止时间,等不到就失败并带上现场。"停下来"改用
join证明线程已死(那才是测试名字里的stops cleanly)。
五、CI 的三个新维度
第一条腿已经证明"环境差异值得单独跑",于是顺手把审计出来、代价最低的三条补上:
::ffff:127.0.0.1的回环判定在 3.11 与 3.12 之间变过,而那直接决定这个工具要不要令牌LANG常常根本没设,而 ponte 的输出里有中文。注意PYTHONCOERCECLOCALE=0是必须的,否则 Python 会把 C locale 自动"挽救"成 C.UTF-8(PEP 538),这条腿就成了空跑这条腿在本地就先咬了我一口:ASCII stderr 下我的新自检连中文报错都写不出去,于是它自己成了
它想找的那类环境依赖——探针文本已改成 ASCII。这不是猜测,是
PYTHONIOENCODING=ascii下真的红了。验证
mypy与mypy --platform linux/ smoke 全绿。-W error::DeprecationWarning):418 通过。关掉必须通过。只验前一半是不够的——一个把所有东西都弄红的坏注入同样满足它。