diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a29ac9c..ba35ccac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Unreleased +* Automatically invalidate the load path cache whenever the Ruby version change. (#387) + This is to avoid issues in case the same installation path is re-used for subsequent ruby patch releases. + # 1.9.3 * Only disable the compile cache for source files impacted by [Ruby 3.0.3 [Bug 18250]](https://bugs.ruby-lang.org/issues/18250). diff --git a/lib/bootsnap/load_path_cache/store.rb b/lib/bootsnap/load_path_cache/store.rb index 5f5e9fa2..071ca4f4 100644 --- a/lib/bootsnap/load_path_cache/store.rb +++ b/lib/bootsnap/load_path_cache/store.rb @@ -7,6 +7,9 @@ module Bootsnap module LoadPathCache class Store + VERSION_KEY = '__bootsnap_ruby_version__' + CURRENT_VERSION = "#{RUBY_REVISION}-#{RUBY_PLATFORM}".freeze + NestedTransactionError = Class.new(StandardError) SetOutsideTransactionNotAllowed = Class.new(StandardError) @@ -62,15 +65,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] == CURRENT_VERSION + 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 @@ -93,6 +101,10 @@ def dump_data retry rescue SystemCallError end + + def default_data + { VERSION_KEY => CURRENT_VERSION } + end end end end diff --git a/test/load_path_cache/store_test.rb b/test/load_path_cache/store_test.rb index 74e23544..4fbce665 100644 --- a/test/load_path_cache/store_test.rb +++ b/test/load_path_cache/store_test.rb @@ -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(Store, :CURRENT_VERSION, "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