summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndré Arko <mail@arko.net>2015-04-19 17:27:55 -0700
committerAndré Arko <mail@arko.net>2015-04-19 17:27:55 -0700
commitaffcaefa8898503cb17ac1d327f2b588925e27d5 (patch)
tree080ee05adfda0f8095b5a3068f60980343cf37a8
parent182372b565f6d3a9fb4a8f8cc9e6cff81f1ee793 (diff)
parent38b900bd45e60c35813904409c4478baf0de5a95 (diff)
downloadbundler-affcaefa8898503cb17ac1d327f2b588925e27d5.tar.gz
Merge pull request #3577 from pducks32/master
refactor parallel installer
-rw-r--r--lib/bundler/installer.rb44
-rw-r--r--lib/bundler/installer/parallel_installer.rb117
-rw-r--r--spec/install/parallel/spec_installation_spec.rb37
3 files changed, 156 insertions, 42 deletions
diff --git a/lib/bundler/installer.rb b/lib/bundler/installer.rb
index 7365b8f27e..68fd5cad75 100644
--- a/lib/bundler/installer.rb
+++ b/lib/bundler/installer.rb
@@ -87,6 +87,7 @@ module Bundler
# installation is just SO MUCH FASTER. so we let people opt in.
jobs = [Bundler.settings[:jobs].to_i-1, 1].max
if jobs > 1 && can_install_in_parallel?
+ require 'bundler/installer/parallel_installer'
install_in_parallel jobs, options[:standalone], force
else
install_sequentially options[:standalone], force
@@ -277,48 +278,7 @@ module Bundler
end
def install_in_parallel(size, standalone, force = false)
- name2spec = {}
- remains = {}
- enqueued = {}
- specs.each do |spec|
- name2spec[spec.name] = spec
- remains[spec.name] = true
- end
-
- worker_pool = Worker.new size, lambda { |name, worker_num|
- spec = name2spec[name]
- message = install_gem_from_spec spec, standalone, worker_num, force
- { :name => spec.name, :post_install => message }
- }
-
- # Keys in the remains hash represent uninstalled gems specs.
- # We enqueue all gem specs that do not have any dependencies.
- # Later we call this lambda again to install specs that depended on
- # previously installed specifications. We continue until all specs
- # are installed.
- enqueue_remaining_specs = lambda do
- remains.keys.each do |name|
- next if enqueued[name]
- spec = name2spec[name]
- if ready_to_install?(spec, remains)
- worker_pool.enq name
- enqueued[name] = true
- end
- end
- end
- enqueue_remaining_specs.call
-
- until remains.empty?
- message = worker_pool.deq
- remains.delete message[:name]
- if message[:post_install]
- Installer.post_install_messages[message[:name]] = message[:post_install]
- end
- enqueue_remaining_specs.call
- end
- message
- ensure
- worker_pool && worker_pool.stop
+ ParallelInstaller.call(self, specs, size, standalone, force)
end
# We only want to install a gem spec if all its dependencies are met.
diff --git a/lib/bundler/installer/parallel_installer.rb b/lib/bundler/installer/parallel_installer.rb
new file mode 100644
index 0000000000..d4a9f7c253
--- /dev/null
+++ b/lib/bundler/installer/parallel_installer.rb
@@ -0,0 +1,117 @@
+require 'bundler/worker'
+
+
+class ParallelInstaller
+
+ class SpecInstallation
+
+ attr_accessor :spec, :name, :post_install_message, :state
+ def initialize(spec)
+ @spec, @name = spec, spec.name
+ @state = :none
+ @post_install_message = ""
+ end
+
+ def installed?
+ state == :installed
+ end
+
+ def enqueued?
+ state == :enqueued
+ end
+
+ # Only true when spec in neither installed nor already enqueued
+ def ready_to_enqueue?
+ !installed? && !enqueued?
+ end
+
+ def has_post_install_message?
+ post_install_message.empty?
+ end
+
+ def ignorable_dependency?(dep)
+ dep.type == :development || dep.name == @name
+ end
+
+ # Checks installed dependencies against spec's dependencies to make
+ # sure needed dependencies have been installed.
+ def dependencies_installed?(remaining_specs)
+ installed_specs = remaining_specs.reject(&:installed?).map(&:name)
+ already_installed = lambda {|dep| installed_specs.include? dep.name }
+ dependencies.all? {|d| already_installed[d] }
+ end
+
+ # Represents only the non-development dependencies and the ones that
+ # are itself.
+ def dependencies
+ @dependencies ||= all_dependencies.reject {|dep| ignorable_dependency? dep }
+ end
+
+ # Represents all dependencies
+ def all_dependencies
+ @spec.dependencies
+ end
+ end
+
+ def self.call(*args)
+ new(*args).call
+ end
+
+ # Returns max number of threads machine can handle with a min of 1
+ def self.max_threads
+ [Bundler.settings[:jobs].to_i-1, 1].max
+ end
+
+ def initialize(installer, all_specs, size, standalone, force)
+ @installer = installer
+ @size = size
+ @standalone = standalone
+ @force = force
+ @specs = all_specs.map { |s| SpecInstallation.new(s) }
+ end
+
+ def call
+ enqueue_specs
+ process_specs until @specs.all?(&:installed?)
+ ensure
+ worker_pool && worker_pool.stop
+ end
+
+ def worker_pool
+ @worker_pool ||= Bundler::Worker.new @size, lambda { |spec_install, worker_num|
+ message = @installer.install_gem_from_spec spec_install.spec, @standalone, worker_num, @force
+ spec_install.post_install_message = message unless message.nil?
+ spec_install
+ }
+ end
+
+ # Dequeue a spec and save its post-install message anf then enqueue the
+ # remaining specs.
+ # Some specs might've had to wait til this spec was installed to be
+ # processed so the call to `enqueue_specs` is important after every
+ # dequeue.
+ def process_specs
+ spec = worker_pool.deq
+ spec.state = :installed
+ collect_post_install_message spec if spec.has_post_install_message?
+ enqueue_specs
+ end
+
+ def collect_post_install_message(spec)
+ Bundler::Installer.post_install_messages[spec.name] = spec.post_install_message
+ end
+
+ # Keys in the remains hash represent uninstalled gems specs.
+ # We enqueue all gem specs that do not have any dependencies.
+ # Later we call this lambda again to install specs that depended on
+ # previously installed specifications. We continue until all specs
+ # are installed.
+ def enqueue_specs
+ @specs.select(&:ready_to_enqueue?).each do |spec|
+ if spec.dependencies_installed? @specs
+ worker_pool.enq spec
+ spec.state = :enqueued
+ end
+ end
+ end
+end
diff --git a/spec/install/parallel/spec_installation_spec.rb b/spec/install/parallel/spec_installation_spec.rb
new file mode 100644
index 0000000000..97288fe02f
--- /dev/null
+++ b/spec/install/parallel/spec_installation_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+require 'bundler/installer/parallel_installer'
+
+describe ParallelInstaller::SpecInstallation do
+ describe "#ready_to_enqueue?" do
+
+ let!(:dep) do
+ a_spec = Object.new
+ def a_spec.name
+ "I like tests"
+ end
+ a_spec
+ end
+
+ context "when in enqueued state" do
+ it "is falsey" do
+ spec = ParallelInstaller::SpecInstallation.new(dep)
+ spec.state = :enqueued
+ expect(spec.ready_to_enqueue?).to be_falsey
+ end
+ end
+
+ context "when in installed state" do
+ it "returns falsey" do
+ spec = ParallelInstaller::SpecInstallation.new(dep)
+ spec.state = :installed
+ expect(spec.ready_to_enqueue?).to be_falsey
+ end
+ end
+
+ it "returns truthy" do
+ spec = ParallelInstaller::SpecInstallation.new(dep)
+ expect(spec.ready_to_enqueue?).to be_truthy
+ end
+ end
+
+end