bootsnap 1.4.1 → 1.10.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +189 -0
  3. data/LICENSE.txt +1 -1
  4. data/README.md +67 -18
  5. data/exe/bootsnap +5 -0
  6. data/ext/bootsnap/bootsnap.c +319 -119
  7. data/ext/bootsnap/extconf.rb +22 -14
  8. data/lib/bootsnap/bundler.rb +2 -0
  9. data/lib/bootsnap/cli/worker_pool.rb +136 -0
  10. data/lib/bootsnap/cli.rb +281 -0
  11. data/lib/bootsnap/compile_cache/iseq.rb +65 -18
  12. data/lib/bootsnap/compile_cache/json.rb +88 -0
  13. data/lib/bootsnap/compile_cache/yaml.rb +332 -39
  14. data/lib/bootsnap/compile_cache.rb +35 -7
  15. data/lib/bootsnap/explicit_require.rb +5 -3
  16. data/lib/bootsnap/load_path_cache/cache.rb +83 -32
  17. data/lib/bootsnap/load_path_cache/change_observer.rb +6 -1
  18. data/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb +39 -47
  19. data/lib/bootsnap/load_path_cache/core_ext/loaded_features.rb +12 -0
  20. data/lib/bootsnap/load_path_cache/loaded_features_index.rb +69 -26
  21. data/lib/bootsnap/load_path_cache/path.rb +8 -5
  22. data/lib/bootsnap/load_path_cache/path_scanner.rb +56 -29
  23. data/lib/bootsnap/load_path_cache/realpath_cache.rb +6 -5
  24. data/lib/bootsnap/load_path_cache/store.rb +49 -18
  25. data/lib/bootsnap/load_path_cache.rb +20 -32
  26. data/lib/bootsnap/setup.rb +3 -33
  27. data/lib/bootsnap/version.rb +3 -1
  28. data/lib/bootsnap.rb +126 -36
  29. metadata +15 -97
  30. data/.gitignore +0 -17
  31. data/.rubocop.yml +0 -20
  32. data/.travis.yml +0 -24
  33. data/CODE_OF_CONDUCT.md +0 -74
  34. data/CONTRIBUTING.md +0 -21
  35. data/Gemfile +0 -8
  36. data/README.jp.md +0 -231
  37. data/Rakefile +0 -12
  38. data/bin/ci +0 -10
  39. data/bin/console +0 -14
  40. data/bin/setup +0 -8
  41. data/bin/test-minimal-support +0 -7
  42. data/bin/testunit +0 -8
  43. data/bootsnap.gemspec +0 -45
  44. data/dev.yml +0 -10
  45. data/lib/bootsnap/load_path_cache/core_ext/active_support.rb +0 -100
  46. data/shipit.rubygems.yml +0 -0
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Bootsnap
2
4
  module LoadPathCache
3
5
  module ChangeObserver
@@ -14,18 +16,20 @@ module Bootsnap
14
16
  @lpc_observer.push_paths(self, *entries.map(&:to_s))
15
17
  super
16
18
  end
19
+ alias_method :append, :push
17
20
 
18
21
  def unshift(*entries)
19
22
  @lpc_observer.unshift_paths(self, *entries.map(&:to_s))
20
23
  super
21
24
  end
25
+ alias_method :prepend, :unshift
22
26
 
23
27
  def concat(entries)
24
28
  @lpc_observer.push_paths(self, *entries.map(&:to_s))
25
29
  super
26
30
  end
27
31
 
28
- # uniq! keeps the first occurance of each path, otherwise preserving
32
+ # uniq! keeps the first occurrence of each path, otherwise preserving
29
33
  # order, preserving the effective load path
30
34
  def uniq!(*args)
31
35
  ret = super
@@ -54,6 +58,7 @@ module Bootsnap
54
58
 
55
59
  def self.register(observer, arr)
56
60
  return if arr.frozen? # can't register observer, but no need to.
61
+
57
62
  arr.instance_variable_set(:@lpc_observer, observer)
58
63
  arr.extend(ArrayMixin)
59
64
  end
