Skip to content

Commit 0597b51

Browse files
committed
Optimize S3 transfer: streaming parallel download, multipart upload
Three performance improvements to the S3 transfer layer: stat() now returns Content-Length alongside the existence check, so get() can plan parallel range requests without issuing a second HEAD request. parallelGet() no longer buffers each 32 MiB chunk into a Go []byte via io.ReadAll. Workers now send the open http.Response body to a per-chunk channel; the serial writer streams each body directly into the pipe with io.Copy. In-flight chunks wait in kernel TCP receive buffers rather than Go heap, eliminating the ~256 MiB of allocation pressure and GC work that occurred during every download. put() uses S3 multipart upload for objects larger than 64 MiB (8 parts in parallel, 64 MiB each). Smaller objects continue to use single-part PUT. Parallel part uploads match the throughput of parallel range GETs. The upload is aborted automatically on any part failure.
1 parent fc41d39 commit 0597b51

2 files changed

Lines changed: 234 additions & 56 deletions

File tree

cmd/gradle-cache/main.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,12 @@ func (c *RestoreCmd) Run(ctx context.Context) error {
9393
}
9494

9595
var hitKey string
96+
var hitSize int64
9697
for _, sha := range commits {
9798
key := s3Key(sha, c.CacheKey, bundleFile)
98-
if err := client.stat(ctx, c.Bucket, key); err == nil {
99+
if size, err := client.stat(ctx, c.Bucket, key); err == nil {
99100
hitKey = key
101+
hitSize = size
100102
break
101103
}
102104
slog.Debug("cache miss", "sha", sha[:min(8, len(sha))])
@@ -123,7 +125,7 @@ func (c *RestoreCmd) Run(ctx context.Context) error {
123125
return errors.Wrap(err, "create temp dir")
124126
}
125127

126-
body, _, err := client.get(ctx, c.Bucket, hitKey)
128+
body, err := client.get(ctx, c.Bucket, hitKey, hitSize)
127129
if err != nil {
128130
return errors.Wrap(err, "get bundle")
129131
}
@@ -148,12 +150,10 @@ func (c *RestoreCmd) Run(ctx context.Context) error {
148150
"speed_mbps", fmt.Sprintf("%.1f", float64(cb.n)/dlElapsed.Seconds()/1e6))
149151
}
150152

151-
// Log total restore time (find + download + extraction, all pipelined).
152-
// The "extract tail" is the small gap between the last byte being consumed
153-
// and the last file being written; most extraction happened during download.
153+
// Log total restore time. Download and extraction are pipelined so
154+
// total ≈ download time + a small flush of buffered pipeline stages.
154155
slog.Info("restore pipeline complete",
155-
"total_duration", totalElapsed.Round(time.Millisecond),
156-
"extract_tail", time.Since(cb.eofAt).Round(time.Millisecond))
156+
"total_duration", totalElapsed.Round(time.Millisecond))
157157

158158
// Symlink $GRADLE_USER_HOME/caches → tmpDir/caches.
159159
cachesTarget := filepath.Join(tmpDir, "caches")
@@ -269,7 +269,7 @@ func (c *SaveCmd) Run(ctx context.Context) error {
269269
key := s3Key(c.Commit, c.CacheKey, bundleFile)
270270

271271
// Skip upload if bundle already exists.
272-
if err := client.stat(ctx, c.Bucket, key); err == nil {
272+
if _, err := client.stat(ctx, c.Bucket, key); err == nil {
273273
slog.Info("bundle already exists", "key", key)
274274
return nil
275275
}

0 commit comments

Comments
 (0)