Skip to content
This repository has been archived by the owner on Apr 14, 2021. It is now read-only.

Retry fetch specs with --retry #2601

Merged
merged 2 commits into from Sep 28, 2013
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/bundler.rb
Expand Up @@ -33,6 +33,7 @@ module Bundler
autoload :MatchPlatform, 'bundler/match_platform'
autoload :RemoteSpecification, 'bundler/remote_specification'
autoload :Resolver, 'bundler/resolver'
autoload :Retry, 'bundler/retry'
autoload :RubyVersion, 'bundler/ruby_version'
autoload :RubyDsl, 'bundler/ruby_dsl'
autoload :Runtime, 'bundler/runtime'
Expand Down
8 changes: 4 additions & 4 deletions lib/bundler/cli.rb
Expand Up @@ -15,6 +15,8 @@ def self.start(*)

def initialize(*)
super
ENV['BUNDLE_GEMFILE'] = File.expand_path(options[:gemfile]) if options[:gemfile]
Bundler::Retry.attempts = options[:retry] || Bundler.settings[:retry] || Bundler::Retry::DEFAULT_ATTEMPTS
Bundler.rubygems.ui = UI::RGProxy.new(Bundler.ui)
rescue UnknownArgumentError => e
raise InvalidOption, e.message
Expand All @@ -30,6 +32,8 @@ def initialize(*)
default_task :install
class_option "no-color", :type => :boolean, :banner => "Disable colorization in output"
class_option "verbose", :type => :boolean, :banner => "Enable verbose output mode", :aliases => "-V"
class_option "retry", :type => :numeric, :aliases => "-r", :banner =>
"Specify the number of times you wish to attempt network commands"

def help(cli = nil)
case cli
Expand Down Expand Up @@ -106,8 +110,6 @@ def init
method_option "dry-run", :type => :boolean, :default => false, :banner =>
"Lock the Gemfile"
def check
ENV['BUNDLE_GEMFILE'] = File.expand_path(options[:gemfile]) if options[:gemfile]

Bundler.settings[:path] = File.expand_path(options[:path]) if options[:path]
begin
definition = Bundler.definition
Expand Down Expand Up @@ -187,8 +189,6 @@ def install
opts[:without] = opts[:without].map{|g| g.tr(' ', ':') }
end

# Can't use Bundler.settings for this because settings needs gemfile.dirname
ENV['BUNDLE_GEMFILE'] = File.expand_path(opts[:gemfile]) if opts[:gemfile]
ENV['RB_USER_INSTALL'] = '1' if Bundler::FREEBSD

# Just disable color in deployment mode
Expand Down
14 changes: 10 additions & 4 deletions lib/bundler/installer.rb
Expand Up @@ -79,11 +79,17 @@ def run(options)
# Since we are installing, we can resolve the definition
# using remote specs
unless local
options["local"] ?
@definition.resolve_with_cache! :
@definition.resolve_remotely!
if options["local"]
@definition.resolve_with_cache!
else
Bundler::Retry.new("source fetch").attempts do
@definition.resolve_remotely!
end
end
end

# Must install gems in the order that the resolver provides
# as dependencies might actually affect the installation of
# the gem.
Installer.post_install_messages = {}

# the order that the resolver provides is significant, since
Expand Down
59 changes: 59 additions & 0 deletions lib/bundler/retry.rb
@@ -0,0 +1,59 @@
module Bundler
# General purpose class for retrying code that may fail
class Retry
DEFAULT_ATTEMPTS = 2
attr_accessor :name, :total_runs, :current_run

class << self
attr_accessor :attempts
end

def initialize(name, attempts = nil)
@name = name
attempts ||= default_attempts
@total_runs = attempts.next # will run once, then upto attempts.times
end

def default_attempts
return Integer(self.class.attempts) if self.class.attempts
DEFAULT_ATTEMPTS
end

def attempt(&block)
@current_run = 0
@failed = false
@error = nil
while keep_trying? do
run(&block)
end
@result
end
alias :attempts :attempt

private
def run(&block)
@failed = false
@current_run += 1
@result = block.call
rescue => e
fail(e)
end

def fail(e)
@failed = true
raise e if last_attempt?
return true unless name
Bundler.ui.warn "Retrying #{name} due to error (#{current_run.next}/#{total_runs}): #{e.message}"
end

def keep_trying?
return true if current_run.zero?
return false if last_attempt?
return true if @failed
end

def last_attempt?
current_run >= total_runs
end
end
end
43 changes: 29 additions & 14 deletions lib/bundler/source/git/git_proxy.rb
@@ -1,7 +1,31 @@
module Bundler
module Source

class Git < Path
class GitNotInstalledError < GitError
def initialize
msg = "You need to install git to be able to use gems from git repositories. "
msg << "For help installing git, please refer to GitHub's tutorial at https://help.github.com/articles/set-up-git"
super msg
end
end

class GitNotAllowedError < GitError
def initialize(command)
msg = "Bundler is trying to run a `git #{command}` at runtime. You probably need to run `bundle install`. However, "
msg << "this error message could probably be more useful. Please submit a ticket at http://github.com/bundler/bundler/issues "
mag << "with steps to reproduce as well as the following\n\nCALLER: #{caller.join("\n")}"
super msg
end
end

