utils: extract rate limit sleep and exponential backoff helpers

Address review feedback:
- Add GitHub::API.sleep_for_rate_limit and use it in dev-cmd/bump.rb
  and dev-cmd/contributions.rb.
- Add Utils.exponential_backoff_sleep and use it in dev-cmd/bump.rb,
  retryable_download.rb and github_packages.rb.
This commit is contained in:
moscowmule2240
2026-08-12 21:56:32 +09:00
parent 9b11042dad
commit d24e884305
8 changed files with 59 additions and 13 deletions
+4 -6
View File
@@ -876,9 +876,7 @@ module Homebrew
)
consecutive_github_api_errors = 0
rescue GitHub::API::RateLimitExceededError => e
sleep_seconds = [e.reset - Time.now.to_i, 1].max
opoo "GitHub rate limit exceeded, sleeping for #{sleep_seconds} seconds..."
sleep sleep_seconds
GitHub::API.sleep_for_rate_limit(e)
retry
rescue GitHub::API::AuthenticationFailedError
# Retrying this for the remaining packages cannot succeed, so stop now.
@@ -886,9 +884,9 @@ module Homebrew
rescue GitHub::API::Error => e
github_api_retries += 1
if github_api_retries <= MAX_GITHUB_API_RETRIES
wait = 2 ** github_api_retries
onoe "#{name}: retrying in #{wait}s after a GitHub API error: #{e}"
sleep wait
Utils.exponential_backoff_sleep(github_api_retries) do |wait|
onoe "#{name}: retrying in #{wait}s after a GitHub API error: #{e}"
end
retry
end
+1 -3
View File
@@ -516,9 +516,7 @@ module Homebrew
end
results
rescue GitHub::API::RateLimitExceededError => e
sleep_seconds = [e.reset - Time.now.to_i, 1].max
opoo "GitHub rate limit exceeded, sleeping for #{sleep_seconds} seconds..."
sleep sleep_seconds
GitHub::API.sleep_for_rate_limit(e)
retry
end
+1 -1
View File
@@ -366,7 +366,7 @@ class GitHubPackages
rescue ErrorDuringExecution
retry_count += 1
odie "Cannot perform an upload to registry after retrying multiple times!" if retry_count >= 10
sleep 2 ** retry_count
Utils.exponential_backoff_sleep(retry_count)
retry
end
+3 -3
View File
@@ -88,12 +88,12 @@ module Homebrew
tries_remaining = @tries - @try
raise if tries_remaining.zero?
wait = 2 ** @try
unless quiet
Utils.exponential_backoff_sleep(@try) do |wait|
next if quiet
what = Utils.pluralize("try", tries_remaining)
ohai "Retrying download in #{wait}s... (#{tries_remaining} #{what} left)"
end
sleep wait
# Preserve the partial `.incomplete` file on network errors so the next
# attempt can resume via `--continue-at`. Clear the cache only when the
@@ -0,0 +1,16 @@
# typed: true
# frozen_string_literal: true
require "utils/github"
RSpec.describe GitHub::API do
describe "::sleep_for_rate_limit" do
it "sleeps for at least 1 second even if the rate limit has already reset" do
exception = GitHub::API::RateLimitExceededError.new(
"API rate limit exceeded", reset: Time.now.to_i - 10, resource: "core", limit: 5000
)
expect(described_class).to receive(:sleep).with(1)
described_class.sleep_for_rate_limit(exception)
end
end
end
+17
View File
@@ -157,6 +157,23 @@ RSpec.describe Utils do
end
end
describe ".exponential_backoff_sleep" do
it "sleeps for 2**try seconds" do
expect(described_class).to receive(:sleep).with(8)
described_class.exponential_backoff_sleep(3)
end
it "sleeps for base**try seconds when a base is given" do
expect(described_class).to receive(:sleep).with(27)
described_class.exponential_backoff_sleep(3, base: 3)
end
it "yields the wait time before sleeping" do
allow(described_class).to receive(:sleep)
expect { |block| described_class.exponential_backoff_sleep(2, &block) }.to yield_with_args(4)
end
end
describe ".underscore" do
# commented out entries require acronyms inflections
let(:words) do
+9
View File
@@ -112,6 +112,15 @@ module Utils
"#{prefix}#{stem}#{suffix}"
end
# Sleeps for an exponentially increasing wait (`base ** try` seconds), yielding
# the wait time first so callers can print a message before sleeping.
sig { params(try: Integer, base: Integer, _blk: T.nilable(T.proc.params(wait: Integer).void)).void }
def self.exponential_backoff_sleep(try, base: 2, &_blk)
wait = base.pow(try)
yield wait if block_given?
sleep wait
end
sig { params(author: String).returns({ email: String, name: String }) }
def self.parse_author!(author)
match_data = /^(?<name>[^<]+?)[ \t]*<(?<email>[^>]+?)>$/.match(author)
+8
View File
@@ -169,6 +169,14 @@ module GitHub
JSON::ParserError,
].freeze
# Sleeps until the rate limit from the given exception has reset.
sig { params(exception: RateLimitExceededError).void }
def self.sleep_for_rate_limit(exception)
sleep_seconds = [exception.reset - Time.now.to_i, 1].max
opoo "GitHub rate limit exceeded, sleeping for #{sleep_seconds} seconds..."
sleep sleep_seconds
end
# Gets the token from the GitHub CLI for github.com.
sig { returns(T.nilable(String)) }
def self.github_cli_token