fix(cache): 发布侧 key 只取 inputs.os 且 tag run 只读不写(M270 r1 复审 P2)
r1 复审(p2-1items)指出:tag 触发的 run 只能读 main 或同名 tag 的缓存,而 main 上
没有任何 job 跑 ubuntu-22.04 / windows-11-arm 这些标签 ⇒ 发布侧四个槽位每次发布都是冷编,
写出的 target 缓存永远拿不回来,还会占 10 GB 配额把 main 的热缓存 LRU 挤掉。r0 的
「key = inputs.os + inputs.slot」既没解决死缓存,还把 Windows 借 main 热缓存的路掐了。
- 发布侧 key 改成 `${{ inputs.os }}`:linux-x64(22.04) 与 main 的 ubuntu-latest 仍然不同
key(M269 那条必须断),而 windows-x64(windows-latest) 与 main 的 ci/build 重新同 key
—— 同镜像,借它的热缓存安全(M270 前 Windows 一直走这条且产物是绿的)。
- 发布侧加 `save-if: ${{ github.ref_type != 'tag' }}`:tag run 一律不写(写出来是死缓存),
只有 main 上的 dispatch 预热会写 —— 那次 run 的作用域是 main,之后的 tag run 才读得到。
刻意不用 `cache-targets: false`:那会把 Windows 借热缓存的路一起掐掉。
- ci.yml 侧只修 Nit:`Linux-X64` → `Linux-x64`(node 的 os.arch() 是小写)。
- docs 照实重写:tag 缓存作用域、发布侧每次冷编的必然性、预热怎么做、只读不写防什么、
「同一镜像标签但底层悄悄升级」这档剩余风险与手动 bump 办法;并补回 M164 MSYS 报错的
原文(可搜索的指路)。新的 key 演算:改前主 key 相同 2 对(跨镜像 1 对)/ 跨镜像前缀
命中 2 组;r1 = 1 对(全是同镜像 windows)/ 0 组。
This commit is contained in:
1 parent
93dc27bed8
commit
a6060e7649
3 files changed
+117
-65
No files matched your search
@@ -63,7 +63,8 @@ python packaging/make_artifacts.py --slot windows-arm64 \
|
||||
`CARGO_TARGET_AARCH64_PC_WINDOWS_MSVC_LINKER`(Hostarm64 原生优先、Hostx64 次之;`-find` 的斜杠/反斜杠两种形态都试)。
|
||||
顺序不能反:只覆盖链接器而不给 `LIB`,撞上的就是 `LNK1181: cannot open input file 'kernel32.lib'`。
|
||||
**`cmd //c` 不能直接收「带引号的 vcvarsall 路径」**:MSYS 会把 `"` 改写成 `\"`,cmd 的 `/c` 剥离规则接不住,
|
||||
结果 `LIB`/`INCLUDE` 全空(M164 复审实测;`cmd //c` 只调**不含引号**的路径是好的)。故本步往 `$RUNNER_TEMP`
|
||||
结果 `LIB`/`INCLUDE` 全空(M164 复审实测;`cmd //c` 只调**不含引号**的路径是好的)。当时的报错原文是
|
||||
`'\"C:\...\vcvarsall.bat\"' 不是内部或外部命令`(将来搜到这句、想知道来龙去脉,就看这一段)。故本步往 `$RUNNER_TEMP`
|
||||
写一个 CRLF 的 wrapper `.bat`(`call "<vcvarsall>" arm64` + `set`),`cmd //c` 只收相对文件名——引号留在
|
||||
`.bat` 内部由 cmd 自己解析,路径带空格也不受影响。
|
||||
- **编译前体检已降级为「只取证」**(M164):M141/M142 的版本用 `find` 猜 `um/arm64/kernel32.lib` 在不在、猜缺就
|
||||
@@ -151,7 +152,7 @@ Windows 为什么用 MSVC 静态 CRT 而不是本机这套 GNU 工具链:两
|
||||
key = prefix-key「v0-rust」
|
||||
+ 「-<key 输入>」 ← 本仓 M270 才第一次用(下文「修法」)
|
||||
+ 「-<GITHUB_JOB>」 ← 是 job **id**,不是 job 的 `name:`
|
||||
+ 「-<os.type()>-<os.arch()>」 ← Linux-X64 / Windows_NT-X64 / Darwin-ARM64,**没有镜像标签**
|
||||
+ 「-<os.type()>-<os.arch()>」 ← Linux-x64 / Windows_NT-x64 / Darwin-arm64,**没有镜像标签**
|
||||
+ 「-<env hash 前 8 位>」 ← rustc -vV(release + host + commit-hash)+ CARGO*/CC/CFLAGS/CXX/CMAKE/RUST* 的 sha1
|
||||
restoreKey = 到这里为止的一整串 ← 兜底恢复 key(**不含**锁哈希)
|
||||
key += 「-<manifest/lock hash 前 8 位>」 ← 各 Cargo.toml(版本号被抹平)+ Cargo.lock + .cargo/config.toml
|
||||
@@ -167,57 +168,108 @@ cacheKey = key
|
||||
**为什么 CI 与发布两侧会撞**:`ci.yml` 的 job id 是 `build`(跑 `ubuntu-latest` = 24.04),`release-build.yml` 的 job id
|
||||
也是 `build`(跑 `ubuntu-22.04`);`rust-toolchain.toml` 是 `channel = "stable"`(同一天两边解析到同一版 rustc),
|
||||
`Cargo.lock` 也是同一份 ⇒ env hash 与锁哈希全相同 ⇒ 两个 key **逐字节相同**。这串表达式里**没有任何一项**能反映出镜像。
|
||||
`RUSTFLAGS` 也救不了:它只写在 `cargo build` 那一步的 `env:` 里,cache 步根本看不到它。
|
||||
|
||||
**为什么 cargo 自己不发现**:freshness 判据是 fingerprint,记的是编译器版本、features、profile、依赖 metadata ——
|
||||
而 `-C metadata` 里**没有 glibc 版本**。于是恢复来的 `.rlib` 被判 fresh、不重编,问题一路拖到最后的链接。
|
||||
|
||||
**修法**:两个文件里 7 处 `Swatinem/rust-cache@v2` 全部加显式 `key:` —— 发布侧 `${{ inputs.os }}-${{ inputs.slot }}`
|
||||
(如 `ubuntu-22.04-linux-x64`),CI 侧用 `${{ matrix.os }}`(矩阵维度)或该 job 的字面量标签。`key` 输入落在**前缀段**
|
||||
(`v0-rust-<key>-<job>-…`),所以主 key 与兜底 `restoreKey` 同时带上镜像维度,**两条恢复路径一起断**。
|
||||
**修法**(两个文件里 7 处 `Swatinem/rust-cache@v2`;全仓共 8 处,见本节末尾):
|
||||
|
||||
**演算(这次的核心判据)**:把上面那段拼接写成脚本,对 12 个站点 × 2 个 `Cargo.lock` 状态逐对做前缀判定,改前 / 改后:
|
||||
- **发布侧**:`key: ${{ inputs.os }}`(只用镜像标签,**不带 slot**)+ `save-if: ${{ github.ref_type != 'tag' }}`
|
||||
(tag run 只读不写)。两条各有单独小节。
|
||||
- **CI 侧**:三个矩阵 job 用 `key: ${{ matrix.os }}`(自动跟 `runs-on`);三个单 OS job 用它自己的镜像标签字面量。
|
||||
后者的 `key:` 与 `runs-on:` 是**两份手写文本**:改 `runs-on` 时必须一起改 `key:`,否则镜像维度会静默失准(不报错,
|
||||
只是继续跨镜像共用)。
|
||||
- `key` 输入落在**前缀段**(`v0-rust-<key>-<job>-…`,在 `restoreKey` 定格之前),所以主 key 与兜底 `restoreKey`
|
||||
同时带上镜像维度,**两条恢复路径一起分开**。
|
||||
|
||||
| 对照 | 改前(7 处都没有 `key:`) | 改后 |
|
||||
#### 发布侧 key 为什么只取 `inputs.os`(不带 slot)
|
||||
|
||||
两个方向同时成立,去掉 slot 才对:
|
||||
|
||||
- **必须断的**:`linux-x64` @ `ubuntu-22.04` vs `ci/build` @ `ubuntu-latest` ⇒ 不同 key。这就是 M269 那条路径。
|
||||
- **故意保留的**:`windows-x64` @ `windows-latest` vs `ci/build` @ `windows-latest` ⇒ **同 key**。同镜像、同 rustc、
|
||||
同一份锁 ⇒ 借 main 上的热缓存是安全的:M270 之前 Windows 走的正是这条路,而 `windows-x64` 一直在 `PUBLISH_SLOTS`
|
||||
里、产物是绿的。slot 本来就是冗余维度(每个槽位的 `inputs.os` 互不相同)。
|
||||
- 代价与取舍:加了 slot 的那一版会把上面这一对也分开,两者的实质差异只是 `RUSTFLAGS=-C target-feature=+crt-static`。
|
||||
合并回同一把 key 后 cargo 按 fingerprint 区分(`RUSTFLAGS` 是否被计入本仓没有实测;但这是 M270 前线上一直成立的
|
||||
状态),最坏情况是多编一遍,不会错编。
|
||||
|
||||
#### 发布侧为什么「只读不写」(tag run 的缓存作用域)
|
||||
|
||||
- 官方文档(`docs.github.com` → dependency-caching → "Restrictions for accessing a cache"):一次 run 只能恢复
|
||||
**当前分支**或**默认分支(main)**创建的缓存;不同 tag 名之间互不可读(`release-a` 写的缓存,`release-b` 读不到)。
|
||||
- 推论一:发布侧的 key 是 `ubuntu-22.04` / `ubuntu-22.04-arm` / `windows-11-arm` / `macos-*`,而 **main 上一个 job 都
|
||||
不跑这些标签**(`ci.yml` 只有 `ubuntu-latest` / `windows-latest` / `macos-latest`)⇒ 这四个槽位**每次发布都是冷编**,
|
||||
连 registry / git 源码也拿不到(rust-cache 是「一把 key 一个 entry」,所有路径捆在一起,没有分路径缓存)。
|
||||
唯一还能借到热缓存的是 `windows-x64`(见上一小节)。
|
||||
- 推论二:发布侧写出去的缓存只能被**同名 tag 的下一次 run** 读到,实际就是一次性死缓存;它还占 per-repo 的 10 GB
|
||||
配额(超出按最后访问时间 LRU 淘汰、7 天未访问过期),会把 main 上真正在用的 CI 热缓存挤掉。**所以 tag run 不写。**
|
||||
- 为什么不是写死 `save-if: false`:把「写」留给唯一有效的场景 —— **在 main 上预热**。`gh workflow run release.yml`
|
||||
(在 main 的末梢上跑)那次 run 的缓存作用域是 **main**,之后从 main 末梢切出来的 tag run 才读得到它(锁哈希与
|
||||
rustc 版本一致才命中)。真要彻底禁写就把这一行改成 `false`,那时预热也要临时改文件才做得到。
|
||||
|
||||
#### 演算(核心判据;r1 换了 key,r0 那张表作废)
|
||||
|
||||
把上面的拼接写成脚本,对 15 个站点(ci 9:clippy/test/build 各 2 个 OS + 3 个单 OS job;发布 6)× 2 个
|
||||
`Cargo.lock` 状态两两判定:
|
||||
|
||||
| 关系 | 改前(7 处都没有 `key:`) | 改后(`key: inputs.os` + `save-if`) |
|
||||
| --- | --- | --- |
|
||||
| 路径①:主 key 逐字节相同 | **2 对**:`ci/build` == `rel/linux-x64` | 0 对 |
|
||||
| 路径②:`restoreKey` 前缀命中**别的站点** | **4 组**(`v0-rust-build-Linux-X64-<env>` 命中 `…-<另一个锁哈希>`) | 0 组 |
|
||||
| ① `rel/windows-x64` ↔ `ci/build`(**同镜像**) | 主 key 相同 | 主 key **仍相同**(故意保留 / 安全) |
|
||||
| ① `rel/linux-x64` ↔ `ci/build`(**跨镜像**) | 主 key 相同 ⇒ **M269 的 bug** | **不相同**(必须断) |
|
||||
| ② **跨镜像** `restoreKey` 前缀命中 | **2 组**(`ci/build@ubuntu-latest` ↔ `rel/linux-x64` 两个方向) | **0 组** |
|
||||
|
||||
拼出来长这样(env / 锁哈希用占位值,取不同的值不影响结论):
|
||||
拼出来(env / 锁哈希用占位值,取值不影响结论):
|
||||
|
||||
```
|
||||
改前 ci/build restoreKey = v0-rust-build-Linux-X64-3f9a1c07
|
||||
cacheKey = v0-rust-build-Linux-X64-3f9a1c07-b2d6e815
|
||||
改前 rel/linux-x64 restoreKey = v0-rust-build-Linux-X64-3f9a1c07 ← 与上行完全相同 ⇒ 路径①当场命中
|
||||
cacheKey = v0-rust-build-Linux-X64-3f9a1c07-b2d6e815
|
||||
改后 ci/build restoreKey = v0-rust-ubuntu-latest-build-Linux-X64-3f9a1c07
|
||||
cacheKey = v0-rust-ubuntu-latest-build-Linux-X64-3f9a1c07-b2d6e815
|
||||
改后 rel/linux-x64 restoreKey = v0-rust-ubuntu-22.04-linux-x64-build-Linux-X64-3f9a1c07
|
||||
cacheKey = v0-rust-ubuntu-22.04-linux-x64-build-Linux-X64-3f9a1c07-b2d6e815
|
||||
改后 ci/build@ubuntu-latest cacheKey = v0-rust-ubuntu-latest-build-Linux-x64-3f9a1c07-b2d6e815
|
||||
改后 rel/linux-x64@22.04 cacheKey = v0-rust-ubuntu-22.04-build-Linux-x64-3f9a1c07-b2d6e815
|
||||
↑ 前缀 v0-rust-ubuntu- 之后立刻分叉(`l` vs `2`);两边 restoreKey 都更长 ⇒ 前缀命中不可能
|
||||
改后 ci/build@windows-latest cacheKey = v0-rust-windows-latest-build-Windows_NT-x64-3f9a1c07-b2d6e815
|
||||
改后 rel/windows-x64 cacheKey = v0-rust-windows-latest-build-Windows_NT-x64-3f9a1c07-b2d6e815
|
||||
↑ 逐字节相同(同镜像,这是要保留的复用);锁哈希不同时退化成路径②,仍是同镜像,安全
|
||||
```
|
||||
|
||||
`v0-rust-ubuntu-latest-…` 与 `v0-rust-ubuntu-22.04-…` 在第一个 `-` 之后就分叉,任何一方的 `restoreKey` 都不是另一方
|
||||
`cacheKey` 的前缀(脚本对 12 个站点两两验证,主 key 相同 0 对、前缀命中 0 组)。前缀判定是**字面量**的 —— 只要
|
||||
`key:` 里不出现 `-<jobid>-<os类型>-<arch>-<env哈希>` 这种形状的文本(runner 标签不会是),两条 key 就不可能重新互为
|
||||
前缀;将来谁把自由文本塞进 `key:`,这一条要自己重新确认。
|
||||
脚本按「**跨镜像**」这一条断言(15 个站点两两 + 两个锁状态):改后任意两个 **runner 标签不同**的站点之间,主 key
|
||||
相同 0 对、`restoreKey` 前缀命中 0 组。仍存在的相同/前缀关系全部落在**同一个镜像标签内部** —— 也就是
|
||||
`rel/windows-x64` ↔ `ci/build` 那一对(同 `windows-latest`,正是要保留的复用)以及同站点换锁哈希那种(本来就该
|
||||
复用依赖)。判据的输出是一次性的脚本,跑完即删;上面这几行就是它的结论。
|
||||
|
||||
**`cache-targets` 要不要一起关掉:不关。** 关掉就只缓存 registry / git 源码,跨镜像确实彻底无风险,但每个 job 都要
|
||||
**从头编译整棵依赖树**(M269 干净容器实测同一条命令 18m43s 量级),日更的 CI 矩阵和每次发版都得付这笔钱。加了镜像维度
|
||||
之后,`target/` 的恢复永远来自**同一个镜像标签**下的缓存,M269 那条路径已经被切断;剩下被同一把 key 一起管住的还有
|
||||
`cache-bin`(`~/.cargo/bin` 里的 rustup shim 也是宿主编译的二进制),它与 `target/` 同域,没有引入额外风险。
|
||||
#### `cache-targets` 到底关不关:**不关**
|
||||
|
||||
**抓得住什么 / 抓不住什么**:
|
||||
- **CI 侧**:关掉就只缓存 registry / git 源码,每个 job 都要从头编整棵依赖树(M269 干净容器实测同一条命令 18m43s
|
||||
量级),日更矩阵每次提交都付这笔钱,收益为 0 —— 加镜像维度之后 `target/` 的恢复永远来自**同一镜像标签**,
|
||||
M269 那条路径已经断了。
|
||||
- **发布侧**:关掉确实能连 target 缓存一起省,但它**把 Windows 借 main 热缓存这条路一起掐掉**(target 不进缓存就
|
||||
没得借),而 `windows-x64` 是发布侧唯一还能借到的一档。发布侧的死缓存问题由 `save-if` 解决,比关 targets 精准:
|
||||
**该写的一次都不落,该借的一次都不少。**
|
||||
- 被同一把 key 一起管住的还有 `cache-bin`:`~/.cargo/bin` 里是 `dtolnay/rust-toolchain` 从 `static.rust-lang.org`
|
||||
装下来的**官方预编译** rustup 代理(不是在这台 runner 上编的),与 `target/` 同域、同一个 key,不引入额外风险。
|
||||
|
||||
- 抓得住**跨镜像复用**:不同 runner 标签之间(`ubuntu-22.04` ↔ `ubuntu-latest`)不再共享 `target/`,两条恢复路径都断,
|
||||
判据就是上面那张演算表。
|
||||
- **抓不住「同一个镜像标签、但底层镜像悄悄升级」**:`ubuntu-latest` 从 24.04 升到 26.04 时分母还是同一个标签,key 不变,
|
||||
旧缓存照样能恢复;22.04 打一次 glibc 补丁同理。这一档**没有自动化机制**,靠手动 bump:改 `prefix-key`
|
||||
(`v0-rust` → `v1-rust`)或改本处 `key:` 的字面量,一次性作废旧缓存。**别把这修法当成「以后不会再有这类问题」。**
|
||||
- 顺带:`rust-toolchain.toml` 是 `stable`,rustc 一升版 env hash 就变 ⇒ **编译器升级自动冷启动**,这一档本来就不在风险里。
|
||||
#### 抓得住什么 / 抓不住什么
|
||||
|
||||
**代价与一次性影响**:缓存条数从「每 job 一条」变成「每 job × 每镜像一条」,GitHub 的 per-repo 上限是 10 GB(LRU 淘汰 +
|
||||
7 天未访问过期),命中率靠 `restoreKey` 的锁哈希前缀兜着。**这次改动本身会让现有缓存一次性失效**(旧 key 再也匹配不上,
|
||||
交给 LRU 自然淘汰),合入后第一轮是冷缓存,别把它当成回归。
|
||||
- 抓得住**跨镜像复用**:不同 runner 标签之间(`ubuntu-22.04` ↔ `ubuntu-latest`)不再共享 `target/`,两条恢复路径
|
||||
都断,判据就是上面那张表。
|
||||
- **抓不住「同一个镜像标签、但底层镜像悄悄升级」**:`ubuntu-latest` 从 24.04 升到 26.04、或 22.04 打一次 glibc 补丁时,
|
||||
标签不变 ⇒ key 不变 ⇒ 旧缓存照样能恢复。这一档**没有自动化机制**,靠手动 bump:改 `prefix-key`(`v0-rust` →
|
||||
`v1-rust`)或改对应 `key:` 的字面量,一次性作废旧缓存。**别把这修法当成「以后不会再有这类问题」。**
|
||||
- 顺带:`rust-toolchain.toml` 是 `stable`,rustc 一升版 env hash 就变 ⇒ **编译器升级自动冷启动**,这一档本来就不在
|
||||
风险里。
|
||||
|
||||
**没验的部分**:真实 Actions 上的缓存命中 / 未命中本地验不了 —— 本机只能证明 key 字符串确实分开了(上面的演算)以及
|
||||
workflow 仍被真 YAML 解析器接受(`python packaging/check_workflows_yaml.py`,M190 的门禁)。「命中后日志里不再出现
|
||||
`__isoc23_*`」要等下一次 tag 的 `linux-x64` 构建日志说话。
|
||||
#### 代价与一次性影响
|
||||
|
||||
- CI 侧:缓存条数从「每 job 一条」变成「每 job × 每镜像一条」;三个单 OS job 各多一次冷编。
|
||||
- 发布侧:只有 `windows-x64` 还能借 main 的热缓存;`linux-x64` / `linux-arm64` / `windows-arm64` / `macos-*`
|
||||
**每次发布都冷编** —— 这是断开 M269 那条路径的必然代价(22.04 在 main 侧没有任何产出者)。想省就按上面
|
||||
「在 main 上预热」那条做。
|
||||
- 合入后所有旧 key 的缓存一次性失配,靠 LRU 自然淘汰;第一次 CI run 是冷缓存,别把它当成回归。
|
||||
- 全仓第 8 处 `rust-cache` 在 `.github/workflows/dev-guards.yml:128`(job id `clippy`,只跑 `ubuntu-latest`),
|
||||
**本次刻意不动**:它唯一的 runner 就是 `ubuntu-latest`,job id 也不与发布侧 `build` 重名,今天不可能跨镜像相撞。
|
||||
副作用是它的 key 与 `ci/clippy` 从此不再逐字节相同(改前是相同的)⇒ 合入后 dev-guards 那次 clippy 会冷编一次,
|
||||
一次性、无害。
|
||||
|
||||
**没验的部分**:真实 Actions 上的缓存命中 / 未命中本地验不了 —— 本机只能证明 key 字符串(上面的演算)、YAML 仍被真
|
||||
解析器接受(`python packaging/check_workflows_yaml.py`)、以及结构正确。「tag run 只能读 main / 同名 tag」这条来自
|
||||
官方文档而不是观测;下一次 tag 正好顺手验三件事:`Cache restored from key:` 是否变成 `No cache found.`(linux-x64
|
||||
应当是)、`Compiling aws-lc-sys` 是否重编、日志里还有没有 `__isoc23_*`。
|
||||
Reference in new issue
Block a user