class GitCommandError < GitError
def initialize(command, path = nil)
msg = "Git error: command `git #{command}` in directory #{Dir.pwd} has failed."
msg << "\nIf this error persists you could try removing the cache directory '#{path}'" if path && path.exist?
super msg
end
end

# The GitProxy is responsible to iteract with git repositories.
# All actions required by the Git source is encapsualted in this
# object.
Expand Down Expand Up @@ -84,21 +108,12 @@ def git_null(command)
end

def git(command, check_errors=true)
if allow?
raise GitError, "You need to install git to be able to use gems from git repositories. For help installing git, please refer to GitHub's tutorial at https://help.github.com/articles/set-up-git" if !Bundler.git_present?

raise GitNotAllowedError.new(command) unless allow?
raise GitNotInstalledError.new unless Bundler.git_present?
Bundler::Retry.new("git #{command}").attempts do
out = SharedHelpers.with_clean_git_env { %x{git #{command}} }

if check_errors && $?.exitstatus != 0
msg = "Git error: command `git #{command}` in directory #{Dir.pwd} has failed."
msg << "\nIf this error persists you could try removing the cache directory '#{path}'" if path.exist?
raise GitError, msg
end
raise GitCommandError.new(command, path) if check_errors && !$?.success?
out
else
raise GitError, "Bundler is trying to run a `git #{command}` at runtime. You probably need to run `bundle install`. However, " \
"this error message could probably be more useful. Please submit a ticket at http://github.com/bundler/bundler/issues " \
"with steps to reproduce as well as the following\n\nCALLER: #{caller.join("\n")}"
end
end

Expand Down
37 changes: 37 additions & 0 deletions spec/bundler/retry_spec.rb
@@ -0,0 +1,37 @@
require 'spec_helper'

describe "bundle retry" do
it "return successful result if no errors" do
attempts = 0
result = Bundler::Retry.new(nil, 3).attempt do
attempts += 1
:success
end
expect(result).to eq(:success)
expect(attempts).to eq(1)
end

it "returns the first valid result" do
jobs = [->{ raise "foo" }, ->{ :bar }, ->{ raise "foo" }]
attempts = 0
result = Bundler::Retry.new(nil, 3).attempt do
attempts += 1
job = jobs.shift
job.call
end
expect(result).to eq(:bar)
expect(attempts).to eq(2)
end

it "raises the last error" do
error = Bundler::GemfileNotFound
attempts = 0
expect {
Bundler::Retry.new(nil, 3).attempt do
attempts += 1
raise error
end
}.to raise_error(error)
expect(attempts).to eq(4)
end
end
2 changes: 1 addition & 1 deletion spec/install/gems/flex_spec.rb
Expand Up @@ -207,7 +207,7 @@
the gems in your Gemfile, which may resolve the conflict.
E

bundle :install
bundle :install, :retry => 0
expect(out).to eq(nice_error)
end
end
Expand Down
15 changes: 15 additions & 0 deletions spec/install/invalid_spec.rb
Expand Up @@ -33,3 +33,18 @@
expect(out).to match(/invalid symlink/)
end
end

describe "invalid or inaccessible gem source" do
it "can be retried" do
gemfile <<-G
source "file://#{gem_repo_missing}"
gem "rack"
gem "signed_gem"
G
bundle "install", :retry => 2
exp = Regexp.escape("Retrying source fetch due to error (2/3)")
expect(out).to match(exp)
exp = Regexp.escape("Retrying source fetch due to error (3/3)")
expect(out).to match(exp)
end
end
2 changes: 1 addition & 1 deletion spec/runtime/load_spec.rb
Expand Up @@ -32,7 +32,7 @@
expect {
ENV['BUNDLE_GEMFILE'] = ""
Bundler.load
}.not_to raise_error(Bundler::GemfileNotFound)
}.not_to raise_error()
end

end
Expand Down
3 changes: 2 additions & 1 deletion spec/support/helpers.rb
Expand Up @@ -56,7 +56,7 @@ def lib
end

def bundle(cmd, options = {})
expect_err = options.delete(:expect_err)
expect_err = options.delete(:expect_err)
exitstatus = options.delete(:exitstatus)
options["no-color"] = true unless options.key?("no-color") || %w(exec conf).include?(cmd.to_s[0..3])

Expand Down Expand Up @@ -190,6 +190,7 @@ def strip_whitespace(str)
def install_gemfile(*args)
gemfile(*args)
opts = args.last.is_a?(Hash) ? args.last : {}
opts[:retry] ||= 0
bundle :install, opts
end

Expand Down
4 changes: 4 additions & 0 deletions spec/support/path.rb
Expand Up @@ -48,6 +48,10 @@ def gem_repo1(*args)
tmp("gems/remote1", *args)
end

def gem_repo_missing(*args)
tmp("gems/missing", *args)
end

def gem_repo2(*args)
tmp("gems/remote2", *args)
end
Expand Down