Skip to content

Commit 18a0ceb

Browse files
committed
Handle truncated bundles gracefully during restore
1 parent 851fadd commit 18a0ceb

2 files changed

Lines changed: 45 additions & 1 deletion

File tree

gradlecache/gradlecache_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
archive_tar "archive/tar"
66
"bytes"
77
"context"
8+
stderrors "errors"
89
"fmt"
910
"io"
1011
"net/http"
@@ -487,6 +488,41 @@ func TestExtractZstdDrainsBufferedReaderToEOF(t *testing.T) {
487488
})
488489
}
489490

491+
// ─── Truncated archive test ──────────────────────────────────────────────────
492+
493+
// TestExtractBundleTruncatedArchive verifies that extractBundleZstd returns an
494+
// io.ErrUnexpectedEOF when the archive is truncated, so the caller can warn
495+
// instead of fatally erroring out.
496+
func TestExtractBundleTruncatedArchive(t *testing.T) {
497+
ctx := context.Background()
498+
srcDir := t.TempDir()
499+
500+
// Create two files so we can truncate mid-archive.
501+
must(t, os.MkdirAll(filepath.Join(srcDir, "caches"), 0o755))
502+
must(t, os.WriteFile(filepath.Join(srcDir, "caches", "first.jar"), bytes.Repeat([]byte("A"), 4096), 0o644))
503+
must(t, os.WriteFile(filepath.Join(srcDir, "caches", "second.jar"), bytes.Repeat([]byte("B"), 4096), 0o644))
504+
505+
var archive bytes.Buffer
506+
must(t, CreateDeltaTarZstd(ctx, &archive, srcDir, []string{"caches/first.jar", "caches/second.jar"}))
507+
508+
// Truncate the compressed archive at roughly 60% to simulate a partial upload.
509+
truncated := archive.Bytes()[:archive.Len()*60/100]
510+
511+
gradleHome := t.TempDir()
512+
projectDir := t.TempDir()
513+
514+
_, err := extractBundleZstd(ctx, bytes.NewReader(truncated), []extractRule{
515+
{prefix: "caches/", baseDir: gradleHome},
516+
}, projectDir, false)
517+
518+
if err == nil {
519+
t.Fatal("expected an error from truncated archive, got nil")
520+
}
521+
if !stderrors.Is(err, io.ErrUnexpectedEOF) {
522+
t.Fatalf("expected io.ErrUnexpectedEOF in error chain, got: %v", err)
523+
}
524+
}
525+
490526
// ─── Round-trip archive test ─────────────────────────────────────────────────
491527

492528
// TestTarZstdRoundTrip verifies that CreateTarZstd → extractTarZstd preserves

gradlecache/restore.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -436,7 +436,15 @@ func Restore(ctx context.Context, cfg RestoreConfig) error {
436436
netTiming := &timingReader{r: cb}
437437
ps, err := extractBundleZstd(ctx, netTiming, rules, cfg.ProjectDir, !gradleUserHomeEmpty)
438438
if err != nil {
439-
return errors.Wrap(err, "extract bundle")
439+
// A truncated tar archive (e.g. from a crash during upload) produces an
440+
// unexpected EOF during extraction. Treat this as a warning rather than
441+
// a fatal error: the files extracted before the truncation point are
442+
// still usable and better than no cache at all.
443+
if errors.Is(err, io.ErrUnexpectedEOF) {
444+
log.Warn("bundle appears truncated! using partially extracted cache", "err", err)
445+
} else {
446+
return errors.Wrap(err, "extract bundle")
447+
}
440448
}
441449

442450
totalElapsed := time.Since(dlStart)

0 commit comments

Comments
 (0)