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
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
|
require 'concurrent/map'
require 'action_view/dependency_tracker'
require 'monitor'
module ActionView
class Digestor
cattr_reader(:cache)
@@cache = Concurrent::Map.new
@@digest_monitor = Monitor.new
class PerRequestDigestCacheExpiry < Struct.new(:app) # :nodoc:
def call(env)
ActionView::Digestor.cache.clear
app.call(env)
end
end
class << self
# Supported options:
#
# * <tt>name</tt> - Template name
# * <tt>finder</tt> - An instance of <tt>ActionView::LookupContext</tt>
# * <tt>dependencies</tt> - An array of dependent views
# * <tt>partial</tt> - Specifies whether the template is a partial
def digest(name:, finder:, **options)
options.assert_valid_keys(:dependencies, :partial)
cache_key = ([ name, finder.details_key.hash ].compact + Array.wrap(options[:dependencies])).join('.')
# this is a correctly done double-checked locking idiom
# (Concurrent::Map's lookups have volatile semantics)
@@cache[cache_key] || @@digest_monitor.synchronize do
@@cache.fetch(cache_key) do # re-check under lock
compute_and_store_digest(cache_key, name, finder, options)
end
end
end
private
def compute_and_store_digest(cache_key, name, finder, options) # called under @@digest_monitor lock
klass = if options[:partial] || name.include?("/_")
# Prevent re-entry or else recursive templates will blow the stack.
# There is no need to worry about other threads seeing the +false+ value,
# as they will then have to wait for this thread to let go of the @@digest_monitor lock.
pre_stored = @@cache.put_if_absent(cache_key, false).nil? # put_if_absent returns nil on insertion
PartialDigestor
else
Digestor
end
@@cache[cache_key] = stored_digest = klass.new(name, finder, options).digest
ensure
# something went wrong or ActionView::Resolver.caching? is false, make sure not to corrupt the @@cache
@@cache.delete_pair(cache_key, false) if pre_stored && !stored_digest
end
end
def self.tree(name, finder, partial = false, seen = {})
if obj = seen[name]
obj
else
logical_name = name.gsub(%r|/_|, "/")
if finder.disable_cache { finder.exists?(logical_name, [], partial) }
template = finder.disable_cache { finder.find(logical_name, [], partial) }
node = seen[name] = Node.new(name, logical_name, template, partial, [])
else
node = seen[name] = Missing.new(name, logical_name, nil, partial, [])
return node
end
deps = DependencyTracker.find_dependencies(name, template, finder.view_paths)
deps.each do |dep_file|
node.children << tree(dep_file, finder, true, seen)
end
node
end
end
class Node < Struct.new(:name, :logical_name, :template, :partial, :children)
def to_dep(finder)
if partial
PartialDigestor.new(name, finder, partial: partial)
else
Digestor.new(name, finder, partial: partial)
end
end
def digest
Digest::MD5.hexdigest("#{template.source}-#{dependency_digest}")
end
def dependency_digest
children.map(&:digest).join("-")
end
end
class Missing < Node
def digest
''
end
end
attr_reader :name, :finder, :options
def initialize(name, finder, options = {})
@name, @finder = name, finder
@options = options
end
def digest
Digest::MD5.hexdigest("#{source}-#{dependency_digest}").tap do |digest|
logger.debug " Cache digest for #{template.inspect}: #{digest}"
end
rescue ActionView::MissingTemplate
logger.error " Couldn't find template for digesting: #{name}"
''
end
def dependencies
DependencyTracker.find_dependencies(name, template, finder.view_paths)
rescue ActionView::MissingTemplate
logger.error " '#{name}' file doesn't exist, so no dependencies"
[]
end
def children
dependencies.collect do |dependency|
PartialDigestor.new(dependency, finder)
end
end
def nested_dependencies
dependencies.collect do |dependency|
dependencies = PartialDigestor.new(dependency, finder).nested_dependencies
dependencies.any? ? { dependency => dependencies } : dependency
end
end
private
class NullLogger
def self.debug(_); end
def self.error(_); end
end
def logger
ActionView::Base.logger || NullLogger
end
def logical_name
name.gsub(%r|/_|, "/")
end
def partial?
false
end
def template
@template ||= finder.disable_cache { finder.find(logical_name, [], partial?) }
end
def source
template.source
end
def dependency_digest
template_digests = dependencies.collect do |template_name|
Digestor.digest(name: template_name, finder: finder, partial: true)
end
(template_digests + injected_dependencies).join("-")
end
def injected_dependencies
Array.wrap(options[:dependencies])
end
end
class PartialDigestor < Digestor # :nodoc:
def partial?
true
end
end
end
|