diff --git a/github_changelog_generator.gemspec b/github_changelog_generator.gemspec index c114b9c..a621547 100644 --- a/github_changelog_generator.gemspec +++ b/github_changelog_generator.gemspec @@ -29,4 +29,5 @@ Gem::Specification.new do |spec| spec.add_runtime_dependency("octokit", ["~> 4.0"]) spec.add_runtime_dependency("faraday-http-cache") spec.add_runtime_dependency("activesupport") + spec.add_runtime_dependency("retriable", ["~> 2.1"]) end diff --git a/lib/github_changelog_generator/octo_fetcher.rb b/lib/github_changelog_generator/octo_fetcher.rb index aa56d01..2e68f51 100644 --- a/lib/github_changelog_generator/octo_fetcher.rb +++ b/lib/github_changelog_generator/octo_fetcher.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true +require "retriable" module GitHubChangelogGenerator # A Fetcher responsible for all requests to GitHub and all basic manipulation with related data # (such as filtering, validating, e.t.c) @@ -8,6 +9,7 @@ module GitHubChangelogGenerator class OctoFetcher PER_PAGE_NUMBER = 100 MAX_THREAD_NUMBER = 25 + MAX_FORBIDDEN_RETRIES = 100 CHANGELOG_GITHUB_TOKEN = "CHANGELOG_GITHUB_TOKEN" GH_RATE_LIMIT_EXCEEDED_MSG = "Warning: Can't finish operation: GitHub API rate limit exceeded, change log may be " \ "missing some issues. You can limit the number of issues fetched using the `--max-issues NUM` argument." @@ -280,17 +282,45 @@ Make sure, that you push tags to remote repo via 'git push --tags'" # # @return [Object] returns exactly the same, what you put in the block, but wrap it with begin-rescue block def check_github_response - begin - value = yield - rescue Octokit::Unauthorized => e - Helper.log.error e.message - abort "Error: wrong GitHub token" - rescue Octokit::Forbidden => e - Helper.log.warn e.message + Retriable.retriable(retry_options) do + yield + end + + rescue Octokit::Forbidden => e + Helper.log.error("#{e.class}: #{e.message}") + sys_abort("Exceeded retry limit") + rescue Octokit::Unauthorized => e + Helper.log.error("#{e.class}: #{e.message}") + sys_abort("Error: wrong GitHub token") + end + + # Exponential backoff + def retry_options + { + on: [Octokit::Forbidden], + tries: MAX_FORBIDDEN_RETRIES, + base_interval: sleep_base_interval, + multiplier: 1.0, + rand_factor: 0.0, + on_retry: retry_callback + } + end + + def sleep_base_interval + 1.0 + end + + def retry_callback + proc do |exception, try, elapsed_time, next_interval| + Helper.log.warn("RETRY - #{exception.class}: '#{exception.message}'") + Helper.log.warn("#{try} tries in #{elapsed_time} seconds and #{next_interval} seconds until the next try") Helper.log.warn GH_RATE_LIMIT_EXCEEDED_MSG Helper.log.warn @client.rate_limit end - value + end + + def sys_abort(msg) + abort(msg) end # Print specified line on the same string diff --git a/lib/github_changelog_generator/parser.rb b/lib/github_changelog_generator/parser.rb index 9323e46..c62f187 100755 --- a/lib/github_changelog_generator/parser.rb +++ b/lib/github_changelog_generator/parser.rb @@ -42,7 +42,7 @@ module GitHubChangelogGenerator # setup parsing options def self.setup_parser(options) - parser = OptionParser.new do |opts| + parser = OptionParser.new do |opts| # rubocop:disable Metrics/BlockLength opts.banner = "Usage: github_changelog_generator [options]" opts.on("-u", "--user [USER]", "Username of the owner of target GitHub repo") do |last| options[:user] = last diff --git a/spec/unit/octo_fetcher_spec.rb b/spec/unit/octo_fetcher_spec.rb index f74e25f..f2dbdeb 100644 --- a/spec/unit/octo_fetcher_spec.rb +++ b/spec/unit/octo_fetcher_spec.rb @@ -12,6 +12,31 @@ describe GitHubChangelogGenerator::OctoFetcher do let(:fetcher) { GitHubChangelogGenerator::OctoFetcher.new(options) } + describe "#check_github_response" do + context "when returns successfully" do + it "returns block value" do + expect(fetcher.send(:check_github_response) { 1 + 1 }).to eq(2) + end + end + + context "when raises Octokit::Unauthorized" do + it "aborts" do + expect(fetcher).to receive(:sys_abort).with("Error: wrong GitHub token") + fetcher.send(:check_github_response) { raise(Octokit::Unauthorized) } + end + end + + context "when raises Octokit::Forbidden" do + it "sleeps and retries and then aborts" do + retry_limit = GitHubChangelogGenerator::OctoFetcher::MAX_FORBIDDEN_RETRIES - 1 + allow(fetcher).to receive(:sleep_base_interval).exactly(retry_limit).times.and_return(0) + + expect(fetcher).to receive(:sys_abort).with("Exceeded retry limit") + fetcher.send(:check_github_response) { raise(Octokit::Forbidden) } + end + end + end + describe "#fetch_github_token" do token = GitHubChangelogGenerator::OctoFetcher::CHANGELOG_GITHUB_TOKEN context "when token in ENV exist" do