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
82 lines
3.8 KiB
Bash
Executable File
82 lines
3.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Consume side: make this run's own target dir exist, warm, and private.
|
|
#
|
|
# Usage: seed-target-dir.sh <own-key> <base-key> <cache-root> <tag> [fallback-dir]
|
|
# own-key cache key for this run's own ref
|
|
# base-key cache key for the ref to layer over ("" for a run whose own
|
|
# ref IS a reference branch)
|
|
# cache-root mount point of the persistent volume
|
|
# tag a per-job-per-run unique string, used to name the staging
|
|
# directory so two concurrent jobs can never collide on it
|
|
# fallback-dir optional absolute path to seed from when no snapshot exists
|
|
# (a legacy flat cache dir during a migration, typically)
|
|
#
|
|
# The design in one paragraph: a PR branch's first run hardlink-clones the
|
|
# base branch's PUBLISHED SNAPSHOT. Hardlink, because the clone then costs
|
|
# time proportional to inode count rather than data volume — on ext4, with no
|
|
# reflink support, that is the only way to make "layer over the base" cheap.
|
|
# Snapshot rather than the base's live target dir, because a live directory is
|
|
# being written by its own job while a consumer reads it, and a torn read
|
|
# pairs one build's fingerprint with another build's artifact — which degrades
|
|
# to a WRONG reuse, not to a safe miss. Neither half is novel; the combination
|
|
# is what makes the scheme both cheap and sound, instead of one or the other.
|
|
#
|
|
# Everything about correctness under concurrency lives in
|
|
# hardlink_clone_into() and unshare_mutable_paths() in cache-lib.sh — read
|
|
# those before changing anything here.
|
|
set -euo pipefail
|
|
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/cache-lib.sh"
|
|
|
|
if [ $# -lt 4 ] || [ $# -gt 5 ]; then
|
|
echo "::error::seed-target-dir.sh: expected 4 or 5 arguments (own-key, base-key, cache-root, tag, [fallback-dir])" >&2
|
|
exit 1
|
|
fi
|
|
|
|
OWN_KEY="$1"; BASE_KEY="$2"; ROOT="$3"; TAG="$4"; FALLBACK="${5:-}"
|
|
OWN_DIR=$(target_dir_for "$ROOT" "$OWN_KEY")
|
|
|
|
mkdir -p "$ROOT"
|
|
|
|
if [ -d "$OWN_DIR" ]; then
|
|
echo "seed: reusing this ref's own cache at ${OWN_DIR} ($(usage_gb "$OWN_DIR") GB)"
|
|
echo "seeded-from=own" >> "${GITHUB_OUTPUT:-/dev/null}"
|
|
exit 0
|
|
fi
|
|
|
|
# Source preference, most specific first:
|
|
#
|
|
# 1. the base ref's snapshot — the ordinary PR case, and the whole point.
|
|
# 2. this ref's OWN snapshot — a reference branch whose live target dir was
|
|
# evicted under disk pressure can restore itself from the last snapshot
|
|
# it published, instead of paying a cold rebuild. Free, given the
|
|
# snapshot already exists.
|
|
# 3. an explicit fallback directory — migration off a pre-existing flat
|
|
# cache, so the first run under this scheme isn't a needless cold build.
|
|
CANDIDATES=()
|
|
[ -n "$BASE_KEY" ] && CANDIDATES+=("$(snapshot_dir_for "$ROOT" "$BASE_KEY"):base snapshot")
|
|
CANDIDATES+=("$(snapshot_dir_for "$ROOT" "$OWN_KEY"):own snapshot")
|
|
[ -n "$FALLBACK" ] && CANDIDATES+=("${FALLBACK}:fallback dir")
|
|
|
|
for entry in "${CANDIDATES[@]}"; do
|
|
src="${entry%%:*}"; label="${entry#*:}"
|
|
[ -d "$src" ] || continue
|
|
echo "seed: hardlink-cloning ${label} ${src} ($(usage_gb "$src") GB) -> ${OWN_DIR}"
|
|
start=$(date +%s)
|
|
if hardlink_clone_into "$src" "$OWN_DIR" "$TAG"; then
|
|
echo "seed: cloned in $(( $(date +%s) - start ))s"
|
|
echo "seeded-from=${label// /-}" >> "${GITHUB_OUTPUT:-/dev/null}"
|
|
else
|
|
# Another job sharing this cache key won the rename while we were
|
|
# cloning. Its directory is complete (the rename is the publish step), so
|
|
# there is nothing to do but use it — and nothing was ever observable in
|
|
# a half-seeded state.
|
|
echo "seed: another job seeded ${OWN_DIR} concurrently; discarded our staging copy and using theirs"
|
|
echo "seeded-from=concurrent-peer" >> "${GITHUB_OUTPUT:-/dev/null}"
|
|
fi
|
|
exit 0
|
|
done
|
|
|
|
echo "seed: no snapshot or fallback available — ${OWN_DIR} starts cold"
|
|
echo "seeded-from=cold" >> "${GITHUB_OUTPUT:-/dev/null}"
|
|
mkdir -p "$OWN_DIR"
|