Skip to content

fix(docker): deadsnakes 索引没抓下来时硬停(推翻 bash-builtins 假设) - #111

Merged
ReSerendipity merged 5 commits into
mainfrom
fix/docker-ppa-index-hardfail
Sep 22, 2026
Merged

ReSerendipity merged 5 commits into
mainfrom
fix/docker-ppa-index-hardfail

Conversation

@ReSerendipity

Copy link
Copy Markdown
Owner

这条修什么

docker-build.yml 的镜像构建会间歇性红:

run 时间 结果
35598902698 09-21 12:19 红
35599696780 09-21 12:28 红
35607107888 09-21 13:40 绿(Dockerfile 未变)

根因(并且推翻我先前写进 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 的日志):

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     ← 报错落在两条命令之后,且不像网络问题
E: Unable to locate package python3.12-venv

绿色 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 现场等价的状态,要求同时成立:

  1. 反空验证:未加 PPA 时 apt-cache show python3.12 必须判 false(否则这条前置条件恒真、等于没守卫);
  2. 旧序列复现出「update 返回 0 + W: Some index files failed to download → install 报 Unable to locate package」;
  3. 新守卫在该状态下非零退出并打出可操作原因;
  4. 健康态下 Dockerfile 第一条 RUN 原文整段跑通(证明修法没把正常路径弄坏)。

探针自身还断言抽取结果(必须含 $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 故障"的记载需按本条改写。

症状(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>
先前这里写的是"确定性基础设施故障、jammy 归档期的 man-db/manpages 组合"。两条都不成立:
探针 PR #110 五步全绿,而绿色 run 35607107888 里同样出现了那条 update-alternatives error。
本仓 FIX_LOG.md(2026-09-11 行)与 docs/agents/GOTCHAS.md #113 早就写着它是非致命噪声 ——
定位开工前没查自己的记录,等于用新猜测推翻了旧的正确结论。

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>
@ReSerendipity

Copy link
Copy Markdown
Owner Author

探针已拿到结论并删除(commit 89dd40c)

探针 run 35616302808(真 Linux + 同一基础镜像,4m52s,8 步全绿)逐条对上了它要证明的东西:

要证明 实测
前置条件不是恒真 未加 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>
@ReSerendipity
ReSerendipity merged commit f39bdb1 into main Sep 22, 2026
31 checks passed
@ReSerendipity
ReSerendipity deleted the fix/docker-ppa-index-hardfail branch September 23, 2026 07:15
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