-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Split the download and install process of a gem #9381
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,6 +9,7 @@ class Rubygems < Source | |
|
|
||
| # Ask for X gems per API request | ||
| API_REQUEST_SIZE = 100 | ||
| REQUIRE_MUTEX = Mutex.new | ||
|
|
||
| attr_accessor :remotes | ||
|
|
||
|
|
@@ -21,6 +22,8 @@ def initialize(options = {}) | |
| @allow_local = options["allow_local"] || false | ||
| @prefer_local = false | ||
| @checksum_store = Checksum::Store.new | ||
| @gem_installers = {} | ||
| @gem_installers_mutex = Mutex.new | ||
|
|
||
| Array(options["remotes"]).reverse_each {|r| add_remote(r) } | ||
|
|
||
|
|
@@ -162,49 +165,40 @@ def specs | |
| end | ||
| end | ||
|
|
||
| def install(spec, options = {}) | ||
| def download(spec, options = {}) | ||
| if (spec.default_gem? && !cached_built_in_gem(spec, local: options[:local])) || (installed?(spec) && !options[:force]) | ||
| print_using_message "Using #{version_message(spec, options[:previous_spec])}" | ||
| return nil # no post-install message | ||
| return true | ||
| end | ||
|
|
||
| path = fetch_gem_if_possible(spec, options[:previous_spec]) | ||
| raise GemNotFound, "Could not find #{spec.file_name} for installation" unless path | ||
|
|
||
| return if Bundler.settings[:no_install] | ||
|
|
||
| install_path = rubygems_dir | ||
| bin_path = Bundler.system_bindir | ||
|
|
||
| require_relative "../rubygems_gem_installer" | ||
|
|
||
| installer = Bundler::RubyGemsGemInstaller.at( | ||
| path, | ||
| security_policy: Bundler.rubygems.security_policies[Bundler.settings["trust-policy"]], | ||
| install_dir: install_path.to_s, | ||
| bin_dir: bin_path.to_s, | ||
| ignore_dependencies: true, | ||
| wrappers: true, | ||
| env_shebang: true, | ||
| build_args: options[:build_args], | ||
| bundler_extension_cache_path: extension_cache_path(spec) | ||
| ) | ||
| installer = rubygems_gem_installer(spec, options) | ||
|
|
||
| if spec.remote | ||
| s = begin | ||
| installer.spec | ||
| rescue Gem::Package::FormatError | ||
| Bundler.rm_rf(path) | ||
| Bundler.rm_rf(installer.gem) | ||
| raise | ||
| rescue Gem::Security::Exception => e | ||
| raise SecurityError, | ||
| "The gem #{File.basename(path, ".gem")} can't be installed because " \ | ||
| "The gem #{installer.gem} can't be installed because " \ | ||
| "the security policy didn't allow it, with the message: #{e.message}" | ||
| end | ||
|
|
||
| spec.__swap__(s) | ||
| end | ||
|
|
||
| spec | ||
| end | ||
|
|
||
| def install(spec, options = {}) | ||
| if (spec.default_gem? && !cached_built_in_gem(spec, local: options[:local])) || (installed?(spec) && !options[:force]) | ||
| print_using_message "Using #{version_message(spec, options[:previous_spec])}" | ||
| return nil # no post-install message | ||
| end | ||
|
|
||
| return if Bundler.settings[:no_install] | ||
|
|
||
| installer = rubygems_gem_installer(spec, options) | ||
| spec.source.checksum_store.register(spec, installer.gem_checksum) | ||
|
|
||
| message = "Installing #{version_message(spec, options[:previous_spec])}" | ||
|
|
@@ -516,6 +510,34 @@ def extension_cache_slug(spec) | |
| return unless remote = spec.remote | ||
| remote.cache_slug | ||
| end | ||
|
|
||
| # We are using a mutex to reaed and write from/to the hash. | ||
| # The reason this double synchronization was added is for performance | ||
| # and lock the mutex for the shortest possible amount of time. Otherwise, | ||
| # all threads are fighting over this mutex and when it gets acquired it gets locked | ||
| # until a threads finishes downloading a gem, leaving the other threads waiting | ||
| # doing nothing. | ||
| def rubygems_gem_installer(spec, options) | ||
| @gem_installers_mutex.synchronize { @gem_installers[spec.name] } || begin | ||
| path = fetch_gem_if_possible(spec, options[:previous_spec]) | ||
| raise GemNotFound, "Could not find #{spec.file_name} for installation" unless path | ||
|
|
||
| REQUIRE_MUTEX.synchronize { require_relative "../rubygems_gem_installer" } | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we require
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unfortunately, I don't think I can. I tried in https://github.com/ruby/rubygems/actions/runs/22919157297 but too many failures. The This Ruby 3.2 bug is really annoying, especially for Bundler/RG, because this codebase has to
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, I understand. |
||
|
|
||
| installer = Bundler::RubyGemsGemInstaller.at( | ||
| path, | ||
| security_policy: Bundler.rubygems.security_policies[Bundler.settings["trust-policy"]], | ||
| install_dir: rubygems_dir.to_s, | ||
| bin_dir: Bundler.system_bindir.to_s, | ||
| ignore_dependencies: true, | ||
| wrappers: true, | ||
| env_shebang: true, | ||
| build_args: options[:build_args], | ||
| bundler_extension_cache_path: extension_cache_path(spec) | ||
| ) | ||
| @gem_installers_mutex.synchronize { @gem_installers[spec.name] ||= installer } | ||
| end | ||
| end | ||
|
Comment on lines
+521
to
+540
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why @gem_installers_mutex.synchronize do
@gem_installers[spec.name] ||= begin
...
end
endI think that the latter is better. If we use the former,
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I opted to go with this because I needed to lock the mutex for the shortest amount of time. I originally used your suggestion but saw a net speed regression because the mutex was locked by a thread until it finished downloading a gem.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see. How about adding a comment why we don't use the standard
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's a good idea, thanks for the suggestion I added it ! |
||
| end | ||
| end | ||
| end | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Bundler::RubyGemsGemInstaller object is memoized so that the
installpart doesn't have to re-instantiate one. The reason is because creating a new Bundler::RubyGemsGemInstaller is expensive due to the underlingPackagethat reads the tarball and get the metadata file (as well as computing the gem digest).rubygems/lib/rubygems/package.rb
Lines 606 to 607 in 381c4e3