@@ -1,65 +1,54 @@
1
- module Bootsnap
2
- module LoadPathCache
3
- module CoreExt
4
- def self.make_load_error(path)
5
- err = LoadError.new("cannot load such file -- #{path}")
6
- err.define_singleton_method(:path) { path }
7
- err
8
- end
9
- end
10
- end
11
- end
1
+ # frozen_string_literal: true
12
2
 
13
3
  module Kernel
14
- module_function # rubocop:disable Style/ModuleFunction
4
+ module_function
15
5
 
16
6
  alias_method(:require_without_bootsnap, :require)
17
7
 
18
- # Note that require registers to $LOADED_FEATURES while load does not.
19
- def require_with_bootsnap_lfi(path, resolved = nil)
20
- Bootsnap::LoadPathCache.loaded_features_index.register(path, resolved) do
21
- require_without_bootsnap(resolved || path)
22
- end
23
- end
24
-
25
8
  def require(path)
26
- return false if Bootsnap::LoadPathCache.loaded_features_index.key?(path)
9
+ string_path = path.to_s
10
+ return false if Bootsnap::LoadPathCache.loaded_features_index.key?(string_path)
27
11
 
28
- if (resolved = Bootsnap::LoadPathCache.load_path_cache.find(path))
29
- return require_with_bootsnap_lfi(path, resolved)
12
+ resolved = Bootsnap::LoadPathCache.load_path_cache.find(string_path)
13
+ if Bootsnap::LoadPathCache::FALLBACK_SCAN.equal?(resolved)
14
+ if (cursor = Bootsnap::LoadPathCache.loaded_features_index.cursor(string_path))
15
+ ret = require_without_bootsnap(path)
16
+ resolved = Bootsnap::LoadPathCache.loaded_features_index.identify(string_path, cursor)
17
+ Bootsnap::LoadPathCache.loaded_features_index.register(string_path, resolved)
18
+ return ret
19
+ else
20
+ return require_without_bootsnap(path)
21
+ end
22
+ elsif false == resolved
23
+ return false
24
+ elsif resolved.nil?
25
+ error = LoadError.new(+"cannot load such file -- #{path}")
26
+ error.instance_variable_set(:@path, path)
27
+ raise error
28
+ else
29
+ # Note that require registers to $LOADED_FEATURES while load does not.
30
+ ret = require_without_bootsnap(resolved)
31
+ Bootsnap::LoadPathCache.loaded_features_index.register(string_path, resolved)
32
+ return ret
30
33
  end
31
-
32
- raise(Bootsnap::LoadPathCache::CoreExt.make_load_error(path))
33
- rescue Bootsnap::LoadPathCache::ReturnFalse
34
- false
35
- rescue Bootsnap::LoadPathCache::FallbackScan
36
- require_with_bootsnap_lfi(path)
37
34
  end
38
35
 
39
36
  alias_method(:require_relative_without_bootsnap, :require_relative)
40
37
  def require_relative(path)
38
+ location = caller_locations(1..1).first
41
39
  realpath = Bootsnap::LoadPathCache.realpath_cache.call(
42
- caller_locations(1..1).first.absolute_path, path
40
+ location.absolute_path || location.path, path
43
41
  )
44
42
  require(realpath)
45
43
  end
46
44
 
47
45
  alias_method(:load_without_bootsnap, :load)
48
46
  def load(path, wrap = false)
49
- if (resolved = Bootsnap::LoadPathCache.load_path_cache.find(path))
50
- return load_without_bootsnap(resolved, wrap)
51
- end
52
-
53
- # load also allows relative paths from pwd even when not in $:
54
- if File.exist?(relative = File.expand_path(path))
55
- return load_without_bootsnap(relative, wrap)
47
+ if (resolved = Bootsnap::LoadPathCache.load_path_cache.find(path, try_extensions: false))
48
+ load_without_bootsnap(resolved, wrap)
49
+ else
50
+ load_without_bootsnap(path, wrap)
56
51
  end
57
-
58
- raise(Bootsnap::LoadPathCache::CoreExt.make_load_error(path))
59
- rescue Bootsnap::LoadPathCache::ReturnFalse
60
- false
61
- rescue Bootsnap::LoadPathCache::FallbackScan
62
- load_without_bootsnap(path, wrap)
63
52
  end
