forked from Shopify/bootsnap
/
store.rb
98 lines (87 loc) · 2.59 KB
/
store.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# frozen_string_literal: true
require_relative('../explicit_require')
Bootsnap::ExplicitRequire.with_gems('msgpack') { require('msgpack') }
Bootsnap::ExplicitRequire.from_rubylibdir('fileutils')
module Bootsnap
module LoadPathCache
class Store
NestedTransactionError = Class.new(StandardError)
SetOutsideTransactionNotAllowed = Class.new(StandardError)
def initialize(store_path)
@store_path = store_path
@txn_mutex = Mutex.new
@dirty = false
load_data
end
def get(key)
@data[key]
end
def fetch(key)
raise(SetOutsideTransactionNotAllowed) unless @txn_mutex.owned?
v = get(key)
unless v
@dirty = true
v = yield
@data[key] = v
end
v
end
def set(key, value)
raise(SetOutsideTransactionNotAllowed) unless @txn_mutex.owned?
if value != @data[key]
@dirty = true
@data[key] = value
end
end
def transaction
raise(NestedTransactionError) if @txn_mutex.owned?
@txn_mutex.synchronize do
begin
yield
ensure
commit_transaction
end
end
end
private
def commit_transaction
if @dirty
dump_data
@dirty = false
end
end
def load_data
@data = begin
File.open(@store_path, encoding: Encoding::BINARY) do |io|
MessagePack.load(io)
end
# handle malformed data due to upgrade incompatibility
rescue Errno::ENOENT, MessagePack::MalformedFormatError, MessagePack::UnknownExtTypeError, EOFError
{}
rescue ArgumentError => error
if error.message =~ /negative array size/
{}
else
raise
end
end
end
def dump_data
# Change contents atomically so other processes can't get invalid
# caches if they read at an inopportune time.
tmp = "#{@store_path}.#{Process.pid}.#{(rand * 100000).to_i}.tmp"
FileUtils.mkpath(File.dirname(tmp))
exclusive_write = File::Constants::CREAT | File::Constants::EXCL | File::Constants::WRONLY
# `encoding:` looks redundant wrt `binwrite`, but necessary on windows
# because binary is part of mode.
File.open(tmp, mode: exclusive_write, encoding: Encoding::BINARY) do |io|
MessagePack.dump(@data, io, freeze: true)
end
FileUtils.mv(tmp, @store_path)
rescue Errno::EEXIST
retry
rescue Errno::EROFS
end
end
end
end