Skip to content

Commit

Permalink
Ignore load path caches generated by a different ruby version
Browse files Browse the repository at this point in the history
Fix: #384

Until now it was assume that if the `$LOAD_PATH` was identical,
then the content of the paths would be too.

However from one minor ruby version to another, the layout of the
stdlib can change. So if the newer version is installed in the same
place than the previous one, it might cause the load path cache to
be invalid.

So we store `RUBY_DESCRIPTION` in the cache and make sure it matches
before reusing the cache.
  • Loading branch information
byroot committed Jan 10, 2022
1 parent a0aa3d2 commit 784ae45
Show file tree
Hide file tree
Showing 2 changed files with 37 additions and 3 deletions.
16 changes: 13 additions & 3 deletions lib/bootsnap/load_path_cache/store.rb
Expand Up @@ -7,6 +7,7 @@
module Bootsnap
module LoadPathCache
class Store
VERSION_KEY = '__bootsnap_ruby_version__'
NestedTransactionError = Class.new(StandardError)
SetOutsideTransactionNotAllowed = Class.new(StandardError)

Expand Down Expand Up @@ -62,15 +63,20 @@ def commit_transaction

def load_data
@data = begin
File.open(@store_path, encoding: Encoding::BINARY) do |io|
data = File.open(@store_path, encoding: Encoding::BINARY) do |io|
MessagePack.load(io)
end
if data.is_a?(Hash) && data[VERSION_KEY] == RUBY_DESCRIPTION
data
else
default_data
end
# handle malformed data due to upgrade incompatibility
rescue Errno::ENOENT, MessagePack::MalformedFormatError, MessagePack::UnknownExtTypeError, EOFError
{}
default_data
rescue ArgumentError => error
if error.message =~ /negative array size/
{}
default_data
else
raise
end
Expand All @@ -93,6 +99,10 @@ def dump_data
retry
rescue SystemCallError
end

def default_data
{ VERSION_KEY => RUBY_DESCRIPTION }
end
end
end
end
24 changes: 24 additions & 0 deletions test/load_path_cache/store_test.rb
Expand Up @@ -80,6 +80,30 @@ def test_ignore_read_only_filesystem
store.transaction { store.set('a', 1) }
refute(File.exist?(@path))
end

def test_bust_cache_on_ruby_change
store.transaction { store.set('a', 'b') }

assert_equal 'b', Store.new(@path).get('a')

stub_const(Object, :RUBY_DESCRIPTION, "foobar") do
assert_nil Store.new(@path).get('a')
end
end

private

def stub_const(owner, const_name, stub_value)
original_value = owner.const_get(const_name)
owner.send(:remove_const, const_name)
owner.const_set(const_name, stub_value)
begin
yield
ensure
owner.send(:remove_const, const_name)
owner.const_set(const_name, original_value)
end
end
end
end
end

0 comments on commit 784ae45

Please sign in to comment.