From 8726b29f7c75705671d7c061fc7582d9b35eeae7 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Mon, 10 Jan 2022 12:11:51 +0100 Subject: [PATCH] Ignore load path caches generated by a different ruby version Fix: https://github.com/Shopify/bootsnap/issues/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. --- CHANGELOG.md | 3 +++ lib/bootsnap/load_path_cache/store.rb | 18 +++++++++++++++--- test/load_path_cache/store_test.rb | 24 ++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 3 deletions(-) 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