Railsで以下のエラーがログに書かれていて、原因を調査したのでその備忘録。バージョンは5.2.1です。
Couldn't find template for digesting: vars/var
こんな感じな条件で発生しました。
- テンプレートに
cache do end
でキャッシュを入れている - 同一テンプレート内のrenderの引数に変数・メソッドを指定している
- 指定した変数名とメソッド名で
{複数形の名前}/{名前}
のテンプレートが存在しない
<div>
<% @var = 'aaa' %>
<%= render @var %>
<% cache :foo do %>
<% end %>
</div>
コードリーディング
ヘルパーメソッドのcache
は以下のように定義されています。cache_fragment_nameメソッドでキャッシュキーを生成しています。
module ActionView
# = Action View Cache Helper
module Helpers #:nodoc:
module CacheHelper
def cache(name = {}, options = {}, &block)
if controller.respond_to?(:perform_caching) && controller.perform_caching
name_options = options.slice(:skip_digest, :virtual_path)
safe_concat(fragment_for(cache_fragment_name(name, name_options), options, &block))
else
yield
end
nil
end
cache_fragment_name
はskip_digestがfalse、あるいは指定されていない場合に fragment_name_with_digest
メソッドを呼び出します。
def cache_fragment_name(name = {}, skip_digest: nil, virtual_path: nil)
if skip_digest
name
else
fragment_name_with_digest(name, virtual_path)
end
end
fragment_name_with_digest
はActionView::Digestor.digest
を呼び出してダイジェストを生成し、そのダイジェストをキャッシュキーの一部として使います。
private
def fragment_name_with_digest(name, virtual_path)
virtual_path ||= @virtual_path
if virtual_path
name = controller.url_for(name).split("://").last if name.is_a?(Hash)
if digest = Digestor.digest(name: virtual_path, finder: lookup_context, dependencies: view_cache_dependencies).presence
[ "#{virtual_path}:#{digest}", name ]
else
[ virtual_path, name ]
end
else
name
end
end
ActionView::Digestor.digest
は依存するファイルを抽出して、それをもとにダイジェストを生成しています。treeメソッドで依存するファイルを抽出しています。
module ActionView
class Digestor
class << self
def digest(name:, finder:, dependencies: [])
dependencies ||= []
cache_key = [ name, finder.rendered_format, dependencies ].flatten.compact.join(".")
# this is a correctly done double-checked locking idiom
# (Concurrent::Map's lookups have volatile semantics)
finder.digest_cache[cache_key] || @@digest_mutex.synchronize do
finder.digest_cache.fetch(cache_key) do # re-check under lock
partial = name.include?("/_")
root = tree(name, finder, partial)
dependencies.each do |injected_dep|
root.children << Injected.new(injected_dep, nil, nil)
end
finder.digest_cache[cache_key] = root.digest(finder)
end
end
end
tree
メソッドのDependencyTracker.find_dependencies
が依存ファイルを取り出しているメソッドになります。取り出したファイルに対してさらにtree
を呼び出して再帰的に依存ファイルを取り出します。
# Create a dependency tree for template named +name+.
def tree(name, finder, partial = false, seen = {})
logical_name = name.gsub(%r|/_|, "/")
if template = find_template(finder, logical_name, [], partial, [])
finder.rendered_format ||= template.formats.first
if node = seen[template.identifier] # handle cycles in the tree
node
else
node = seen[template.identifier] = Node.create(name, logical_name, template, partial)
deps = DependencyTracker.find_dependencies(name, template, finder.view_paths)
deps.uniq { |n| n.gsub(%r|/_|, "/") }.each do |dep_file|
node.children << tree(dep_file, finder, true, seen)
end
node
end
else
unless name.include?("#") # Dynamic template partial names can never be tracked
logger.error " Couldn't find template for digesting: #{name}"
end
seen[name] ||= Missing.new(name, logical_name, nil)
end
end
find_templateで対象のファイルが見つからない場合(例えばrender @vars
と指定するとvars/var
が依存するファイルパスとして抽出されてファイルが見つからないとき)はelseブロックが実行されることになり、 Couldn’t find template for digesting:
のエラーが発生することになります。
ActionView::DependencyTracker.find_dependencies
は ERBTracker.call
を呼び出します。
module ActionView
class DependencyTracker # :nodoc:
@trackers = Concurrent::Map.new
def self.find_dependencies(name, template, view_paths = nil)
tracker = @trackers[template.handler]
return [] unless tracker
tracker.call(name, template, view_paths)
end
ERBTracker.callはdependenciesメソッドで依存しているファイルを抜き出します。renderメソッドが書かれている場合、renderメソッドに引数のテンプレートファイルに依存するので、テンプレートファイルのパスが抽出されます。
class ERBTracker # :nodoc:
# Matches:
# partial: "comments/comment", collection: @all_comments => "comments/comment"
# (object: @single_comment, partial: "comments/comment") => "comments/comment"
#
# "comments/comments"
# 'comments/comments'
# ('comments/comments')
#
# (@topic) => "topics/topic"
# topics => "topics/topic"
# (message.topics) => "topics/topic"
RENDER_ARGUMENTS = /\A
(?:\s*\(?\s*) # optional opening paren surrounded by spaces
(?:.*?#{PARTIAL_HASH_KEY}|#{LAYOUT_HASH_KEY})? # optional hash, up to the partial or layout key declaration
(?:#{STRING}|#{VARIABLE_OR_METHOD_CHAIN}) # finally, the dependency name of interest
/xm
LAYOUT_DEPENDENCY = /\A
(?:\s*\(?\s*) # optional opening paren surrounded by spaces
(?:.*?#{LAYOUT_HASH_KEY}) # check if the line has layout key declaration
(?:#{STRING}|#{VARIABLE_OR_METHOD_CHAIN}) # finally, the dependency name of interest
/xm
def self.call(name, template, view_paths = nil)
new(name, template, view_paths).dependencies
end
def dependencies
render_dependencies + explicit_dependencies
end
解決策
ダイジェスト生成時の問題なので、skip_digest: trueにしてダイジェストを生成しないようにすればエラーは発生しなくなります。ファイル変更時などでキャッシュ切れしなくなるので、キャッシュ時間が長めな場合は注意が必要ですが…。<div>
<% @var = 'aaa' %>
<%= render @var %>
<% cache :foo, skip_digest: true do %>
<% end %>
</div>
renderの引数で分岐するのではなくrenderは静的にしておいて、対象のテンプレートで処理を分岐するのも良さそうです。
ただし、エラーログには残るものの、エラーがraiseしたり予期せぬ挙動になったりするわけではないので、そのままにしておいても実害は無いです。