Files
zeekay eefd9dc1dc fix(cache): a Go cache save keeps what its run used
Every save archived the whole build cache it restored plus what it
compiled, so nothing ever left it. hanzo-inc/cloud's four gate shares
grew to 7-8.5 GiB of zstd, 27 GiB unpacked; the runner pods restoring
them were evicted at their 80Gi ephemeral-storage limit mid-gate, and
every release since run 1009 was skipped.

Restore now stamps each restored entry two hours old, past the hour in
which Go leaves a used entry's mtime alone, and save drops every entry
still older than the restore. The cut archives live under the name
suffix -used, so none written before is restored again.
2026-10-16 23:18:26 -07:00

78 lines
4.0 KiB
Bash
Executable File

#!/usr/bin/env bash
# gocache — this org's Go module and build cache, carried between runners.
#
# gocache restore <module-dir> # prints hit=exact|partial|miss to $GITHUB_OUTPUT
# gocache save <module-dir> # writes on a trusted ref, on a miss, and nowhere else
#
# A runner is a pod that lives for one job, so a cache on its disk dies with it.
# Measured across 188 job logs: not one Go cache was restored, and 142 Go-gate
# minutes were 31 downloading modules, 69 compiling and 41 testing. A second
# pass over a warm cache compiled hanzoai/commerce's vet in 15s instead of 198s.
#
# The store, who may write, the parts and the index are bin/cache's; this script
# names what Go caches and what that is valid for.
#
# THE KEY: <owner/repo>/<go version>-<arch>/<sha256 of go.sum>. Modules are a
# function of go.sum, and a build cache is only valid for the toolchain and
# architecture that wrote it. A key that misses restores the newest key the repo
# saved (`latest`), so a go.sum bump still reuses every module that did not move.
#
# Go trusts GOMODCACHE and GOCACHE without re-hashing them, which is why only a
# trusted ref writes (bin/cache).
#
# A SAVE KEEPS WHAT ITS RUN USED. Each save archived the whole build cache it had
# restored plus what it compiled, so every save carried every entry since the
# first, and nothing ever left: hanzo-inc/cloud's shares grew to 7-8.5 GiB of
# zstd, 27 GiB unpacked, and the runner pods restoring them were evicted at the
# 80Gi ephemeral-storage limit mid-gate (hanzo-inc/cloud runs 1009-1013), so
# every release was skipped. So restore stamps each restored entry two hours
# old, past the hour within which Go leaves a used entry's mtime alone
# (cmd/go/internal/cache, mtimeInterval); an entry the run reads is stamped
# anew, and save drops every entry still older than the restore. Archives cut
# that way live under their own name (`-used`), so one written before is never
# restored again.
#
# ENV: everything bin/cache reads (HANZO_API_TOKEN, HANZO_API, CACHE_BUCKET, CACHE_PART).
set -uo pipefail
cmd="${1:-}"; dir="${2:-.}"
out="${GITHUB_OUTPUT:-/dev/null}"
skip() { echo "::notice::gocache: $*"; echo "hit=miss" >> "$out"; exit 0; }
case "$cmd" in restore|save) ;; *) echo "usage: gocache restore|save <module-dir>" >&2; exit 2 ;; esac
command -v go >/dev/null 2>&1 || skip "go is not on this runner"
[ -f "$dir/go.sum" ] || skip "$dir has no go.sum"
mod=$(go env GOMODCACHE); build=$(go env GOCACHE)
# Every later step reads the caches from exactly these directories, so a gate
# that names its own with `${GOCACHE:-…}` (hanzo-inc/cloud's do) builds into what
# was restored, and save archives what the gate built.
[ "$cmd" = restore ] && printf 'GOMODCACHE=%s\nGOCACHE=%s\n' "$mod" "$build" >> "${GITHUB_ENV:-/dev/null}"
if [ "$cmd" = save ]; then
[ -d "$mod" ] || { echo "gocache: no module cache to save"; exit 0; }
mkdir -p "$build" # a gate that compiled nothing still leaves modules worth keeping
fi
# A caller that splits its gate across parallel jobs (build.yml `gates`) keeps
# one cache per share: each compiles its own part of the tree, and one key per
# job means no share finds its key already taken by another's archive.
name="$(go env GOVERSION)-$(go env GOARCH)-used"
[ -n "${GOCACHE_SHARE:-}" ] && name="$name-$(printf '%s' "$GOCACHE_SHARE" | sha256sum | cut -c1-8)"
key="$(sha256sum "$dir/go.sum" | cut -c1-32)"
# When the restore finished, for save to tell a used entry from one that only came along.
since="${RUNNER_TEMP:-${TMPDIR:-/tmp}}/gocache-$name.since"
if [ "$cmd" = restore ]; then
"$(dirname "$0")/cache" restore "$name" "$key" mod="$mod" build="$build"
if [ -d "$build" ]; then
find "$build" -type f -exec touch -d "@$(( $(date +%s) - 7200 ))" {} + 2>/dev/null
fi
touch "$since"
exit 0
fi
if [ -f "$since" ]; then
find "$build" -type f ! -newer "$since" -delete 2>/dev/null
echo "gocache: the build cache holds what this run used, $(du -sh "$build" 2>/dev/null | cut -f1)"
fi
exec "$(dirname "$0")/cache" save "$name" "$key" mod="$mod" build="$build"