-
Notifications
You must be signed in to change notification settings - Fork 5.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[deliver] increase chances of success when creating a new app version even when Apple servers are degraded #21742
Changes from 8 commits
a0ac71c
b30f0ea
34e7132
9c7a720
fa444bb
8fbfc33
e415c7c
d7bbaec
064f5c1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -89,7 +89,7 @@ def upload(options) | |
enabled_languages = detect_languages(options) | ||
|
||
app_store_version_localizations = verify_available_version_languages!(options, app, enabled_languages) unless options[:edit_live] | ||
app_info = fetch_edit_app_info(app) | ||
app_info = fetch_edit_app_info(app, max_retries: options[:version_check_wait_retry_limit]) | ||
app_info_localizations = verify_available_info_languages!(options, app, app_info, enabled_languages) unless options[:edit_live] || !updating_localized_app_info?(options, app, app_info) | ||
|
||
if options[:edit_live] | ||
|
@@ -100,7 +100,7 @@ def upload(options) | |
|
||
if version.nil? | ||
UI.message("Couldn't find live version, editing the current version on App Store Connect instead") | ||
version = fetch_edit_app_store_version(app, platform) | ||
version = fetch_edit_app_store_version(app, platform, max_retries: options[:version_check_wait_retry_limit]) | ||
# we don't want to update the localised_options and non_localised_options | ||
# as we also check for `options[:edit_live]` at other areas in the code | ||
# by not touching those 2 variables, deliver is more consistent with what the option says | ||
|
@@ -109,7 +109,7 @@ def upload(options) | |
UI.message("Found live version") | ||
end | ||
else | ||
version = fetch_edit_app_store_version(app, platform) | ||
version = fetch_edit_app_store_version(app, platform, max_retries: options[:version_check_wait_retry_limit]) | ||
localised_options = (LOCALISED_VERSION_VALUES.keys + LOCALISED_APP_VALUES.keys) | ||
non_localised_options = NON_LOCALISED_VERSION_VALUES.keys | ||
end | ||
|
@@ -427,41 +427,49 @@ def detect_languages(options) | |
.uniq | ||
end | ||
|
||
def fetch_edit_app_store_version(app, platform, wait_time: 10) | ||
retry_if_nil("Cannot find edit app store version", wait_time: wait_time) do | ||
def fetch_edit_app_store_version(app, platform, max_retries:, initial_wait_time: 10) | ||
retry_if_nil("Cannot find edit app store version", tries: max_retries, initial_wait_time: initial_wait_time) do | ||
app.get_edit_app_store_version(platform: platform) | ||
end | ||
end | ||
|
||
def fetch_edit_app_info(app, wait_time: 10) | ||
retry_if_nil("Cannot find edit app info", wait_time: wait_time) do | ||
def fetch_edit_app_info(app, max_retries:, initial_wait_time: 10) | ||
retry_if_nil("Cannot find edit app info", tries: max_retries, initial_wait_time: initial_wait_time) do | ||
app.fetch_edit_app_info | ||
end | ||
end | ||
|
||
def fetch_live_app_info(app, wait_time: 10) | ||
retry_if_nil("Cannot find live app info", wait_time: wait_time) do | ||
def fetch_live_app_info(app, max_retries:, initial_wait_time: 10) | ||
retry_if_nil("Cannot find live app info", tries: max_retries, initial_wait_time: initial_wait_time) do | ||
app.fetch_live_app_info | ||
end | ||
end | ||
|
||
def retry_if_nil(message, tries: 5, wait_time: 10) | ||
# Retries a block of code if the return value is nil, with an exponential backoff. | ||
def retry_if_nil(message, tries:, initial_wait_time: 10) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. CI will timeout in this retry method because of def retry_if_nil(message, tries:, initial_wait_time: 10)
wait_time = initial_wait_time
loop do
tries -= 1
value = yield
return value if value
UI.message("#{message}... Retrying after #{wait_time} seconds (remaining: #{tries} tries)")
retrying_status_time = 0
while retrying_status_time < wait_time do
UI.message("Retrying status: #{retrying_status_time}/#{wait_time} seconds")
sleep(initial_wait_time)
retrying_status_time += initial_wait_time
end
return nil if tries.zero?
wait_time *= 2 # Double the wait time for the next iteration
end
end DiffOutput:There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a very good point @crazymanish ! I'm torn between your implementation (which's great IMO), or incorporating the feedback discussed earlier in this PR about trying exponentially only until a certain threshold (e.g. 5 minutes), and then from then on keep trying every 5 minutes. As long as this threshold is <10minutes (which is the default console log timeout used in many CI services), we should be good. What do you think? Should we incorporate your code regardless of the decision above? There's no harm in it, just makes code more complicated (justifiably so) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Aha! i missed the threshold comment...Any retrying logic before the CI timeout (10 minutes) sounds good... Another small thing we could do is: we can have the customized threshold value This will give freedom to our users to retry after every 1 minute, 3 minutes, 5 minutes, or any X minutes (based on their CI timeout X values)
wait_time = min(wait_time * 2, version_check_wait_retry_threshold * 60)` btw, Thank you very much for doing the good work in this PR, it seems lots of people are facing this issue!! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for the suggestion @crazymanish 🙏 I usually try to avoid bloating up the actions/tools with so many options, and I think in this case there's no need to allow that level of customization. I'd go with that implementation and go for a safe fixed number of 5 minutes which seems to be reasonable and work with every CI provider I can think of 👀 do you feel strongly we should allow customization of this by the users? I'm going to go ahead and implement this now but I can change later too 🙏 |
||
wait_time = initial_wait_time | ||
loop do | ||
tries -= 1 | ||
|
||
value = yield | ||
return value if value | ||
|
||
UI.message("#{message}... Retrying after #{wait_time} seconds (remaining: #{tries})") | ||
sleep(wait_time) | ||
# Calculate sleep time to be the lesser of the exponential backoff or 5 minutes. | ||
# This prevents problems with CI's console output timeouts (of usually 10 minutes), and also | ||
# speeds up the retry time for the user, as waiting longer than 5 minutes is a too long wait for a retry. | ||
sleep_time = [wait_time * 2, 5 * 60].min | ||
UI.message("#{message}... Retrying after #{sleep_time} seconds (remaining: #{tries})") | ||
sleep(sleep_time) | ||
|
||
return nil if tries.zero? | ||
|
||
wait_time *= 2 # Double the wait time for the next iteration | ||
end | ||
end | ||
|
||
# Checking if the metadata to update includes localised App Info | ||
def updating_localized_app_info?(options, app, app_info) | ||
app_info ||= fetch_live_app_info(app) | ||
app_info ||= fetch_live_app_info(app, max_retries: options[:version_check_wait_retry_limit]) | ||
unless app_info | ||
UI.important("Can't find edit or live App info. Skipping upload.") | ||
return false | ||
|
@@ -533,7 +541,7 @@ def verify_available_info_languages!(options, app, app_info, languages) | |
# Finding languages to enable | ||
def verify_available_version_languages!(options, app, languages) | ||
platform = Spaceship::ConnectAPI::Platform.map(options[:platform]) | ||
version = fetch_edit_app_store_version(app, platform) | ||
version = fetch_edit_app_store_version(app, platform, max_retries: options[:version_check_wait_retry_limit]) | ||
|
||
unless version | ||
UI.user_error!("Cannot update languages - could not find an editable version for '#{platform}'") | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would consider creating an object called RetryConfig and move all fields related to the retry (
max_retries
,initial_wait_time)
into it.This would change the
retry_if_nil
method like thisand all other calls would pass a single parameter that could be initialized in one place.
Does that make sense?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah it does, just not sure if it's worth unifying these into an object, since it's just 2 configs? I usually do these type of things once there are 3 or more parameters 👀 Do you think this would make a significant difference? In terms of e.g. UX/DX, testability, maintainability…? @lacostej
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We fetch the
options[:version_check_wait_retry_limit]
5 times in the code, I suspect there should be a way to make this a bit better.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's a proposal
fetch_*
methods. We even remove thewait_time
oneretry_if_nil
, and instead, initialize theinitial_wait_time
fromoptions
within the implementationthis should remove most retry specific information in most of the code
0.01
to ensure that they do not take too long. There we could instead stub thesleep
call instead, and just ensure it gets called the right amount of times. It will even make the tests slightly faster. Stubbing sleep requires a bit more work to do right.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@lacostej I think this would be a good approach, I'll try to come back to this later this week 🙏
If you have a clear understanding on how to implement this and wants to take a stab before I have time to get to it, that'd probably help us getting this out of the door faster as I don't think I'll have much availability in the next few days 🙇 if not, all good too! 🙏
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey team, I won't have time to look into this in the upcoming future 😢 @lacostej or anyone else, feel free to pick this up 🙏I'm also ok with merging this in as is and creating a separate task to refactor later (although I suspect it won't be picked up because it won't be a high priority 😞)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hello 👋 I will take it over 😊
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The follow-up pull request that addresses requested changes: #21861 🙇
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mollyIV
Yes this is good. If you look at the overall combined PR, the code is I think cleaner.
thanks for spending the time to do this!