64
53
  end
65
54
 
@@ -73,10 +62,13 @@ class Module
73
62
  # The challenge is that we don't control the point at which the entry gets
74
63
  # added to $LOADED_FEATURES and won't be able to hook that modification
75
64
  # since it's done in C-land.
76
- autoload_without_bootsnap(const, Bootsnap::LoadPathCache.load_path_cache.find(path) || path)
77
- rescue Bootsnap::LoadPathCache::ReturnFalse
78
- false
79
- rescue Bootsnap::LoadPathCache::FallbackScan
80
- autoload_without_bootsnap(const, path)
65
+ resolved = Bootsnap::LoadPathCache.load_path_cache.find(path)
66
+ if Bootsnap::LoadPathCache::FALLBACK_SCAN.equal?(resolved)
67
+ autoload_without_bootsnap(const, path)
68
+ elsif resolved == false
69
+ return false
70
+ else
71
+ autoload_without_bootsnap(const, resolved || path)
72
+ end
81
73
  end
82
74
  end
@@ -1,7 +1,19 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class << $LOADED_FEATURES
2
4
  alias_method(:delete_without_bootsnap, :delete)
3
5
  def delete(key)
4
6
  Bootsnap::LoadPathCache.loaded_features_index.purge(key)
5
7
  delete_without_bootsnap(key)
6
8
  end
9
+
10
+ alias_method(:reject_without_bootsnap!, :reject!)
11
+ def reject!(&block)
12
+ backup = dup
13
+
14
+ # FIXME: if no block is passed we'd need to return a decorated iterator
15
+ reject_without_bootsnap!(&block)
16
+
17
+ Bootsnap::LoadPathCache.loaded_features_index.purge_multi(backup - self)
18
+ end
7
19
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Bootsnap
2
4
  module LoadPathCache
3
5
  # LoadedFeaturesIndex partially mirrors an internal structure in ruby that
@@ -24,21 +26,22 @@ module Bootsnap
24
26
  class LoadedFeaturesIndex
25
27
  def initialize
26
28
  @lfi = {}
27
- @mutex = defined?(::Mutex) ? ::Mutex.new : ::Thread::Mutex.new # TODO: Remove once Ruby 2.2 support is dropped.
29
+ @mutex = Mutex.new
28
30
 
29
31
  # In theory the user could mutate $LOADED_FEATURES and invalidate our
30
- # cache. If this ever comes up in practice or if you, the
31
- # enterprising reader, feels inclined to solve this problem we could
32
+ # cache. If this ever comes up in practice - or if you, the
33
+ # enterprising reader, feels inclined to solve this problem - we could
32
34
  # parallel the work done with ChangeObserver on $LOAD_PATH to mirror
33
35
  # updates to our @lfi.
34
36
  $LOADED_FEATURES.each do |feat|
35
37
  hash = feat.hash
36
38
  $LOAD_PATH.each do |lpe|
37
39
  next unless feat.start_with?(lpe)
40
+
38
41
  # /a/b/lib/my/foo.rb
39
42
  # ^^^^^^^^^
40
43
  short = feat[(lpe.length + 1)..-1]
41
- stripped = strip_extension(short)
44
+ stripped = strip_extension_if_elidable(short)
42
45
  @lfi[short] = hash
43
46
  @lfi[stripped] = hash
44
47
  end
@@ -55,15 +58,41 @@ module Bootsnap
55
58
  end
56
59
  end
57
60
 
61
+ def purge_multi(features)
62
+ rejected_hashes = features.each_with_object({}) { |f, h| h[f.hash] = true }
63
+ @mutex.synchronize do
64
+ @lfi.reject! { |_, hash| rejected_hashes.key?(hash) }
65
+ end
66
+ end
67
+
58
68
  def key?(feature)
59
69
  @mutex.synchronize { @lfi.key?(feature) }
60
70
  end
61
71
 
