fix(docker): deadsnakes 索引没抓下来时硬停(推翻 bash-builtins 假设) - #111
Conversation
症状(docker-build.yml):2026-09-21 run 35598902698 / 35599696780 红, 13:40 的 35607107888 绿 —— 同一个 Dockerfile,差别只在 PPA 索引抓没抓到。 根因不在 update-alternatives/bash-builtins.7.gz(先前我把它当根因,这里推翻: 那条 error 在**绿色** run 里同样出现,是 apt-get upgrade 期间的良性噪声)。 真正的链条是: Ign:7 https://ppa.launchpadcontent.net/deadsnakes/ppa/ubuntu jammy/main amd64 Packages W: Some index files failed to download. They have been ignored, or old ones used instead. -> apt-get update 仍然返回 0 -> 两条命令之后才炸:E: Unable to locate package python3.12 / python3.12-venv 即"警告后照旧继续"的静默降级,报错信息和真实原因(索引没取到)完全对不上。 修法:两段(builder / runtime)都把「PPA 索引里真查得到 python3.12」当作继续的前置条件, 带 Acquire::Retries 重试至多 5 轮,仍取不到就非零退出并写明"是 PPA 侧或网络故障、不是本仓 依赖声明问题",附手工核对用的 URL。不引入 path-exclude 之类未验证的规避。 临时探针 _docker-apt-probe.yml 负责验收(结论拿到后删除):它用 awk 从 Dockerfile 里抽出 守卫段与第一条 RUN **原文**执行(不手抄副本,避免漂移),并把 deadsnakes 源指向 127.0.0.1:9 + 删掉缓存索引,造出与 CI 现场等价的状态,要求同时看到 「旧序列静默降级复现」+「新守卫非零退出」+「健康态 stage1 原文跑通」。 本地已用 stub 验过循环语义:健康态 1 次判据即放行,破坏态跑满 5 轮后退出码 1 并打出原因。 Signed-off-by: ReSerendipity <zengyangc@outlook.com>
探针 run 35616302808(4m52s,8 步全绿)的实测记录,逐条对应它要证明的东西:
- 反空验证:未加 PPA 时 `apt-cache show python3.12` 退出码 100 —— 前置条件确实能为 false,不是恒真;
- 健康态:抽出的守卫原文退出码 0;
- 破坏态(deadsnakes 源改指 127.0.0.1:9 + 删缓存索引):
旧序列 `W: Some index files failed to download...` + `apt-get update 退出码 = 0`
→ 两条命令之后 `E: Unable to locate package python3.12`、install 退出码 100
—— 与红 run 35598902698 的症状逐字一致,等于把那个间歇故障在需要它的时候复现了出来;
新守卫同一状态下跑满 5 轮、退出码 1、打出"PPA 侧或网络故障"的可操作原因;
- 健康态全链路:Dockerfile 第一条 RUN 原文在真实 PPA 下装到 python3.12.13 + ensurepip 正常。
探针期间还白捡一条现场证据:它自己的"装上 PPA"那一步就出现了一次真的
`Ign:7 .../deadsnakes/... Packages`(同一作业里后面的 update 恢复成 Hit/Get)——
这个抖动是当前状态、不是历史事故。
最终验收改看完整镜像构建(Build & Scan Image)跑绿。
Signed-off-by: ReSerendipity <zengyangc@outlook.com>
探针已拿到结论并删除(commit
|
| 要证明 | 实测 |
|---|---|
| 前置条件不是恒真 | 未加 PPA 时 apt-cache show python3.12 退出码 100 |
| 健康态不误伤 | 抽出的守卫原文退出码 0(一次判据即放行,不重试) |
| 旧序列的静默降级确实存在 | 破坏态下 W: Some index files failed to download... + apt-get update 退出码 = 0 → 两条命令之后 E: Unable to locate package python3.12、install 退出码 100 |
| 新守卫会硬停 | 同一状态下跑满 5 轮、退出码 1、打出「PPA 侧或网络故障,不是本仓依赖声明问题」+ 可 curl 核对的 URL |
| 正常路径没被改坏 | Dockerfile 第一条 RUN 原文在真实 PPA 下装到 Python 3.12.13,ensurepip 正常 |
破坏态是把 deadsnakes 源指向 127.0.0.1:9 并删掉已缓存索引造出来的,症状与红 run 35598902698 逐字一致 —— 也就是不需要等下一次抖动就能验收。
探针作业自身还有一处意外收获:它自己的「装上 PPA」那一步就撞了一次真的
Ign:7 https://ppa.launchpadcontent.net/deadsnakes/ppa/ubuntu jammy/main amd64 Packages
(同一作业里后面的 update 恢复成 Hit/Get)。所以这个抖动是当前状态,不是历史事故。
本地侧另有一层:用 stub 替掉 apt 后单跑守卫循环(健康态 1 次判据放行 / 破坏态 5 轮后退出码 1),
并核对抽取逻辑在 CRLF 与 LF 两种行尾下产出的 guard.sh、run01.sh md5 完全一致
—— 抽取本身不随行尾漂移。verify_cloud_native.py 的 A1/A5/A7 在改动后仍全绿,mypy 棘轮 103 不变。
剩下的最终验收 = 完整镜像构建(Build & Scan Image)跑绿。
顺带一条与本 PR 无关的门禁噪声:
Secret Scan (gitleaks, full history)这次红是
curl: (22) The requested URL returned error: 504(下载 gitleaks 二进制失败,7 秒即挂),
不是扫到密钥。已重跑。这正是本 PR 修的那一类形状:上游取不到东西时,门禁以"看起来像成功"的方式挂掉或跳过,
区别只是 gitleaks 这条至少是硬失败。
docker-smoke 的"镜像内引擎导入探针"(#107 加的那步)第一次真正跑到就红: FAIL integrated_app.vendor.voxcpm ModuleNotFoundError No module named 'einops' (run 35618578940 / job 106395583267)。不是我这个分支改坏的 —— 同一镜像在 PPA 恢复正常之前 根本构建不到那一步。 真因是**依赖声明缺项**: - `app/integrated_app/vendor/voxcpm/model/voxcpm.py:29`、`voxcpm2.py:30`、 `modules/locenc/local_encoder.py:4` 在模块顶层 `from einops import rearrange`; - einops 在本仓只出现在 `training` extra 里; - 而 funasr / modelscope 只在 **extras** 里声明 einops(读发行元数据实测),我们装的是无 extras 的核心集 —— 所以按 requirements.txt 或 `[project].dependencies` 装出来的任何干净环境都起不来 tts-1(VoxCPM2,默认自动加载的那个引擎)。 - 本机 .venv 永远看不见此事:einops 是从训练链路传递进来的 (`pip show` Required-by: conformer, indextts, torch-einops-utils, vector-quantize-pytorch)。 同形状的第二条:`addict` 早就声明为生产依赖(pyproject 注释写明"缺失时降噪会降级关闭、音质下降"), 却在 `requirements-lock.txt` 与 `launcher/requirements-small.txt` 里**都不存在**,modelscope 对它的 依赖同样只在 extras 里 —— 便携包那条离线钉装链与镜像一样装不出来。这条一直藏在 "桌面安装包链路无任何 workflow 覆盖、本机也无从安装"的盲区里;便携侧我只到"清单缺项"的证据强度, 没实拆过包。 改动: - pyproject 核心依赖 += einops>=0.7.0(带实测出处与教训),requirements.txt 由 sync_requirements.py 同步(26 个依赖,`--check` 通过); - 两份钉版集各补 addict==2.4.0 与 einops==0.8.2(版本取本机 .venv 实测,与当初对齐锁集的做法 一致),`--check-small` 94 项对齐通过; - 新增 D5(tests/test_dependency_consistency.py):核心声明的每个包必须在两份钉版集里都有钉版, 唯一豁免是 torch 三件套(便携包里作为独立组件安装),allowlist 写死以便"新增豁免"必须过评审; 配一条变异自证(摘掉 einops 的钉版必须变红,torch 缺席不得被误判); - 探针的模块清单点到真正 import 的那三个模块,报错文案不再只指向 transformers 区间, 而是分成"没装(多半只在 extras 里声明)"与"版本区间不对"两类。 本地:D5 两条 + 既有 11 条 = 13 passed;docker-smoke.yml YAML 解析通过。 验收:等 `Boot hardened container & probe` 在真镜像上转绿 —— 那一步现在既是探针也是这条修复的证据。 Signed-off-by: ReSerendipity <zengyangc@outlook.com>
它是 pre-commit 的一个 hook:一次 `RemoteDisconnected` 就让整条 `git commit` 失败, 而顺序抓 ~97 个包的元数据让"撞上一次丢包"几乎成必然。今晚实测: `http.client.RemoteDisconnected` 在 `check()` 里**没被捕到** —— `urllib` 只在 `h.request()` 那一步把 OSError 包成 `URLError`, `h.getresponse()` 抛出的是裸的 OSError 子类,所以 hook 吐的是 traceback, 而不是脚本自己设计的"unchecked + 退出码 2"。 - `_fetch_requires_dist`:3 次重试 + 退避,异常面加 `OSError` / `http.client.HTTPException` / `ValueError`,单次超时 25s → 10s; - `prefetch()`:8 线程并发抓全量元数据后灌缓存; - 未能核验时的提示改成可操作方向("先确认 pypi.org 可达,这不是锁冲突,别去改锁")。 实测(本机,97 个包):改前 3 分钟起跳且中途 traceback;改后 **12.6s**, `冲突 0 条 / 未核验 0 条 / PASS` —— 同时也就是本 PR 新补的 `addict==2.4.0`、`einops==0.8.2` 两条钉版与锁集自洽的证据。 Signed-off-by: ReSerendipity <zengyangc@outlook.com>
这条修什么
docker-build.yml的镜像构建会间歇性红:根因(并且推翻我先前写进 docs/DOD.md 的判断)
先前我认定根因是
update-alternatives: error: alternative path /usr/share/man/man7/bash-builtins.7.gz doesn't exist,还为此开了探针 PR #110。这个判断是错的:探针作业 5 步全绿,而那条 error 在绿色 run 35607107888 的#11 97.72同样出现 —— 它是apt-get upgrade期间的良性噪声,和构建失败无关。真链条(红 run 的日志):
绿色 run 的同一行是
Get:7 ... Packages [44.3 kB]。也就是说:deadsnakes 的索引抓不抓得到决定构建成不成立,而 apt 对"某个索引没抓下来"只给 W: 并返回 0。改法
builder 与 runtime 两段都把「PPA 索引里真查得到
python3.12」当作继续的前置条件:Acquire::Retries=3重试至多 5 轮,仍取不到就非零退出,并把原因写成「PPA 侧或网络故障,不是本仓依赖声明问题」+ 可直接 curl 核对的 URL。没有引入path-exclude、|| true之类未经验证的规避。验收方式(不靠"等下一次抖动")
临时探针作业
_docker-apt-probe.yml用 awk 从 Dockerfile 里抽出原文执行(守卫段 + 第一条 RUN,避免手抄副本漂移),然后把 deadsnakes 源指向127.0.0.1:9、删掉已缓存索引,造出与 CI 现场等价的状态,要求同时成立:apt-cache show python3.12必须判 false(否则这条前置条件恒真、等于没守卫);探针自身还断言抽取结果(必须含
$i、Acquire::Retries、不得越界到下一条 RUN)—— 本地已跑过:guard.sh/run01.sh在 CRLF 与 LF 两种行尾下 md5 一致;用 stub 替掉 apt 后,健康态 1 次判据即放行、破坏态跑满 5 轮后退出码 1。结论拿到后删掉探针文件;最终验收 = 完整镜像构建跑绿。
与 #110 的关系
#110 是按错误假设(bash-builtins/manpages)开的一次性探针,它的价值是把那条假设证伪了(5 步全绿)。故关闭 #110,由本条接替。
Refs: #110;docs/DOD.md 里"确定性 apt 故障"的记载需按本条改写。