Skip to content

Extract GlotPress download retry logic and apply to all cases#667

Merged
iangmaia merged 10 commits intotrunkfrom
iangmaia/refactor-glotpress-downloader
Oct 18, 2025
Merged

Extract GlotPress download retry logic and apply to all cases#667
iangmaia merged 10 commits intotrunkfrom
iangmaia/refactor-glotpress-downloader

Conversation

@iangmaia
Copy link
Contributor

@iangmaia iangmaia commented Oct 17, 2025

Related to p1760606044394999-slack-CC7L49W13
Fixes AINFRA-1419

What does it do?

This PR extracts the GlotPress download code with retry logic originally from metadata_download_helper.rb to a separate helper GlotPressDownloader, and use that same helper also in android_localize_helper.rb and ios_l10n_helper.rb.

Testing

I've created a draft PR where I ran lanes that download GlotPress: wordpress-mobile/WordPress-Android#22291 and triggered two builds:

Checklist before requesting a review

  • Run bundle exec rubocop to test for code style violations and recommendations.
  • Add Unit Tests (aka specs/*_spec.rb) if applicable.
  • Run bundle exec rspec to run the whole test suite and ensure all your tests pass.
  • Make sure you added an entry in the CHANGELOG.md file to describe your changes under the appropriate existing ### subsection of the existing ## Trunk section.
  • If applicable, add an entry in the MIGRATION.md file to describe how the changes will affect the migration from the previous major version and what the clients will need to change and consider.

@iangmaia iangmaia self-assigned this Oct 17, 2025
@iangmaia iangmaia added bug Something isn't working enhancement New feature or request labels Oct 17, 2025
@iangmaia iangmaia changed the title Extract GlotPress translations download logic to separate GlotPressDownloader helper Extract GlotPress download retry logic and apply to all cases Oct 17, 2025
@iangmaia iangmaia marked this pull request as ready for review October 17, 2025 10:35
Copy link
Contributor

@AliSoftware AliSoftware left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Preliminary review, I didn't check the whole code in details nor the unit tests just yet, but leaving my initial high-level thoughts still as a first pass.

@AliSoftware AliSoftware self-requested a review October 17, 2025 10:47
# @return The result of the block if provided, or true/false indicating success
#
def download(url, locale)
@current_locale = locale # Store for error handling
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there no way to propagate that locale to each method call that might need it, instead of using an instance variable, to avoid that risk altogether?
For example I think just passing it as a parameter to make_request (the only method which seems to reference that @current_locale apparently?) would be better?

Especially, I don't think we use Parallel.map right now to download multiple locales in parallel—but instead we just use a .each serial loop, right?—…but if we ever do, and thus one GlotPressDownloader might be reused for multiple loop iterations in parallel, storing that in an instance variable would lead to race condition issues.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, the fact that we have to remember to reset the auto-retry counter once we get a 200 response (line 64) so that we're ready to handle the next call to download in the next caller loop iteration… makes it brittle too as well given the reuse context.

So I wonder if we shouldn't instead require to create a new instance for every single download, i.e. not have a single GlotpressDownloader instance created outside of the caller's loop and calling downloader.download(url, locale) on the same instance inside each loop iteration, but instead have the url + locale be on the constructor (def initialize) and download take no argument at all, to enfoce each loop iteration having to create a separate instance, so that we're guaranteed that there won't be any instance variables with old values we forgot to reset between loops, and that we wouldn't risk having race conditions from reuse (especially if we migrate to Parallel.map one day…)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great suggestion, thanks! 👍 it makes the state management easier to follow. Updated on 19fc711.

# Unexpected status code (including 404, 500, etc.)
status_line = "#{response.code} #{response.message}"
UI.error("Error downloading locale `#{locale}` — #{status_line} (#{original_uri})")
if !FastlaneCore::Helper.is_ci? && UI.confirm("Retry downloading `#{locale}`?")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if is_ci? is the right condition for it here, as opposed to "is the current terminal interactive"… which I think Fastlane's UI module already have a helper for?

[EDIT] Yes, UI.interactive? is the one I had in mind 💭

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good one! 👍 Updated on 5f11e78.

Copy link
Contributor

@AliSoftware AliSoftware left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving to unblock, as the 2 comments are nitpicks not blockers.

PS: Probably worth testing this on a real project (e.g. making wordpress-android point its Gemfile to this PR's branch and run a test lane calling this to validate it works in real world use case) before merging, just to be extra sure.

# @yield [String] The response body if the download was successful
# @return The result of the block if provided, or true/false indicating success if no block provided
#
def download(&)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that all call sites will now basically always call downloader = GlotpressDownloader.new(…) followed immediately by downloader.download, I wonder if it would be worth adding a convenience class method too to make all the call sites that use that pattern even simpler?

def self.download(url:, locale:, auto_retry: false, &)
  downloader = self.new(url: url, locale: locale, auto_retry: auto_retry)
  downloader.download(&)
end

…der.rb

Co-authored-by: Olivier Halligon <olivier.halligon@automattic.com>
@iangmaia
Copy link
Contributor Author

PS: Probably worth testing this on a real project

Done, updated the description with a reference to a draft PR.

@iangmaia iangmaia merged commit dbb739c into trunk Oct 18, 2025
6 checks passed
@iangmaia iangmaia deleted the iangmaia/refactor-glotpress-downloader branch October 18, 2025 00:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants