
rust-cache 为何在 host 模式 Forgejo runner 上无法恢复缓存
来源标题:Why rust-cache never restores on host-mode Forgejo runners
forgejo-runner 在 host 模式下会为每个 job 在 ~/.cache/act 下创建随机命名目录,导致工作区路径每次不同,rust-cache 因而始终无法恢复 target/,每次运行都保存新缓存条目。sccache 也受影响:crates.io 依赖能命中缓存,但工作区自身 crate 每次都要重新编译。问题根源是该随机目录名。
以下正文由 AI 翻译自 Hacker News 最新(原文版权归原站所有),可能存在偏差,重要内容请以原文为准。
在 host 模式下,forgejo-runner 会直接在 runner 所在机器的宿主机上运行 job 的各个 step,而不是在容器里运行。我们在 Stoke CI 就采用这种方式:每个 job 都会分配到一台专用虚拟机,job 结束后该虚拟机即被删除;而机器内部的 Docker 仍然负责处理 container:、services: 以及 docker build。
在 host 模式下使用原版 forgejo-runner 时,rust-cache 永远无法恢复 target/,即使配置了持久化的 actions/cache server 也一样:每次运行都会保存一条新条目,而下一次运行却什么都找不到。sccache 也存在一个相关的问题:来自 crates.io 的 crate 可以从缓存中命中,但工作区自身的 crate 每次都会被重新编译。
这两个问题的根源都在于一个名称随机的目录。
在 host 模式下,forgejo-runner 会在 host.workdir_parent(默认为 ~/.cache/act)下为每个 job 创建一个目录,job 的工作区就位于该目录之中。这个目录的名称是随机的。以下代码摘自 forgejo-runner v13.2.1 的 act/runner/run_context.go:
cacheDir := rc.ActionCacheDir() randName := common.MustRandName(8) miscpath := filepath.Join(cacheDir, randName) // ... path := filepath.Join(miscpath, "hostexecutor")
因此,一个 job 的工作区是 ~/.cache/act/3f9c1a2b7d04e6a1/hostexecutor,而下一个 job 的工作区则是 ~/.cache/act/d07e55c4a91b02f3/hostexecutor。在一台长期运行、可以同时执行多个 job 的 runner 上,这样做完全正确:各个 job 不会互相干扰对方的文件。但任何把工作区的绝对路径纳入 key 的缓存,现在每次运行都会失效。
actions/cache 条目的查找并不仅仅依据 key。每个条目还带有一个 version,由 @actions/cache 根据被缓存的路径列表计算得出:
export function getCacheVersion( paths: string[], compressionMethod?: CompressionMethod, enableCrossOsArchive = false ): string { const components = paths.slice() // ... compression method, platform, salt ... return crypto.createHash('sha256').update(components.join('|')).digest('hex')
forgejo-runner 的 cache server 只会返回 key 和 version 都匹配的条目。而 rust-cache 在缓存 target/ 之前,会先把工作区转换成绝对路径:
root = path.resolve(root); target = path.join(root, target);
由于路径中包含一个随机目录,每个 job 计算出的 version 都不一样。key 匹配上了,version 却对不上,于是恢复时什么都找不到。job 结束时执行的保存仍然有效,因此缓存里会不断堆积后续 job 永远找不到的条目。
一个使用相对路径的普通 actions/cache 步骤(例如 path: target)不受影响,因为它的 version 是根据你填写的路径原样计算的。
对 Rust 而言,sccache 的缓存 key 包含编译器的工作目录以及 CARGO_* 环境变量。其中之一是 CARGO_MANIFEST_DIR,即被编译 crate 的绝对路径。来自 crates.io 的 crate 在 ~/.cargo/registry 内编译,这个路径在每个 job 中都相同,因此能够命中。你自己的 crate 位于随机目录之下,因此无法命中。
随机名称是为了在共享 runner 上让各个 job 彼此隔离。而一台 Stoke CI 机器从不会接收第二个 job:它以 one-job 模式运行 forgejo-runner,随后即被删除。因此我们给 runner 增加了一个选项 host.workdir_name,用来替代随机名称:
cacheDir := rc.ActionCacheDir()
- randName := common.MustRandName(8)
- miscpath := filepath.Join(cacheDir, randName)
- // A fixed name keeps paths (and so cache keys) stable across jobs; it is only
- // safe when the runner runs one job at a time, e.g. with one-job.
- dirName := rc.Config.HostWorkdirName
- if dirName == "" {
- dirName = common.MustRandName(8)
- }
- miscpath := filepath.Join(cacheDir, dirName)
补丁的其余部分负责把这个设置接入 runner 的配置,并拒绝任何不是单一目录名的取值。我们机器的 runner 配置这样设置:
host: workdir_parent: /home/runner/work workdir_name: job
现在每个 job 的工作区都是 /home/runner/work/job/hostexecutor。rust-cache 在缓存已预热的情况下能够恢复 target/,sccache 也能命中工作区自身的 crate。没有打该补丁的 runner 会忽略这个选项,继续使用随机名称。
不要在可能同时运行多个 job 的 runner 上使用固定名称:那些 job 会共用同一个目录。
仅就 sccache 而言,较新的版本(0.14 及以后)提供了 SCCACHE_BASEDIRS,可以从缓存 key 中剥离你列出的基础目录。它正是为这类问题设计的。我们没有为 rust-cache 找到类似的机制,它总是把工作区解析为绝对路径。
我们并不想长期维护一个补丁,因此一旦 forgejo-runner 提供等效选项,我们就会弃用自己这个补丁。
正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。