diff --git a/bundler/lib/bundler/installer.rb b/bundler/lib/bundler/installer.rb index c5fd75431f41..20948fcbce4e 100644 --- a/bundler/lib/bundler/installer.rb +++ b/bundler/lib/bundler/installer.rb @@ -63,6 +63,11 @@ def run(options) Bundler.create_bundle_path ProcessLock.lock do + # Invalidate any stale gem specification cache from before we acquired the lock. + # Another process may have installed gems while we were waiting. + Gem::Specification.reset + @definition.sources.clear_cache + @definition.ensure_equivalent_gemfile_and_lockfile(options[:deployment]) if @definition.dependencies.empty? diff --git a/bundler/lib/bundler/source/rubygems.rb b/bundler/lib/bundler/source/rubygems.rb index e1e030ffc899..7a020fc1960d 100644 --- a/bundler/lib/bundler/source/rubygems.rb +++ b/bundler/lib/bundler/source/rubygems.rb @@ -320,6 +320,11 @@ def dependency_api_available? @allow_remote && api_fetchers.any? end + def clear_cache + @installed_specs = nil + @default_specs = nil + end + protected def remote_names diff --git a/bundler/lib/bundler/source_list.rb b/bundler/lib/bundler/source_list.rb index 38fa0972e64e..ac141299e5d0 100644 --- a/bundler/lib/bundler/source_list.rb +++ b/bundler/lib/bundler/source_list.rb @@ -136,6 +136,10 @@ def remote! all_sources.each(&:remote!) end + def clear_cache + rubygems_sources.each(&:clear_cache) + end + private def map_sources(replacement_sources) diff --git a/bundler/spec/bundler/source/rubygems_spec.rb b/bundler/spec/bundler/source/rubygems_spec.rb index dde4e4ed4769..516a85fafa1a 100644 --- a/bundler/spec/bundler/source/rubygems_spec.rb +++ b/bundler/spec/bundler/source/rubygems_spec.rb @@ -45,6 +45,36 @@ end end + describe "#clear_cache" do + it "clears the installed_specs cache" do + source = described_class.new + + # Access installed_specs to populate the cache + source.send(:installed_specs) + expect(source.instance_variable_get(:@installed_specs)).not_to be_nil + + # Expire the cache + source.clear_cache + + # Cache should be cleared + expect(source.instance_variable_get(:@installed_specs)).to be_nil + end + + it "clears the default_specs cache" do + source = described_class.new + + # Access default_specs to populate the cache + source.send(:default_specs) + expect(source.instance_variable_get(:@default_specs)).not_to be_nil + + # Expire the cache + source.clear_cache + + # Cache should be cleared + expect(source.instance_variable_get(:@default_specs)).to be_nil + end + end + describe "log debug information" do it "log the time spent downloading and installing a gem" do build_repo2 do diff --git a/bundler/spec/bundler/source_list_spec.rb b/bundler/spec/bundler/source_list_spec.rb index 6e0be6c92fcc..3ed58b867d96 100644 --- a/bundler/spec/bundler/source_list_spec.rb +++ b/bundler/spec/bundler/source_list_spec.rb @@ -442,6 +442,16 @@ end end + describe "#clear_cache" do + let(:rubygems_source) { source_list.add_rubygems_source("remotes" => ["https://rubygems.org"]) } + + it "calls #clear_cache on all rubygems sources" do + expect(rubygems_source).to receive(:clear_cache) + expect(source_list.global_rubygems_source).to receive(:clear_cache) + source_list.clear_cache + end + end + describe "implicit_global_source?" do context "when a global rubygem source provided" do it "returns a falsy value" do diff --git a/bundler/spec/install/process_lock_spec.rb b/bundler/spec/install/process_lock_spec.rb index 344caa3a9312..b096291d1a92 100644 --- a/bundler/spec/install/process_lock_spec.rb +++ b/bundler/spec/install/process_lock_spec.rb @@ -53,5 +53,61 @@ expect(processed).to eq true end end + + it "refreshes gem specification cache after waiting for lock" do + build_repo2 do + build_gem "myrack", "1.0.0" + end + + gemfile <<-G + source "https://gem.repo2" + gem "myrack" + G + + # First, install the gem so it's available + bundle "install" + expect(out).to include("Installing myrack") + + # Queue for thread-safe communication + lock_acquired = Queue.new + can_release_lock = Queue.new + install_output = Queue.new + + # Thread holds lock (simulating another bundle process that just finished installing) + thread = Thread.new do + Bundler::ProcessLock.lock(default_bundle_path) do + # Signal that we have the lock + lock_acquired << true + # Wait until main thread signals we can release + can_release_lock.pop + end + end + + # Wait for thread to acquire lock + lock_acquired.pop + + # Start another install in a thread - it will wait for the lock + install_thread = Thread.new do + bundle "install", verbose: true + install_output << out + end + + # Give subprocess time to start and begin waiting for lock + sleep 0.5 + + # Signal thread to release the lock + can_release_lock << true + + # Wait for both threads to complete + thread.join + install_thread.join + + second_install_out = install_output.pop + + expect(the_bundle).to include_gems "myrack 1.0.0" + # The second install should have refreshed its cache after acquiring + # the lock and seen that myrack was already installed + expect(second_install_out).to include("Using myrack") + end end end