Repository navigation
remote: limit concurrent layer pulls - #2271
Merged
Merged
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #2271 +/- ##
==========================================
+ Coverage 52.03% 52.07% +0.04%
==========================================
Files 165 166 +1
Lines 14203 14234 +31
==========================================
+ Hits 7390 7412 +22
- Misses 6092 6101 +9
Partials 721 721 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
remote.WithJobs controls remote operation parallelism, but lazy images returned by remote.Get(...).Image() did not carry that limit into later layer reads. Consumers such as layout.ReplaceImage could start one remote blob GET per layer at once. Carry the configured job limit into remote-backed blob reads and hold a limiter token until each response body is closed. Fixes google#2140 Reproduced: Added a local repro that pulled an image with 6 layers using remote.WithJobs(2), then concurrently opened every layer's Compressed() stream. Before the fix, the test server observed 6 concurrent layer GETs. After the fix, the maximum observed concurrency was 2. Tested: go test ./pkg/v1/remote go test ./pkg/v1/layout ./pkg/v1/remote
Subserial
force-pushed
the
fix-remote-layer-pull-jobs
branch
from
April 21, 2026 22:25
ae4f5de to
2f9a9ca
Compare
Subserial
approved these changes
Apr 21, 2026
Subserial
pushed a commit
that referenced
this pull request
Apr 28, 2026
crane export uses mutate.Extract for normal layered images. mutate.Extract opened each layer reader in sequence, but deferred each Close until the whole extraction returned. After #2271, remote layer readers hold a pull job slot until their response body is closed, so extraction could fill all available slots and then block opening the next layer. The single-layer crane export path also copied from Uncompressed without closing the reader. Close each extraction layer reader as soon as that layer is processed, and close the single-layer export reader after copying. Fixes #2276 Reproduced: Added a test image whose layer readers model the remote pull limiter: opening a reader consumes one of four slots, and Close releases it. Before the fix, extracting five layers failed when the fifth reader opened before the previous readers were closed. After the fix, extraction completes and all slots are released. Tested: go test ./pkg/v1/mutate go test ./pkg/crane go test ./pkg/v1/remote ./pkg/v1/mutate ./pkg/crane ./cmd/crane/rebase_test.sh
Subserial
pushed a commit
to Subserial/go-containerregistry
that referenced
this pull request
May 15, 2026
remote.WithJobs controls remote operation parallelism, but lazy images returned by remote.Get(...).Image() did not carry that limit into later layer reads. Consumers such as layout.ReplaceImage could start one remote blob GET per layer at once. Carry the configured job limit into remote-backed blob reads and hold a limiter token until each response body is closed. Fixes google#2140 Reproduced: Added a local repro that pulled an image with 6 layers using remote.WithJobs(2), then concurrently opened every layer's Compressed() stream. Before the fix, the test server observed 6 concurrent layer GETs. After the fix, the maximum observed concurrency was 2. Tested: go test ./pkg/v1/remote go test ./pkg/v1/layout ./pkg/v1/remote
Subserial
pushed a commit
to Subserial/go-containerregistry
that referenced
this pull request
May 15, 2026
crane export uses mutate.Extract for normal layered images. mutate.Extract opened each layer reader in sequence, but deferred each Close until the whole extraction returned. After google#2271, remote layer readers hold a pull job slot until their response body is closed, so extraction could fill all available slots and then block opening the next layer. The single-layer crane export path also copied from Uncompressed without closing the reader. Close each extraction layer reader as soon as that layer is processed, and close the single-layer export reader after copying. Fixes google#2276 Reproduced: Added a test image whose layer readers model the remote pull limiter: opening a reader consumes one of four slots, and Close releases it. Before the fix, extracting five layers failed when the fifth reader opened before the previous readers were closed. After the fix, extraction completes and all slots are released. Tested: go test ./pkg/v1/mutate go test ./pkg/crane go test ./pkg/v1/remote ./pkg/v1/mutate ./pkg/crane ./cmd/crane/rebase_test.sh
This was referenced May 19, 2026
nandbhat
added a commit
to nandbhat/go-containerregistry
that referenced
this pull request
May 21, 2026
tarball.MultiRefWrite (and crane pull, which uses it) opens each layer's Compressed() reader and copies it into the tar without closing it. After google#2271, remote layer readers hold a pull-job slot until their response body is closed, so writing more than `defaultJobs` layers fills all available slots and deadlocks the next layer open. This is the symmetric leak to the one google#2277 fixed in mutate.Extract / crane export. Close each layer reader as soon as that layer is written. Reproduced: crane pull postgres:16 /tmp/pg.tar # v0.21.6 hangs; v0.21.5 works Added TestWriteClosesLayerBeforeOpeningNext, which wraps a random.Image with a token-limited Compressed() that fails if a reader is opened before previous readers were closed. Before this change the test fails with "layer reader opened before previous readers were closed". After this change the test passes and the limiter is fully drained. Tested: go test ./pkg/v1/tarball go test ./pkg/v1/tarball ./pkg/v1/mutate ./pkg/crane Signed-off-by: Nandan Bhat <nandanbhatd@gmail.com>
Subserial
pushed a commit
that referenced
this pull request
May 21, 2026
* tarball: close layer readers during Write tarball.MultiRefWrite (and crane pull, which uses it) opens each layer's Compressed() reader and copies it into the tar without closing it. After #2271, remote layer readers hold a pull-job slot until their response body is closed, so writing more than `defaultJobs` layers fills all available slots and deadlocks the next layer open. This is the symmetric leak to the one #2277 fixed in mutate.Extract / crane export. Close each layer reader as soon as that layer is written. Reproduced: crane pull postgres:16 /tmp/pg.tar # v0.21.6 hangs; v0.21.5 works Added TestWriteClosesLayerBeforeOpeningNext, which wraps a random.Image with a token-limited Compressed() that fails if a reader is opened before previous readers were closed. Before this change the test fails with "layer reader opened before previous readers were closed". After this change the test passes and the limiter is fully drained. Tested: go test ./pkg/v1/tarball go test ./pkg/v1/tarball ./pkg/v1/mutate ./pkg/crane Signed-off-by: Nandan Bhat <nandanbhatd@gmail.com> * tarball: place writeLayer next to writeTarEntry Address review feedback on #2308: the previous patch inserted writeLayer between writeTarEntry's leading doc-comment and its declaration, orphaning the comment. Move writeLayer immediately after writeTarEntry so the existing function block is untouched, and tighten writeLayer's own doc-comment to the non-obvious WHY (pull-limiter slot release). No behavioral change. Signed-off-by: Nandan Bhat <nandanbhatd@gmail.com>
1 task
amarkdotdev
added a commit
to amarkdotdev/go-containerregistry
that referenced
this pull request
Jun 17, 2026
Move defer r.Close() and defer pw.Close() to the top of the goroutine so they execute regardless of which error path exits the function. Previously, r.Close() was only called inside the io.Copy error path and the happy-path end, leaving it unclosed when gw.Close() or bw.Flush() failed. When r wraps a limitedReadCloser (used by the pullLimiter introduced in google#2271), failing to close r permanently leaks the limiter token slot. Under layout.WriteImage (used by kaniko for build-stage caching), this eventually deadlocks because all slots are occupied. Also add defer gw.Close() after gw creation so the gzip writer is always properly finalized. Fixes google#2342 Signed-off-by: Aaron <aaroniofjm@gmail.com>
Subserial
pushed a commit
that referenced
this pull request
Jun 17, 2026
Move defer r.Close() and defer pw.Close() to the top of the goroutine so they execute regardless of which error path exits the function. Previously, r.Close() was only called inside the io.Copy error path and the happy-path end, leaving it unclosed when gw.Close() or bw.Flush() failed. When r wraps a limitedReadCloser (used by the pullLimiter introduced in #2271), failing to close r permanently leaks the limiter token slot. Under layout.WriteImage (used by kaniko for build-stage caching), this eventually deadlocks because all slots are occupied. Also add defer gw.Close() after gw creation so the gzip writer is always properly finalized. Fixes #2342 Signed-off-by: Aaron <aaroniofjm@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
remote.WithJobscontrols remote operation parallelism, but lazy images returned byremote.Get(...).Image()did not carry that limit into later layer reads. Consumers such aslayout.ReplaceImagecould start one remote blob GET per layer at once.Carry the configured job limit into remote-backed blob reads and hold a limiter token until each response body is closed.
Fixes #2140
Reproduced:
Added a local repro that pulled an image with 6 layers using
remote.WithJobs(2), then concurrently opened every layer'sCompressed()stream. Before the fix, the test server observed 6 concurrent layer GETs. After the fix, the maximum observed concurrency was 2.Tested:
go test ./pkg/v1/remotego test ./pkg/v1/layout ./pkg/v1/remote