bootsnap 1.4.1 → 1.10.3

Sign up to get free protection for your applications and to get access to all the features.
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