72
+ def cursor(short)
73
+ unless Bootsnap.absolute_path?(short.to_s)
74
+ $LOADED_FEATURES.size
75
+ end
76
+ end
77
+
78
+ def identify(short, cursor)
79
+ $LOADED_FEATURES[cursor..-1].detect do |feat|
80
+ offset = 0
81
+ while (offset = feat.index(short, offset))
82
+ if feat.index(".", offset + 1) && !feat.index("/", offset + 2)
83
+ break true
84
+ else
85
+ offset += 1
86
+ end
87
+ end
88
+ end
89
+ end
90
+
62
91
  # There is a relatively uncommon case where we could miss adding an
63
92
  # entry:
64
93
  #
65
94
  # If the user asked for e.g. `require 'bundler'`, and we went through the
66
- # `FallbackScan` pathway in `kernel_require.rb` and therefore did not
95
+ # `FALLBACK_SCAN` pathway in `kernel_require.rb` and therefore did not
67
96
  # pass `long` (the full expanded absolute path), then we did are not able
68
97
  # to confidently add the `bundler.rb` form to @lfi.
69
98
  #
@@ -73,25 +102,19 @@ module Bootsnap
73
102
  # not quite right; or
74
103
  # 2. Inspect $LOADED_FEATURES upon return from yield to find the matching
75
104
  # entry.
