Skip to content

perf: run a chart's bundled image pushers concurrently - #291

Open
cristifalcas wants to merge 1 commit into
periareon:mainfrom
aspect-forks:cf/parallel-image-pushers
Open

perf: run a chart's bundled image pushers concurrently#291
cristifalcas wants to merge 1 commit into
periareon:mainfrom
aspect-forks:cf/parallel-image-pushers

Conversation

@cristifalcas

Copy link
Copy Markdown
Contributor

A chart's image pushers ran one at a time. Each pusher is a separate process that re-does the registry auth handshake from scratch first thing, so the wall time scaled linearly with the image count even when every image was already present in the registry. An 11-image chart spent over a minute per helm_upgrade re-checking images it had already pushed.

Run the pushers concurrently, up to RULES_HELM_IMAGE_PUSH_CONCURRENCY (default 8). Set it to 1 to restore the previous serial behaviour.

Consequences:

  • Concurrent pushers writing to a shared stdout interleave line by line. Each pusher's output is buffered and flushed as one block, so a block always stays attributable to its pusher.
  • Failing on the first error would abandon pushers already in flight. Every pusher now runs to completion and the errors are joined, so a run reports all of its failures rather than only the earliest.

Test: ~15s with this, against ~68s with main.

A chart's image pushers ran one at a time. Each pusher is a separate
process that re-does the registry auth handshake from scratch before it
looks at anything, so the wall time scaled linearly with the image count
even when every image was already present in the registry. An 11-image
chart spent over a minute per `helm_upgrade` re-checking images it had
already pushed.

The same sequential loop was duplicated across the runner, pusher and
registrar, so hoist it into `helm_utils.RunImagePushers` and run the
pushers concurrently, bounded by `RULES_HELM_IMAGE_PUSH_CONCURRENCY`
(default 4). The default is deliberately modest: the setting multiplies an
already-parallel conversation rather than starting one -- remote.Write
uploads a single image's layers 4 at a time, and nests that again per child
for a multi-platform index -- and a registry that throttles with a bare
HTTP 429 instead of a docker-distribution error body gets no retry out of
go-containerregistry, so an aggressive default turns throttling into a
failed push.

Three details follow from running the pushers at once:

  - Every pusher is announced before its process starts, so the run is
    never silent and a push that stalls against a registry names itself. A
    CI step with a no-output timeout would otherwise be killed with an
    empty log.

  - Concurrent pushers writing to a shared stdout interleave line by line,
    so each one's output is captured and replayed as a single block when it
    exits. At a concurrency of 1 nothing can collide, and the pusher
    inherits stdout and stderr and writes through live -- which is what
    makes `RULES_HELM_IMAGE_PUSH_CONCURRENCY=1` a real fallback to the
    previous behaviour rather than a nominal one.

  - Failing on the first error would abandon pushers already in flight.
    Every pusher now runs to completion and the errors are joined, so a run
    reports all of its failures rather than only the earliest.

The variable is documented in the README and on each of the four rules that
honour it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@cristifalcas
cristifalcas marked this pull request as ready for review August 5, 2026 19:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant