Skip to content

Commit bcc4469

Browse files
authored
Merge pull request #9381 from Shopify/ec-split-download-install
Split the download and install process of a gem
2 parents 5d05ee5 + 713dd5c commit bcc4469

File tree

7 files changed

+134
-44
lines changed

7 files changed

+134
-44
lines changed

bundler/lib/bundler/installer/gem_installer.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,20 @@ def install_from_spec
2525
[false, specific_failure_message(e)]
2626
end
2727

28+
def download
29+
spec.source.download(
30+
spec,
31+
force: force,
32+
local: local,
33+
build_args: Array(spec_settings),
34+
previous_spec: previous_spec,
35+
)
36+
37+
[true, nil]
38+
rescue Bundler::BundlerError => e
39+
[false, specific_failure_message(e)]
40+
end
41+
2842
private
2943

3044
def specific_failure_message(e)

bundler/lib/bundler/installer/parallel_installer.rb

Lines changed: 59 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ def ready_to_enqueue?
3232
state == :none
3333
end
3434

35+
def ready_to_install?(installed_specs)
36+
return false unless state == :downloaded
37+
38+
spec.extensions.none? || dependencies_installed?(installed_specs)
39+
end
40+
3541
def has_post_install_message?
3642
!post_install_message.empty?
3743
end
@@ -84,6 +90,7 @@ def initialize(installer, all_specs, size, standalone, force, local: false, skip
8490

8591
def call
8692
if @rake
93+
do_download(@rake, 0)
8794
do_install(@rake, 0)
8895
Gem::Specification.reset
8996
end
@@ -107,26 +114,54 @@ def failed_specs
107114
end
108115

109116
def install_with_worker
110-
enqueue_specs
111-
process_specs until finished_installing?
117+
installed_specs = {}
118+
enqueue_specs(installed_specs)
119+
120+
process_specs(installed_specs) until finished_installing?
112121
end
113122

114123
def install_serially
115124
until finished_installing?
116125
raise "failed to find a spec to enqueue while installing serially" unless spec_install = @specs.find(&:ready_to_enqueue?)
117126
spec_install.state = :enqueued
127+
do_download(spec_install, 0)
118128
do_install(spec_install, 0)
119129
end
120130
end
121131

122132
def worker_pool
123133
@worker_pool ||= Bundler::Worker.new @size, "Parallel Installer", lambda {|spec_install, worker_num|
124-
do_install(spec_install, worker_num)
134+
case spec_install.state
135+
when :enqueued
136+
do_download(spec_install, worker_num)
137+
when :installable
138+
do_install(spec_install, worker_num)
139+
else
140+
spec_install
141+
end
125142
}
126143
end
127144

128-
def do_install(spec_install, worker_num)
145+
def do_download(spec_install, worker_num)
129146
Plugin.hook(Plugin::Events::GEM_BEFORE_INSTALL, spec_install)
147+
148+
gem_installer = Bundler::GemInstaller.new(
149+
spec_install.spec, @installer, @standalone, worker_num, @force, @local
150+
)
151+
152+
success, message = gem_installer.download
153+
154+
if success
155+
spec_install.state = :downloaded
156+
else
157+
spec_install.error = "#{message}\n\n#{require_tree_for_spec(spec_install.spec)}"
158+
spec_install.state = :failed
159+
end
160+
161+
spec_install
162+
end
163+
164+
def do_install(spec_install, worker_num)
130165
gem_installer = Bundler::GemInstaller.new(
131166
spec_install.spec, @installer, @standalone, worker_num, @force, @local
132167
)
@@ -147,9 +182,19 @@ def do_install(spec_install, worker_num)
147182
# Some specs might've had to wait til this spec was installed to be
148183
# processed so the call to `enqueue_specs` is important after every
149184
# dequeue.
150-
def process_specs
151-
worker_pool.deq
152-
enqueue_specs
185+
def process_specs(installed_specs)
186+
spec = worker_pool.deq
187+
188+
if spec.installed?
189+
installed_specs[spec.name] = true
190+
return
191+
elsif spec.failed?
192+
return
193+
elsif spec.ready_to_install?(installed_specs)
194+
spec.state = :installable
195+
end
196+
197+
worker_pool.enq(spec)
153198
end
154199

155200
def finished_installing?
@@ -185,18 +230,15 @@ def require_tree_for_spec(spec)
185230
# Later we call this lambda again to install specs that depended on
186231
# previously installed specifications. We continue until all specs
187232
# are installed.
188-
def enqueue_specs
189-
installed_specs = {}
190-
@specs.each do |spec|
191-
next unless spec.installed?
192-
installed_specs[spec.name] = true
193-
end
194-
233+
def enqueue_specs(installed_specs)
195234
@specs.each do |spec|
196-
if spec.ready_to_enqueue? && spec.dependencies_installed?(installed_specs)
197-
spec.state = :enqueued
198-
worker_pool.enq spec
235+
if spec.installed?
236+
installed_specs[spec.name] = true
237+
next
199238
end
239+
240+
spec.state = :enqueued
241+
worker_pool.enq spec
200242
end
201243
end
202244
end

bundler/lib/bundler/plugin/api/source.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,14 @@ def options_to_lock
7474
{}
7575
end
7676

77+
# Download the gem specified by the spec at appropriate path.
78+
#
79+
# A source plugin can implement this method to split the download and the
80+
# installation of a gem.
81+
#
82+
# @return [Boolean] Whether the download of the gem succeeded.
83+
def download(spec, opts); end
84+
7785
# Install the gem specified by the spec at appropriate path.
7886
# `install_path` provides a sufficient default, if the source can only
7987
# satisfy one gem, but is not binding.

bundler/lib/bundler/plugin/installer.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,8 @@ def install_from_specs(specs)
110110
paths = {}
111111

112112
specs.each do |spec|
113-
spec.source.install spec
113+
spec.source.download(spec)
114+
spec.source.install(spec)
114115

115116
paths[spec.name] = spec
116117
end

bundler/lib/bundler/self_manager.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ def install_and_restart_with(version)
6363
end
6464

6565
def install(spec)
66+
spec.source.download(spec)
6667
spec.source.install(spec)
6768
end
6869

bundler/lib/bundler/source.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ def version_message(spec, locked_spec = nil)
3131
message
3232
end
3333

34+
def download(*); end
35+
3436
def can_lock?(spec)
3537
spec.source == self
3638
end

bundler/lib/bundler/source/rubygems.rb

Lines changed: 48 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ class Rubygems < Source
99

1010
# Ask for X gems per API request
1111
API_REQUEST_SIZE = 100
12+
REQUIRE_MUTEX = Mutex.new
1213

1314
attr_accessor :remotes
1415

@@ -21,6 +22,8 @@ def initialize(options = {})
2122
@allow_local = options["allow_local"] || false
2223
@prefer_local = false
2324
@checksum_store = Checksum::Store.new
25+
@gem_installers = {}
26+
@gem_installers_mutex = Mutex.new
2427

2528
Array(options["remotes"]).reverse_each {|r| add_remote(r) }
2629

@@ -162,49 +165,40 @@ def specs
162165
end
163166
end
164167

165-
def install(spec, options = {})
168+
def download(spec, options = {})
166169
if (spec.default_gem? && !cached_built_in_gem(spec, local: options[:local])) || (installed?(spec) && !options[:force])
167-
print_using_message "Using #{version_message(spec, options[:previous_spec])}"
168-
return nil # no post-install message
170+
return true
169171
end
170172

171-
path = fetch_gem_if_possible(spec, options[:previous_spec])
172-
raise GemNotFound, "Could not find #{spec.file_name} for installation" unless path
173-
174-
return if Bundler.settings[:no_install]
175-
176-
install_path = rubygems_dir
177-
bin_path = Bundler.system_bindir
178-
179-
require_relative "../rubygems_gem_installer"
180-
181-
installer = Bundler::RubyGemsGemInstaller.at(
182-
path,
183-
security_policy: Bundler.rubygems.security_policies[Bundler.settings["trust-policy"]],
184-
install_dir: install_path.to_s,
185-
bin_dir: bin_path.to_s,
186-
ignore_dependencies: true,
187-
wrappers: true,
188-
env_shebang: true,
189-
build_args: options[:build_args],
190-
bundler_extension_cache_path: extension_cache_path(spec)
191-
)
173+
installer = rubygems_gem_installer(spec, options)
192174

193175
if spec.remote
194176
s = begin
195177
installer.spec
196178
rescue Gem::Package::FormatError
197-
Bundler.rm_rf(path)
179+
Bundler.rm_rf(installer.gem)
198180
raise
199181
rescue Gem::Security::Exception => e
200182
raise SecurityError,
201-
"The gem #{File.basename(path, ".gem")} can't be installed because " \
183+
"The gem #{installer.gem} can't be installed because " \
202184
"the security policy didn't allow it, with the message: #{e.message}"
203185
end
204186

205187
spec.__swap__(s)
206188
end
207189

190+
spec
191+
end
192+
193+
def install(spec, options = {})
194+
if (spec.default_gem? && !cached_built_in_gem(spec, local: options[:local])) || (installed?(spec) && !options[:force])
195+
print_using_message "Using #{version_message(spec, options[:previous_spec])}"
196+
return nil # no post-install message
197+
end
198+
199+
return if Bundler.settings[:no_install]
200+
201+
installer = rubygems_gem_installer(spec, options)
208202
spec.source.checksum_store.register(spec, installer.gem_checksum)
209203

210204
message = "Installing #{version_message(spec, options[:previous_spec])}"
@@ -516,6 +510,34 @@ def extension_cache_slug(spec)
516510
return unless remote = spec.remote
517511
remote.cache_slug
518512
end
513+
514+
# We are using a mutex to reaed and write from/to the hash.
515+
# The reason this double synchronization was added is for performance
516+
# and lock the mutex for the shortest possible amount of time. Otherwise,
517+
# all threads are fighting over this mutex and when it gets acquired it gets locked
518+
# until a threads finishes downloading a gem, leaving the other threads waiting
519+
# doing nothing.
520+
def rubygems_gem_installer(spec, options)
521+
@gem_installers_mutex.synchronize { @gem_installers[spec.name] } || begin
522+
path = fetch_gem_if_possible(spec, options[:previous_spec])
523+
raise GemNotFound, "Could not find #{spec.file_name} for installation" unless path
524+
525+
REQUIRE_MUTEX.synchronize { require_relative "../rubygems_gem_installer" }
526+
527+
installer = Bundler::RubyGemsGemInstaller.at(
528+
path,
529+
security_policy: Bundler.rubygems.security_policies[Bundler.settings["trust-policy"]],
530+
install_dir: rubygems_dir.to_s,
531+
bin_dir: Bundler.system_bindir.to_s,
532+
ignore_dependencies: true,
533+
wrappers: true,
534+
env_shebang: true,
535+
build_args: options[:build_args],
536+
bundler_extension_cache_path: extension_cache_path(spec)
537+
)
538+
@gem_installers_mutex.synchronize { @gem_installers[spec.name] ||= installer }
539+
end
540+
end
519541
end
520542
end
521543
end

0 commit comments

Comments
 (0)