Skip to content

Commit 8052c40

Browse files
authored
Merge pull request #9310 from ngan/fix-stale-cache-after-process-lock
Clear gem specification cache after acquiring process lock
2 parents 103ca42 + ab41874 commit 8052c40

6 files changed

Lines changed: 121 additions & 0 deletions

File tree

bundler/lib/bundler/installer.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,11 @@ def run(options)
6363
Bundler.create_bundle_path
6464

6565
ProcessLock.lock do
66+
# Invalidate any stale gem specification cache from before we acquired the lock.
67+
# Another process may have installed gems while we were waiting.
68+
Gem::Specification.reset
69+
@definition.sources.clear_cache
70+
6671
@definition.ensure_equivalent_gemfile_and_lockfile(options[:deployment])
6772

6873
if @definition.dependencies.empty?

bundler/lib/bundler/source/rubygems.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,13 @@ def dependency_api_available?
314314
@allow_remote && api_fetchers.any?
315315
end
316316

317+
def clear_cache
318+
@specs = nil
319+
@installed_specs = nil
320+
@default_specs = nil
321+
@cached_specs = nil
322+
end
323+
317324
protected
318325

319326
def remote_names

bundler/lib/bundler/source_list.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,10 @@ def remote!
136136
all_sources.each(&:remote!)
137137
end
138138

139+
def clear_cache
140+
rubygems_sources.each(&:clear_cache)
141+
end
142+
139143
private
140144

141145
def map_sources(replacement_sources)

spec/bundler/source/rubygems_spec.rb

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,45 @@
4545
end
4646
end
4747

48+
describe "#clear_cache" do
49+
it "invalidates memoized indexes so subsequent reads rebuild them" do
50+
source = described_class.new
51+
52+
first_specs = source.specs
53+
first_installed = source.send(:installed_specs)
54+
first_default = source.send(:default_specs)
55+
first_cached = source.send(:cached_specs)
56+
57+
expect(source.specs).to equal(first_specs)
58+
expect(source.send(:installed_specs)).to equal(first_installed)
59+
expect(source.send(:default_specs)).to equal(first_default)
60+
expect(source.send(:cached_specs)).to equal(first_cached)
61+
62+
source.clear_cache
63+
64+
expect(source.specs).not_to equal(first_specs)
65+
expect(source.send(:installed_specs)).not_to equal(first_installed)
66+
expect(source.send(:default_specs)).not_to equal(first_default)
67+
expect(source.send(:cached_specs)).not_to equal(first_cached)
68+
end
69+
70+
it "reflects newly-discovered installed gems after clear_cache" do
71+
source = described_class.new
72+
foo = Gem::Specification.new("foo", "1.0.0")
73+
bar = Gem::Specification.new("bar", "1.0.0")
74+
75+
allow(Bundler.rubygems).to receive(:installed_specs).and_return([foo])
76+
expect(source.send(:installed_specs).search("bar")).to be_empty
77+
78+
allow(Bundler.rubygems).to receive(:installed_specs).and_return([foo, bar])
79+
expect(source.send(:installed_specs).search("bar")).to be_empty
80+
81+
source.clear_cache
82+
83+
expect(source.send(:installed_specs).search("bar")).not_to be_empty
84+
end
85+
end
86+
4887
describe "log debug information" do
4988
it "log the time spent downloading and installing a gem" do
5089
build_repo2 do

spec/bundler/source_list_spec.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,16 @@
442442
end
443443
end
444444

445+
describe "#clear_cache" do
446+
let(:rubygems_source) { source_list.add_rubygems_source("remotes" => ["https://rubygems.org"]) }
447+
448+
it "calls #clear_cache on all rubygems sources" do
449+
expect(rubygems_source).to receive(:clear_cache)
450+
expect(source_list.global_rubygems_source).to receive(:clear_cache)
451+
source_list.clear_cache
452+
end
453+
end
454+
445455
describe "implicit_global_source?" do
446456
context "when a global rubygem source provided" do
447457
it "returns a falsy value" do

spec/install/process_lock_spec.rb

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,5 +53,61 @@
5353
expect(processed).to eq true
5454
end
5555
end
56+
57+
it "refreshes gem specification cache after waiting for lock" do
58+
build_repo2 do
59+
build_gem "myrack", "1.0.0"
60+
end
61+
62+
gemfile <<-G
63+
source "https://gem.repo2"
64+
gem "myrack"
65+
G
66+
67+
# First, install the gem so it's available
68+
bundle "install"
69+
expect(out).to include("Installing myrack")
70+
71+
# Queue for thread-safe communication
72+
lock_acquired = Queue.new
73+
can_release_lock = Queue.new
74+
install_output = Queue.new
75+
76+
# Thread holds lock (simulating another bundle process that just finished installing)
77+
thread = Thread.new do
78+
Bundler::ProcessLock.lock(default_bundle_path) do
79+
# Signal that we have the lock
80+
lock_acquired << true
81+
# Wait until main thread signals we can release
82+
can_release_lock.pop
83+
end
84+
end
85+
86+
# Wait for thread to acquire lock
87+
lock_acquired.pop
88+
89+
# Start another install in a thread - it will wait for the lock
90+
install_thread = Thread.new do
91+
bundle "install", verbose: true
92+
install_output << out
93+
end
94+
95+
# Give subprocess time to start and begin waiting for lock
96+
sleep 0.5
97+
98+
# Signal thread to release the lock
99+
can_release_lock << true
100+
101+
# Wait for both threads to complete
102+
thread.join
103+
install_thread.join
104+
105+
second_install_out = install_output.pop
106+
107+
expect(the_bundle).to include_gems "myrack 1.0.0"
108+
# The second install should have refreshed its cache after acquiring
109+
# the lock and seen that myrack was already installed
110+
expect(second_install_out).to include("Using myrack")
111+
end
56112
end
57113
end

0 commit comments

Comments
 (0)