76
- def register(short, long = nil)
77
- if long.nil?
78
- pat = %r{/#{Regexp.escape(short)}(\.[^/]+)?$}
79
- len = $LOADED_FEATURES.size
80
- ret = yield
81
- long = $LOADED_FEATURES[len..-1].detect { |feat| feat =~ pat }
82
- else
83
- ret = yield
84
- end
105
+ def register(short, long)
106
+ return if Bootsnap.absolute_path?(short)
85
107
 
86
108
  hash = long.hash
87
109
 
88
- # do we have 'bundler' or 'bundler.rb'?
89
- altname = if File.extname(short) != ''
90
- # strip the path from 'bundler.rb' -> 'bundler'
91
- strip_extension(short)
92
- elsif long && (ext = File.extname(long))
93
- # get the extension from the expanded path if given
94
- # 'bundler' + '.rb'
110
+ # Do we have a filename with an elidable extension, e.g.,
111
+ # 'bundler.rb', or 'libgit2.so'?
112
+ altname = if extension_elidable?(short)
113
+ # Strip the extension off, e.g. 'bundler.rb' -> 'bundler'.
114
+ strip_extension_if_elidable(short)
115
+ elsif long && (ext = File.extname(long.freeze))
116
+ # We already know the extension of the actual file this
117
+ # resolves to, so put that back on.
95
118
  short + ext
96
119
  end
97
120
 
@@ -99,17 +122,37 @@ module Bootsnap
99
122
  @lfi[short] = hash
100
123
  (@lfi[altname] = hash) if altname
101
124
  end
102
-
103
- ret
104
125
  end
105
126
 
106
127
  private
107
128
 
108
- STRIP_EXTENSION = /\.[^.]*?$/
129
+ STRIP_EXTENSION = /\.[^.]*?$/.freeze
109
130
  private_constant(:STRIP_EXTENSION)
110
131
 
111
- def strip_extension(f)
112
- f.sub(STRIP_EXTENSION, '')
132
+ # Might Ruby automatically search for this extension if
133
+ # someone tries to 'require' the file without it? E.g. Ruby
134
+ # will implicitly try 'x.rb' if you ask for 'x'.
135
+ #
136
+ # This is complex and platform-dependent, and the Ruby docs are a little
137
+ # handwavy about what will be tried when and in what order.
138
+ # So optimistically pretend that all known elidable extensions
139
+ # will be tried on all platforms, and that people are unlikely
140
+ # to name files in a way that assumes otherwise.
141
+ # (E.g. It's unlikely that someone will know that their code
142
+ # will _never_ run on MacOS, and therefore think they can get away
143
+ # with calling a Ruby file 'x.dylib.rb' and then requiring it as 'x.dylib'.)
144
+ #
145
+ # See <https://ruby-doc.org/core-2.6.4/Kernel.html#method-i-require>.
146
+ def extension_elidable?(feature)
147
+ feature.to_s.end_with?(".rb", ".so", ".o", ".dll", ".dylib")
148
+ end
149
+
150
+ def strip_extension_if_elidable(feature)
151
+ if extension_elidable?(feature)
152
+ feature.sub(STRIP_EXTENSION, "")
153
+ else
154
+ feature
155
+ end
113
156
  end
114
157
  end
115
158
  end
@@ -1,4 +1,6 @@
1
- require_relative('path_scanner')
1
+ # frozen_string_literal: true
2
+
3
+ require_relative("path_scanner")
2
4
 
3
5
  module Bootsnap
4
6
  module LoadPathCache
@@ -20,7 +22,7 @@ module Bootsnap
20
22
  attr_reader(:path)
21
23
 
22
24
  def initialize(path)
23
- @path = path.to_s
25
+ @path = path.to_s.freeze
24
26
  end
25
27
 
26
28
  # True if the path exists, but represents a non-directory object
@@ -42,6 +44,7 @@ module Bootsnap
42
44
  # set to zero anyway, just in case we change the stability heuristics.
43
45
  _, entries, dirs = store.get(expanded_path)
44
46
  return [entries, dirs] if entries # cache hit
47
+
45
48
  entries, dirs = scan!
46
49
  store.set(expanded_path, [0, entries, dirs])
47
50
  return [entries, dirs]
@@ -59,7 +62,7 @@ module Bootsnap
59
62
  end
60
63
 
61
64
  def expanded_path
62
- File.expand_path(path)
65
+ File.expand_path(path).freeze
63
66
  end
64
67
 
65
68
  private
@@ -92,8 +95,8 @@ module Bootsnap
92
95
 
93
96
  # Built-in ruby lib stuff doesn't change, but things can occasionally be
94
97
  # installed into sitedir, which generally lives under libdir.
95
- RUBY_LIBDIR = RbConfig::CONFIG['libdir']
96
- RUBY_SITEDIR = RbConfig::CONFIG['sitedir']
98
+ RUBY_LIBDIR = RbConfig::CONFIG["libdir"]
99
+ RUBY_SITEDIR = RbConfig::CONFIG["sitedir"]
97
100
 
98
101
  def stability
99
102
  @stability ||= begin
@@ -1,47 +1,74 @@
1
- require_relative('../explicit_require')
1
+ # frozen_string_literal: true
2
+
3
+ require_relative("../explicit_require")
2
4
 
3
5
  module Bootsnap
4
6
  module LoadPathCache
5
7
  module PathScanner
6
- ALL_FILES = "/{,**/*/**/}*"
7
8
  REQUIRABLE_EXTENSIONS = [DOT_RB] + DL_EXTENSIONS
8
9
  NORMALIZE_NATIVE_EXTENSIONS = !DL_EXTENSIONS.include?(LoadPathCache::DOT_SO)
9
- ALTERNATIVE_NATIVE_EXTENSIONS_PATTERN = /\.(o|bundle|dylib)\z/
10
+ ALTERNATIVE_NATIVE_EXTENSIONS_PATTERN = /\.(o|bundle|dylib)\z/.freeze
10
11
 
11
12
  BUNDLE_PATH = if Bootsnap.bundler?
12
13
  (Bundler.bundle_path.cleanpath.to_s << LoadPathCache::SLASH).freeze
13
14
  else
14
- ''.freeze
15
+ ""
15
16
  end
16
17
 
17
- def self.call(path)
18
- path = path.to_s
19
-
20
- relative_slice = (path.size + 1)..-1
21
- # If the bundle path is a descendent of this path, we do additional
22
- # checks to prevent recursing into the bundle path as we recurse
23
- # through this path. We don't want to scan the bundle path because
24
- # anything useful in it will be present on other load path items.
25
- #
26
- # This can happen if, for example, the user adds '.' to the load path,
27
- # and the bundle path is '.bundle'.
28
- contains_bundle_path = BUNDLE_PATH.start_with?(path)
29
-
30
- dirs = []
31
- requirables = []
32
-
33
- Dir.glob(path + ALL_FILES).each do |absolute_path|
34
- next if contains_bundle_path && absolute_path.start_with?(BUNDLE_PATH)
35
- relative_path = absolute_path.slice(relative_slice)
36
-
37
- if File.directory?(absolute_path)
38
- dirs << relative_path
39
- elsif REQUIRABLE_EXTENSIONS.include?(File.extname(relative_path))
40
- requirables << relative_path
18
+ class << self
19
+ def call(path)
20
+ path = File.expand_path(path.to_s).freeze
21
+ return [[], []] unless File.directory?(path)
22
+
23
+ # If the bundle path is a descendent of this path, we do additional
24
+ # checks to prevent recursing into the bundle path as we recurse
25
+ # through this path. We don't want to scan the bundle path because
26
+ # anything useful in it will be present on other load path items.
27
+ #
28
+ # This can happen if, for example, the user adds '.' to the load path,
29
+ # and the bundle path is '.bundle'.
30
+ contains_bundle_path = BUNDLE_PATH.start_with?(path)
31
+
32
+ dirs = []
33
+ requirables = []
34
+ walk(path, nil) do |relative_path, absolute_path, is_directory|
35
+ if is_directory
36
+ dirs << os_path(relative_path)
37
+ !contains_bundle_path || !absolute_path.start_with?(BUNDLE_PATH)
38
+ elsif relative_path.end_with?(*REQUIRABLE_EXTENSIONS)
39
+ requirables << os_path(relative_path)
40
+ end
41
41
  end
42
+ [requirables, dirs]
42
43
  end
43
44
 
44
- [requirables, dirs]
45
+ def walk(absolute_dir_path, relative_dir_path, &block)
46
+ Dir.foreach(absolute_dir_path) do |name|
47
+ next if name.start_with?(".")
48
+
49
+ relative_path = relative_dir_path ? File.join(relative_dir_path, name) : name
50
+
51
+ absolute_path = "#{absolute_dir_path}/#{name}"
52
+ if File.directory?(absolute_path)
53
+ if yield relative_path, absolute_path, true
54
+ walk(absolute_path, relative_path, &block)
55
+ end
56
+ else
57
+ yield relative_path, absolute_path, false
58
+ end
59
+ end
60
+ end
61
+
62
+ if RUBY_VERSION >= "3.1"
63
+ def os_path(path)
64
+ path.freeze
65
+ end
66
+ else
67
+ def os_path(path)
68
+ path.force_encoding(Encoding::US_ASCII) if path.ascii_only?
69
+ path.freeze
70
+ end
71
+ end
45
72
  end
46
73
  end
47
74
  end
@@ -15,15 +15,16 @@ module Bootsnap
15
15
 
16
16
  def realpath(caller_location, path)
17
17
  base = File.dirname(caller_location)
18
- file = find_file(File.expand_path(path, base))
19
- dir = File.dirname(file)
20
- File.join(dir, File.basename(file))
18
+ abspath = File.expand_path(path, base).freeze
19
+ find_file(abspath)
21
20
  end
22
21
 
23
22
  def find_file(name)
24
- ['', *CACHED_EXTENSIONS].each do |ext|
23
+ return File.realpath(name).freeze if File.exist?(name)
24
+
25
+ CACHED_EXTENSIONS.each do |ext|
25
26
  filename = "#{name}#{ext}"
26
- return File.realpath(filename) if File.exist?(filename)
27
+ return File.realpath(filename).freeze if File.exist?(filename)
27
28
  end
28
29
  name
29
30
  end
@@ -1,17 +1,21 @@
1
- require_relative('../explicit_require')
1
+ # frozen_string_literal: true
2
2
 
3
- Bootsnap::ExplicitRequire.with_gems('msgpack') { require('msgpack') }
4
- Bootsnap::ExplicitRequire.from_rubylibdir('fileutils')
3
+ require_relative("../explicit_require")
4
+
5
+ Bootsnap::ExplicitRequire.with_gems("msgpack") { require("msgpack") }
5
6
 
6
7
  module Bootsnap
7
8
  module LoadPathCache
8
9
  class Store
10
+ VERSION_KEY = "__bootsnap_ruby_version__"
11
+ CURRENT_VERSION = "#{RUBY_REVISION}-#{RUBY_PLATFORM}".freeze # rubocop:disable Style/RedundantFreeze
12
+
9
13
  NestedTransactionError = Class.new(StandardError)
10
14
  SetOutsideTransactionNotAllowed = Class.new(StandardError)
11
15
 
12
16
  def initialize(store_path)
13
17
  @store_path = store_path
14
- @in_txn = false
18
+ @txn_mutex = Mutex.new
15
19
  @dirty = false
16
20
  load_data
17
21
  end
@@ -21,7 +25,8 @@ module Bootsnap
21
25
  end
22
26
 
23
27
  def fetch(key)
24
- raise(SetOutsideTransactionNotAllowed) unless @in_txn
28
+ raise(SetOutsideTransactionNotAllowed) unless @txn_mutex.owned?
29
+
25
30
  v = get(key)
26
31
  unless v
27
32
  @dirty = true
@@ -32,7 +37,8 @@ module Bootsnap
32
37
  end
33
38
 
34
39
  def set(key, value)
35
- raise(SetOutsideTransactionNotAllowed) unless @in_txn
40
+ raise(SetOutsideTransactionNotAllowed) unless @txn_mutex.owned?
41
+
36
42
  if value != @data[key]
37
43
  @dirty = true
38
44
  @data[key] = value
@@ -40,12 +46,15 @@ module Bootsnap
40
46
  end
41
47
 
42
48
  def transaction
43
- raise(NestedTransactionError) if @in_txn
44
- @in_txn = true
45
- yield
46
- ensure
47
- commit_transaction
48
- @in_txn = false
49
+ raise(NestedTransactionError) if @txn_mutex.owned?
50
+
51
+ @txn_mutex.synchronize do
52
+ begin
53
+ yield
54
+ ensure
55
+ commit_transaction
56
+ end
57
+ end
49
58
  end
50
59
 
51
60
  private
@@ -59,25 +68,47 @@ module Bootsnap
59
68
 
60
69
  def load_data
61
70
  @data = begin
62
- MessagePack.load(File.binread(@store_path))
63
- # handle malformed data due to upgrade incompatability
64
- rescue Errno::ENOENT, MessagePack::MalformedFormatError, MessagePack::UnknownExtTypeError, EOFError
65
- {}
71
+ data = File.open(@store_path, encoding: Encoding::BINARY) do |io|
72
+ MessagePack.load(io)
73
+ end
74
+ if data.is_a?(Hash) && data[VERSION_KEY] == CURRENT_VERSION
75
+ data
76
+ else
77
+ default_data
78
+ end
79
+ # handle malformed data due to upgrade incompatibility
80
+ rescue Errno::ENOENT, MessagePack::MalformedFormatError, MessagePack::UnknownExtTypeError, EOFError
81
+ default_data
82
+ rescue ArgumentError => error
83
+ if error.message =~ /negative array size/
84
+ default_data
85
+ else
86
+ raise
87
+ end
66
88
  end
67
89
  end
68
90
 
69
91
  def dump_data
92
+ require "fileutils" unless defined? FileUtils
93
+
70
94
  # Change contents atomically so other processes can't get invalid
71
95
  # caches if they read at an inopportune time.
72
- tmp = "#{@store_path}.#{Process.pid}.#{(rand * 100000).to_i}.tmp"
96
+ tmp = "#{@store_path}.#{Process.pid}.#{(rand * 100_000).to_i}.tmp"
73
97
  FileUtils.mkpath(File.dirname(tmp))
74
98
  exclusive_write = File::Constants::CREAT | File::Constants::EXCL | File::Constants::WRONLY
75
99
  # `encoding:` looks redundant wrt `binwrite`, but necessary on windows
76
100
  # because binary is part of mode.
77
- File.binwrite(tmp, MessagePack.dump(@data), mode: exclusive_write, encoding: Encoding::BINARY)
101
+ File.open(tmp, mode: exclusive_write, encoding: Encoding::BINARY) do |io|
102
+ MessagePack.dump(@data, io, freeze: true)
103
+ end
78
104
  FileUtils.mv(tmp, @store_path)
79
105
  rescue Errno::EEXIST
80
106
  retry
107
+ rescue SystemCallError
108
+ end
109
+
110
+ def default_data
111
+ {VERSION_KEY => CURRENT_VERSION}
81
112
  end
82
113
  end
83
114
  end