feat(cargo-cache): hardlink-clone a per-ref Cargo cache from a published snapshot
Replaces the phase-0 resolution probe with the real actions, merging the two independent per-branch Cargo cache implementations on this forge into the design neither of them had. ## The merge - zemyna seeds a PR branch by `cp -al` hardlink clone (near-free: cost scales with inode count, not bytes) from the base branch's LIVE target dir — a torn read waiting for a second job slot (its own #911). - emowheel seeds from a PUBLISHED IMMUTABLE SNAPSHOT (no race by construction) but with `cp -a`, duplicating ~35 GB per branch. This ships hardlink-clone FROM a published snapshot: zemyna's cost profile, emowheel's soundness, and #911 closed structurally rather than by the runner happening to have one execution slot. ## The bug both implementations have A build inside a `cp -al` clone DOES mutate the directory it was cloned from. Cargo replaces real artifacts, but writes its metadata — and build scripts write their OUT_DIR — with a plain truncating write, straight through the shared inode. Measured set: `.fingerprint/<unit>/dep-<target>` (under CARGO_UNSTABLE_CHECKSUM_FRESHNESS), `build/<pkg>/{output,root-output,out/**}`, `deps/*.d` and `<profile>/*.d`. The checksum-freshness case is a wrong answer, not a slow build: a PR clone rewrites the base's dep-info to describe the PR's sources while the base's cache still holds the artifact built from the base's; once the PR merges, the base's next run finds the checksums match, reports `Fresh`, and links a binary built from the pre-merge code. Reproduced end to end. Fix: hardlink the artifacts (the GB), real-copy the metadata (the MB) — about 3.7% of a 6.9 GB Bevy target dir, against 100% for a full copy. ## Contents - `cargo-cache/action.yml` — consume: resolve keys, seed from the base's snapshot via staging + one atomic rename, strip Cargo lock files, unshare the mutable paths, restore mtimes from git history, lock, prune. - `cargo-cache-publish/action.yml` — publish: record the build watermark, atomically republish the snapshot on a protected branch, release the lock (`mode: release-lock` for the `if: always()` step). - `scripts/` — all logic, so it is testable standalone; the YAML is wiring. - `scripts/*selftest.sh` + `selftest.sh` — five suites, 63 assertions, every fix paired with a control that reproduces the bug. All green locally. Eviction merges emowheel's liveness pass (dead branches pruned unconditionally, not gated on disk pressure) with LRU-under-pressure, but inverts the order within the pressure pass: `target-*` before `snapshot-*`, because a snapshot is hardlinked to everything cloned from it, so evicting one frees almost no real bytes while costing every future PR its warm start. restore-mtimes.sh is ported from emowheel (the watermark variant, which closes the merge hazard zemyna's copy still has) with its provenance de-projectised. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sqh2vscfzisk83VuPVQX9L
This commit is contained in:
Executable
+131
@@ -0,0 +1,131 @@
|
||||
#!/usr/bin/env bash
|
||||
# Regression test for seed-target-dir.sh: which source a run seeds from, and
|
||||
# what happens when two jobs sharing one cache key seed at the same time.
|
||||
#
|
||||
# Runs the ACTUAL script against a real scratch cache directory with fake
|
||||
# target trees standing in for cargo output — no compiler needed, so this is
|
||||
# the fast half of the suite. hardlink-clone-selftest.sh covers the parts that
|
||||
# need a real build.
|
||||
#
|
||||
# What each scenario demonstrates:
|
||||
#
|
||||
# 1. BASE SNAPSHOT PREFERRED — a PR whose base has published a snapshot
|
||||
# seeds from it, and the seeded directory really is a hardlink clone
|
||||
# (shared inodes), not a copy.
|
||||
# 2. OWN DIR WINS — a second run of the same ref reuses what is already
|
||||
# there and does not re-seed over its own work.
|
||||
# 3. OWN SNAPSHOT AS SELF-RESTORE — a publisher whose live target dir was
|
||||
# evicted restores from the snapshot it last published, instead of
|
||||
# rebuilding cold.
|
||||
# 4. FALLBACK DIR — with no snapshot at all, an explicitly configured
|
||||
# fallback (a pre-existing flat cache, during a migration) is used.
|
||||
# 5. COLD — with nothing available, the directory is created empty rather
|
||||
# than the script failing.
|
||||
# 6. LOCK FILES STRIPPED — Cargo's in-place-flock'd lock files never
|
||||
# survive a clone, because a shared lock inode would make two branches
|
||||
# contend on one mutex.
|
||||
# 7. CONCURRENT SEED IS ATOMIC — two seeds racing on one cache key: the
|
||||
# loser discards its staging copy and uses the winner's directory, and
|
||||
# at no point is a partially-populated directory visible under the final
|
||||
# name. This is the property that replaces "the runner only has one job
|
||||
# slot" with an actual guarantee.
|
||||
set -euo pipefail
|
||||
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
. "$script_dir/cache-lib.sh"
|
||||
|
||||
scratch=$(mktemp -d)
|
||||
trap 'rm -rf "$scratch"' EXIT
|
||||
root="$scratch/cache"
|
||||
mkdir -p "$root"
|
||||
pass_count=0
|
||||
|
||||
fail() { echo "ASSERTION FAILED: $*" >&2; exit 1; }
|
||||
ok() { pass_count=$((pass_count + 1)); echo "PASS: $*"; }
|
||||
assert_file() { [ -e "$1" ] || fail "expected $1 to exist ($2)"; ok "$2"; }
|
||||
assert_absent() { [ -e "$1" ] && fail "expected $1 to be gone ($2)"; ok "$2"; }
|
||||
assert_content() { [ "$(cat "$1")" = "$2" ] || fail "expected '$2' in $1, got '$(cat "$1")' ($3)"; ok "$3"; }
|
||||
|
||||
# A plausible target tree: a big shared artifact, a mutable fingerprint, a
|
||||
# build-script output, and a lock file.
|
||||
make_tree() {
|
||||
local d="$1" marker="$2"
|
||||
mkdir -p "$d/debug/deps" "$d/debug/.fingerprint/x" "$d/debug/build/x/out"
|
||||
echo "$marker" > "$d/debug/deps/libx.rlib"
|
||||
echo "$marker" > "$d/debug/.fingerprint/x/dep-lib-x"
|
||||
echo "$marker" > "$d/debug/build/x/out/gen.txt"
|
||||
: > "$d/debug/.cargo-lock"
|
||||
}
|
||||
|
||||
seed() { bash "$script_dir/seed-target-dir.sh" "$@" > "$scratch/log" 2>&1 || { cat "$scratch/log"; fail "seed-target-dir.sh exited non-zero"; }; }
|
||||
|
||||
BASE_KEY=$(cache_key dev)
|
||||
OWN_KEY=$(cache_key feat/thing)
|
||||
|
||||
echo "=== 1: base snapshot preferred, and cloned by hardlink ==="
|
||||
make_tree "$root/snapshot-$BASE_KEY" base-content
|
||||
seed "$OWN_KEY" "$BASE_KEY" "$root" job1
|
||||
own="$root/target-$OWN_KEY"
|
||||
assert_content "$own/debug/deps/libx.rlib" base-content "seeded from the base snapshot"
|
||||
[ "$(stat -c '%i' "$own/debug/deps/libx.rlib")" = "$(stat -c '%i' "$root/snapshot-$BASE_KEY/debug/deps/libx.rlib")" ] \
|
||||
|| fail "artifact was copied, not hardlinked"
|
||||
ok "artifact shares an inode with the snapshot (hardlink clone, not a copy)"
|
||||
[ "$(stat -c '%i' "$own/debug/.fingerprint/x/dep-lib-x")" != "$(stat -c '%i' "$root/snapshot-$BASE_KEY/debug/.fingerprint/x/dep-lib-x")" ] \
|
||||
|| fail "fingerprint still shares an inode with the snapshot"
|
||||
ok "fingerprint is privately owned (unshare_mutable_paths ran)"
|
||||
|
||||
echo
|
||||
echo "=== 6: Cargo lock files never survive a clone ==="
|
||||
assert_absent "$own/debug/.cargo-lock" "cloned .cargo-lock removed"
|
||||
|
||||
echo
|
||||
echo "=== 2: an existing own dir is reused, never re-seeded over ==="
|
||||
echo own-work > "$own/debug/deps/libx.rlib"
|
||||
seed "$OWN_KEY" "$BASE_KEY" "$root" job1
|
||||
assert_content "$own/debug/deps/libx.rlib" own-work "own directory reused as-is"
|
||||
grep -q 'reusing this ref' "$scratch/log" || fail "expected the reuse path in the log"
|
||||
ok "reuse is reported in the log"
|
||||
|
||||
echo
|
||||
echo "=== 3: a publisher restores from its own snapshot after eviction ==="
|
||||
make_tree "$root/snapshot-$BASE_KEY" published-dev
|
||||
seed "$BASE_KEY" "" "$root" job1
|
||||
assert_content "$root/target-$BASE_KEY/debug/deps/libx.rlib" published-dev "publisher self-restored from its own snapshot"
|
||||
|
||||
echo
|
||||
echo "=== 4: explicit fallback dir when no snapshot exists ==="
|
||||
OTHER=$(cache_key feat/other)
|
||||
make_tree "$scratch/legacy-flat" legacy
|
||||
seed "$OTHER" "$(cache_key nosuch)" "$root" job1 "$scratch/legacy-flat"
|
||||
assert_content "$root/target-$OTHER/debug/deps/libx.rlib" legacy "seeded from the fallback dir"
|
||||
|
||||
echo
|
||||
echo "=== 5: cold start when nothing is available ==="
|
||||
COLD=$(cache_key feat/cold)
|
||||
seed "$COLD" "$(cache_key nosuch)" "$root" job1
|
||||
[ -d "$root/target-$COLD" ] || fail "cold start did not create the directory"
|
||||
[ -z "$(ls -A "$root/target-$COLD")" ] || fail "cold start directory is not empty"
|
||||
ok "cold start creates an empty directory rather than failing"
|
||||
|
||||
echo
|
||||
echo "=== 7: two jobs racing on one cache key ==="
|
||||
RACE=$(cache_key feat/race)
|
||||
make_tree "$root/snapshot-$BASE_KEY" race-source
|
||||
# Both jobs seed concurrently from the same snapshot into the same key. Each
|
||||
# stages under its own tag, so the only interaction is the final rename.
|
||||
( bash "$script_dir/seed-target-dir.sh" "$RACE" "$BASE_KEY" "$root" jobA > "$scratch/logA" 2>&1 ) &
|
||||
( bash "$script_dir/seed-target-dir.sh" "$RACE" "$BASE_KEY" "$root" jobB > "$scratch/logB" 2>&1 ) &
|
||||
wait
|
||||
race_dir="$root/target-$RACE"
|
||||
assert_content "$race_dir/debug/deps/libx.rlib" race-source "the surviving directory is complete"
|
||||
[ -z "$(find "$root" -maxdepth 1 -name '.stage-*' -print -quit)" ] || fail "a staging directory was left behind"
|
||||
ok "no staging directory survived the race"
|
||||
# Exactly one job may claim it seeded; the other must report either the
|
||||
# concurrent-peer path or a plain reuse (if it started after the winner
|
||||
# finished). Neither may report a cold start.
|
||||
if grep -q 'starts cold' "$scratch/logA" "$scratch/logB"; then
|
||||
cat "$scratch/logA" "$scratch/logB"; fail "a racing job reported a cold start"
|
||||
fi
|
||||
ok "neither racing job fell through to a cold start"
|
||||
|
||||
echo
|
||||
echo "seed-target-dir-selftest: ${pass_count} assertions passed"
|
||||
Reference in New Issue
Block a user