2018-11-15

Couldn't find template for digestingエラー

Railsで以下のエラーがログに書かれていて、原因を調査したのでその備忘録。バージョンは5.2.1です。

Couldn't find template for digesting: vars/var

こんな感じな条件で発生しました。

テンプレートファイルにこういう感じで書いていると発生します。
<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_digestActionView::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_dependenciesERBTracker.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したり予期せぬ挙動になったりするわけではないので、そのままにしておいても実害は無いです。

このエントリーをはてなブックマークに追加