Skip to content

Apply BUNDLE_RETRY to lazy gemspec downloads - #9868

Open
IslamElsayed wants to merge 1 commit into
ruby:masterfrom
IslamElsayed:fix-fetch-spec-retry
Open

Apply BUNDLE_RETRY to lazy gemspec downloads#9868
IslamElsayed wants to merge 1 commit into
ruby:masterfrom
IslamElsayed:fix-fetch-spec-retry

Conversation

@IslamElsayed

Copy link
Copy Markdown

Closes #9817.

What was the end-user or developer problem that led to this PR?

Resolving through the full-index API lazily downloads individual gemspecs from /quick/Marshal.4.8/*.gemspec.rz. One transient failure on any of those — a read timeout, a reset connection — aborts the whole resolution, no matter what BUNDLE_RETRY is set to.

It bites hardest without a lockfile, where a resolve can fetch a great many gemspecs one at a time, so the chance that at least one hits a blip grows with the size of the dependency set. The reporter saw it as repeated production CI failures.

What is your fix for the problem, implemented in this PR?

Fetcher#fetch_spec made the request bare:

Bundler.safe_load_marshal Bundler.rubygems.inflate(downloader.fetch(uri).body)

while specs_with_retry, twenty lines below in the same class, wraps its network call:

Bundler::Retry.new("fetcher", FAIL_ERRORS).attempts { specs(gem_names, source) }

So the retry machinery and its exclusion list already exist and are already tuned — FAIL_ERRORS is there precisely to stop auth failures and other non-transient responses from being retried. The lazy per-gemspec path simply never opted in. This wraps that download in the same Bundler::Retry.new("fetcher", FAIL_ERRORS), so both network paths through Fetcher now behave the same way.

Only the download branch is wrapped. The file:// and cached-gemspec branches do no network I/O, so retrying them would only repeat a local failure — and a corrupt cached gemspec should surface immediately rather than after N attempts.

Alternative considered: retrying inside Downloader#fetch, which is where the network exceptions are classified. I didn't, because Downloader is shared by every fetcher path — including ones already wrapped in Bundler::Retry — and retrying there would multiply attempts for those callers rather than fix the one that is missing them.

Make sure the following tasks are checked

  • Describe the problem / feature
  • Write tests for features and bug fixes

Two cases in spec/bundler/fetcher_spec.rb, following the existing double-based style there:

  • the first download raises and the second succeeds, and fetch_spec returns the spec. This fails on master — the error propagates on the first attempt.
  • a FAIL_ERRORS member (AuthenticationRequiredError) is raised on the first attempt and not retried. This passes either way by design; it guards against the fix over-retrying things it shouldn't.

Verified green: spec/bundler/fetcher_spec.rb (25 examples), plus spec/bundler/fetcher/, remote_specification_spec.rb, endpoint_specification_spec.rb and retry_spec.rb together (145 examples, 2 pre-existing pending). bin/rake rubocop reports no offenses across 852 files.

Disclosure

Written with AI assistance (Claude). I can explain every line, and the behaviour was verified by running the tests rather than inferred.

Resolving through the full-index API lazily downloads individual
gemspecs from /quick/Marshal.4.8/*.gemspec.rz. `Fetcher#fetch_spec` made
that request bare:

    Bundler.safe_load_marshal Bundler.rubygems.inflate(downloader.fetch(uri).body)

while `specs_with_retry`, in the same class, wraps its network call in
`Bundler::Retry`. So a single transient failure — a read timeout, say —
aborted the whole resolution, no matter what BUNDLE_RETRY was set to.
That is most painful without a lockfile, where a resolve can fetch a
great many gemspecs one at a time and any one of them can end it.

Wrap the download in the same `Bundler::Retry.new("fetcher", FAIL_ERRORS)`
the index path already uses. FAIL_ERRORS is what keeps this from retrying
things that will not change, such as authentication failures.

Only the download branch is wrapped. The file:// and cached-gemspec
branches do no network I/O, so retrying them would just repeat a local
failure.

Closes ruby#9817
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.

BUNDLE_RETRY is not applied to lazy full-index quick/Marshal gemspec downloads

1 participant