Skip to content

Commit fe0d968

Browse files
committed
Stamp delta bundles with base commit SHA and exclude stale artifact DBs
Embed the base commit SHA in delta bundles to prevent cross-base contamination. On restore, write the hit commit to .cache-base-commit. SaveDelta reads it and stamps the delta via S3 metadata and a synthetic __base_commit__ tar entry. Restore checks both before applying. Also exclude module-artifact.bin and resource-at-url.bin from deltas to prevent stale resolution metadata from causing missing-jar failures.
1 parent 8fac5a0 commit fe0d968

7 files changed

Lines changed: 331 additions & 22 deletions

File tree

gradlecache/extract_default.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,10 @@ func processEntry(
225225
return err
226226
}
227227

228+
if name == deltaBaseCommitEntry {
229+
return nil
230+
}
231+
228232
target := targetFn(name)
229233

230234
switch hdr.Typeflag {

gradlecache/ghacache.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,7 @@ func (g *ghaCacheStore) createAndFinalize(ctx context.Context, commit, cacheKey
329329
// put uploads a cache entry from a ReadSeeker of known size.
330330
// For small bundles (≤ 1 block), uses a single PUT. For larger bundles,
331331
// uses parallel Azure Block Blob upload (Put Block + Put Block List).
332-
func (g *ghaCacheStore) put(ctx context.Context, commit, cacheKey string, r io.ReadSeeker, size int64) error {
332+
func (g *ghaCacheStore) put(ctx context.Context, commit, cacheKey string, r io.ReadSeeker, size int64, _ map[string]string) error {
333333
return g.createAndFinalize(ctx, commit, cacheKey, size, func(signedURL string) error {
334334
if size <= ghaBlockSize {
335335
return g.azurePutSingle(ctx, signedURL, r, size)

gradlecache/gradlecache_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ func TestIsDeltaExcluded(t *testing.T) {
5959
excluded := []string{
6060
"fileHashes",
6161
"module-metadata.bin",
62+
"module-artifact.bin",
63+
"resource-at-url.bin",
6264
}
6365
for _, name := range excluded {
6466
if !IsDeltaExcluded(name) {
@@ -806,6 +808,67 @@ func TestDeltaTarZstdRoundTrip(t *testing.T) {
806808
}
807809
}
808810

811+
func TestStampedDeltaRoundTrip(t *testing.T) {
812+
baseCommit := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
813+
gradleHome := t.TempDir()
814+
cachesDir := filepath.Join(gradleHome, "caches", "modules-2")
815+
must(t, os.MkdirAll(cachesDir, 0o755))
816+
must(t, os.WriteFile(filepath.Join(cachesDir, "delta-file.bin"), []byte("delta"), 0o644))
817+
818+
// Create a stamped delta archive.
819+
var buf bytes.Buffer
820+
must(t, createStampedDeltaTarZstdMulti(&buf, baseCommit,
821+
DeltaSource{BaseDir: gradleHome, RelPaths: []string{"caches/modules-2/delta-file.bin"}}))
822+
823+
// ReadDeltaBaseCommit should return the embedded stamp.
824+
r := bytes.NewReader(buf.Bytes())
825+
got, err := ReadDeltaBaseCommit(r)
826+
must(t, err)
827+
if got != baseCommit {
828+
t.Fatalf("ReadDeltaBaseCommit = %q, want %q", got, baseCommit)
829+
}
830+
831+
// The file should still extract correctly (stamp entry is skipped).
832+
dstDir := t.TempDir()
833+
must(t, extractTarZstd(context.Background(), bytes.NewReader(buf.Bytes()), dstDir))
834+
835+
data, err := os.ReadFile(filepath.Join(dstDir, "caches", "modules-2", "delta-file.bin"))
836+
must(t, err)
837+
if string(data) != "delta" {
838+
t.Fatalf("extracted content = %q, want %q", string(data), "delta")
839+
}
840+
841+
// __base_commit__ should NOT exist as a file on disk.
842+
if _, err := os.Stat(filepath.Join(dstDir, deltaBaseCommitEntry)); err == nil {
843+
t.Fatal("__base_commit__ should not be extracted as a real file")
844+
}
845+
}
846+
847+
func TestReadDeltaBaseCommitMissing(t *testing.T) {
848+
// An unstamped delta should return empty string.
849+
var buf bytes.Buffer
850+
must(t, CreateDeltaTarZstdMulti(&buf,
851+
DeltaSource{BaseDir: t.TempDir(), RelPaths: nil}))
852+
853+
r := bytes.NewReader(buf.Bytes())
854+
got, err := ReadDeltaBaseCommit(r)
855+
must(t, err)
856+
if got != "" {
857+
t.Fatalf("ReadDeltaBaseCommit on unstamped delta = %q, want empty", got)
858+
}
859+
}
860+
861+
func TestBaseCommitFileRoundTrip(t *testing.T) {
862+
dir := t.TempDir()
863+
sha := "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
864+
must(t, writeBaseCommitFile(dir, sha))
865+
got, err := readBaseCommitFile(dir)
866+
must(t, err)
867+
if got != sha {
868+
t.Fatalf("readBaseCommitFile = %q, want %q", got, sha)
869+
}
870+
}
871+
809872
func TestSaveDeltaDefaultsProjectDirToWorkingDirectory(t *testing.T) {
810873
ctx := context.Background()
811874
gradleHome := t.TempDir()
@@ -817,6 +880,8 @@ func TestSaveDeltaDefaultsProjectDirToWorkingDirectory(t *testing.T) {
817880

818881
markerPath := filepath.Join(gradleHome, ".cache-restore-marker")
819882
must(t, touchMarkerFile(markerPath))
883+
// Write a base commit file so SaveDelta can stamp the delta.
884+
must(t, writeBaseCommitFile(gradleHome, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"))
820885

821886
// Sleep to ensure files created below have a strictly newer mtime than
822887
// the marker. On Linux ext4 the mtime granularity is 1 ms, but rapid

gradlecache/restore.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,15 @@ func Restore(ctx context.Context, cfg RestoreConfig) error {
386386
deltaCh <- deltaResult{}
387387
return
388388
}
389+
// Check S3 metadata for base-commit mismatch before downloading.
390+
if metaBase := deltaInfo.Metadata[deltaBaseCommitMetaKey]; metaBase != "" && metaBase != hitCommit {
391+
log.Info("skipping delta: built on different base",
392+
"delta_base", metaBase[:min(8, len(metaBase))],
393+
"current_base", hitCommit[:min(8, len(hitCommit))])
394+
deltaCh <- deltaResult{}
395+
return
396+
}
397+
389398
log.Info("downloading delta bundle", "branch", cfg.Branch)
390399
dlStart := time.Now()
391400
body, err := store.get(ctx, dc, cfg.CacheKey, deltaInfo)
@@ -412,6 +421,19 @@ func Restore(ctx context.Context, cfg RestoreConfig) error {
412421
deltaCh <- deltaResult{err: errors.Wrap(err, "rewind delta temp file")}
413422
return
414423
}
424+
425+
// Check tar stamp for base-commit mismatch (covers stores without metadata).
426+
tarBase, tarErr := ReadDeltaBaseCommit(tmp)
427+
if tarErr == nil && tarBase != "" && tarBase != hitCommit {
428+
log.Info("skipping delta: tar stamp shows different base",
429+
"delta_base", tarBase[:min(8, len(tarBase))],
430+
"current_base", hitCommit[:min(8, len(hitCommit))])
431+
tmp.Close() //nolint:errcheck,gosec
432+
os.Remove(tmp.Name()) //nolint:errcheck,gosec
433+
deltaCh <- deltaResult{}
434+
return
435+
}
436+
415437
deltaCh <- deltaResult{tmpFile: tmp, dlStart: dlStart, n: cb.n, eofAt: cb.eofAt}
416438
}()
417439
}
@@ -489,6 +511,10 @@ func Restore(ctx context.Context, cfg RestoreConfig) error {
489511
cfg.Metrics.Distribution("gradle_cache.restore_base.speed_mbps", mbps, "cache_key:"+cfg.CacheKey)
490512
}
491513

514+
if err := writeBaseCommitFile(cfg.GradleUserHome, hitCommit); err != nil {
515+
log.Warn("could not write base commit file", "err", err)
516+
}
517+
492518
if err := touchMarkerFile(filepath.Join(cfg.GradleUserHome, ".cache-restore-marker")); err != nil {
493519
log.Warn("could not write restore marker", "err", err)
494520
}

gradlecache/s3.go

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,9 @@ func newS3Client(region string) (*s3Client, error) {
6363
}
6464

6565
type s3ObjInfo struct {
66-
Size int64
67-
ETag string
66+
Size int64
67+
ETag string
68+
Metadata map[string]string
6869
}
6970

7071
func (c *s3Client) stat(ctx context.Context, bucket, key string) (s3ObjInfo, error) {
@@ -82,10 +83,20 @@ func (c *s3Client) stat(ctx context.Context, bucket, key string) (s3ObjInfo, err
8283
if resp.StatusCode != http.StatusOK {
8384
return s3ObjInfo{}, errors.Errorf("status %d", resp.StatusCode)
8485
}
85-
return s3ObjInfo{
86+
info := s3ObjInfo{
8687
Size: resp.ContentLength,
8788
ETag: resp.Header.Get("ETag"),
88-
}, nil
89+
}
90+
const metaPrefix = "X-Amz-Meta-"
91+
for k, vs := range resp.Header {
92+
if len(vs) > 0 && len(k) > len(metaPrefix) && strings.EqualFold(k[:len(metaPrefix)], metaPrefix) {
93+
if info.Metadata == nil {
94+
info.Metadata = make(map[string]string)
95+
}
96+
info.Metadata[strings.ToLower(k[len(metaPrefix):])] = vs[0]
97+
}
98+
}
99+
return info, nil
89100
}
90101

91102
func (c *s3Client) get(ctx context.Context, bucket, key string, info s3ObjInfo) (io.ReadCloser, error) {

0 commit comments

Comments
 (0)