diff options
author | David Heinemeier Hansson <david@loudthinking.com> | 2012-08-29 14:23:15 -0500 |
---|---|---|
committer | David Heinemeier Hansson <david@loudthinking.com> | 2012-08-29 14:23:56 -0500 |
commit | 502d5e24e28b3634910495d0fb71cb20b1426aee (patch) | |
tree | ed4c626e6ff62c826c7837660bcb0b74ea8393e4 /actionpack/lib | |
parent | 3da10e3261c60b98bb24f2e56ad3829499252663 (diff) | |
download | rails-502d5e24e28b3634910495d0fb71cb20b1426aee.tar.gz rails-502d5e24e28b3634910495d0fb71cb20b1426aee.tar.bz2 rails-502d5e24e28b3634910495d0fb71cb20b1426aee.zip |
Add automatic template digests to all CacheHelper#cache calls (originally spiked in the cache_digests plugin) *DHH*
Diffstat (limited to 'actionpack/lib')
-rw-r--r-- | actionpack/lib/action_view.rb | 1 | ||||
-rw-r--r-- | actionpack/lib/action_view/digestor.rb | 104 | ||||
-rw-r--r-- | actionpack/lib/action_view/helpers/cache_helper.rb | 102 |
3 files changed, 198 insertions, 9 deletions
diff --git a/actionpack/lib/action_view.rb b/actionpack/lib/action_view.rb index 1cf6937578..68a2322163 100644 --- a/actionpack/lib/action_view.rb +++ b/actionpack/lib/action_view.rb @@ -33,6 +33,7 @@ module ActionView autoload :Base autoload :Context autoload :CompiledTemplates, "action_view/context" + autoload :Digestor autoload :Helpers autoload :LookupContext autoload :PathSet diff --git a/actionpack/lib/action_view/digestor.rb b/actionpack/lib/action_view/digestor.rb new file mode 100644 index 0000000000..cfa864cdd4 --- /dev/null +++ b/actionpack/lib/action_view/digestor.rb @@ -0,0 +1,104 @@ +require 'active_support/core_ext' +require 'logger' + +module ActionView + class Digestor + EXPLICIT_DEPENDENCY = /# Template Dependency: ([^ ]+)/ + + # Matches: + # render partial: "comments/comment", collection: commentable.comments + # render "comments/comments" + # render 'comments/comments' + # render('comments/comments') + # + # render(@topic) => render("topics/topic") + # render(topics) => render("topics/topic") + # render(message.topics) => render("topics/topic") + RENDER_DEPENDENCY = / + render\s? # render, followed by an optional space + \(? # start a optional parenthesis for the render call + (partial:)?\s? # naming the partial, used with collection -- 1st capture + ([@a-z"'][@a-z_\/\."']+) # the template name itself -- 2nd capture + /x + + cattr_accessor(:cache) { Hash.new } + cattr_accessor(:logger, instance_reader: true) { ActionView::Base.logger } + + def self.digest(name, format, finder, options = {}) + cache["#{name}.#{format}"] ||= new(name, format, finder, options).digest + end + + attr_reader :name, :format, :finder, :options + + def initialize(name, format, finder, options = {}) + @name, @format, @finder, @options = name, format, finder, options + end + + def digest + Digest::MD5.hexdigest("#{name}.#{format}-#{source}-#{dependency_digest}").tap do |digest| + logger.try :info, "Cache digest for #{name}.#{format}: #{digest}" + end + rescue ActionView::MissingTemplate + logger.try :error, "Couldn't find template for digesting: #{name}.#{format}" + '' + end + + def dependencies + render_dependencies + explicit_dependencies + rescue ActionView::MissingTemplate + [] # File doesn't exist, so no dependencies + end + + def nested_dependencies + dependencies.collect do |dependency| + dependencies = Digestor.new(dependency, format, finder, partial: true).nested_dependencies + dependencies.any? ? { dependency => dependencies } : dependency + end + end + + + private + def logical_name + name.gsub(%r|/_|, "/") + end + + def directory + name.split("/").first + end + + def partial? + options[:partial] || name.include?("/_") + end + + def source + @source ||= finder.find(logical_name, [], partial?, formats: [ format ]).source + end + + + def dependency_digest + dependencies.collect do |template_name| + Digestor.digest(template_name, format, finder, partial: true) + end.join("-") + end + + def render_dependencies + source.scan(RENDER_DEPENDENCY). + collect(&:second).uniq. + + # render(@topic) => render("topics/topic") + # render(topics) => render("topics/topic") + # render(message.topics) => render("topics/topic") + collect { |name| name.sub(/\A@?([a-z]+\.)*([a-z_]+)\z/) { "#{$2.pluralize}/#{$2.singularize}" } }. + + # render("headline") => render("message/headline") + collect { |name| name.include?("/") ? name : "#{directory}/#{name}" }. + + # replace quotes from string renders + collect { |name| name.gsub(/["']/, "") } + end + + def explicit_dependencies + source.scan(EXPLICIT_DEPENDENCY).flatten.uniq + end + end +end
\ No newline at end of file diff --git a/actionpack/lib/action_view/helpers/cache_helper.rb b/actionpack/lib/action_view/helpers/cache_helper.rb index 39518268df..59e1015976 100644 --- a/actionpack/lib/action_view/helpers/cache_helper.rb +++ b/actionpack/lib/action_view/helpers/cache_helper.rb @@ -13,10 +13,9 @@ module ActionView # kick out old entries. For more on key-based expiration, see: # http://37signals.com/svn/posts/3113-how-key-based-cache-expiration-works # - # When using this method, you list the cache dependencies as part of - # the name of the cache, like so: + # When using this method, you list the cache dependency as the name of the cache, like so: # - # <% cache [ "v1", project ] do %> + # <% cache project do %> # <b>All the topics on this project</b> # <%= render project.topics %> # <% end %> @@ -24,15 +23,89 @@ module ActionView # This approach will assume that when a new topic is added, you'll touch # the project. The cache key generated from this call will be something like: # - # views/v1/projects/123-20120806214154 - # ^class ^id ^updated_at + # views/projects/123-20120806214154/7a1156131a6928cb0026877f8b749ac9 + # ^class ^id ^updated_at ^template tree digest # - # If you update the rendering of topics, you just bump the version to v2. - # Otherwise the cache is automatically bumped whenever the project updated_at - # is touched. + # The cache is thus automatically bumped whenever the project updated_at is touched. + # + # If your template cache depends on multiple sources (try to avoid this to keep things simple), + # you can name all these dependencies as part of an array: + # + # <% cache [ project, current_user ] do %> + # <b>All the topics on this project</b> + # <%= render project.topics %> + # <% end %> + # + # This will include both records as part of the cache key and updating either of them will + # expire the cache. + # + # ==== Template digest + # + # The template digest that's added to the cache key is computed by taking an md5 of the + # contents of the entire template file. This ensures that your caches will automatically + # expire when you change the template file. + # + # Note that the md5 is taken of the entire template file, not just what's within the + # cache do/end call. So it's possible that changing something outside of that call will + # still expire the cache. + # + # Additionally, the digestor will automatically look through your template file for + # explicit and implicit dependencies, and include those as part of the digest. + # + # ==== Implicit dependencies + # + # Most template dependencies can be derived from calls to render in the template itself. + # Here are some examples of render calls that Cache Digests knows how to decode: + # + # render partial: "comments/comment", collection: commentable.comments + # render "comments/comments" + # render 'comments/comments' + # render('comments/comments') + # + # render "header" => render("comments/header") + # + # render(@topic) => render("topics/topic") + # render(topics) => render("topics/topic") + # render(message.topics) => render("topics/topic") + # + # It's not possible to derive all render calls like that, though. Here are a few examples of things that can't be derived: + # + # render group_of_attachments + # render @project.documents.where(published: true).order('created_at') + # + # You will have to rewrite those to the explicit form: + # + # render partial: 'attachments/attachment', collection: group_of_attachments + # render partial: 'documents/document', collection: @project.documents.where(published: true).order('created_at') + # + # === Explicit dependencies + # + # Some times you'll have template dependencies that can't be derived at all. This is typically + # the case when you have template rendering that happens in helpers. Here's an example: + # + # <%= render_sortable_todolists @project.todolists %> + # + # You'll need to use a special comment format to call those out: + # + # <%# Template Dependency: todolists/todolist %> + # <%= render_sortable_todolists @project.todolists %> + # + # The pattern used to match these is /# Template Dependency: ([^ ]+)/, so it's important that you type it out just so. + # You can only declare one template dependency per line. + # + # === External dependencies + # + # If you use a helper method, for example, inside of a cached block and you then update that helper, + # you'll have to bump the cache as well. It doesn't really matter how you do it, but the md5 of the template file + # must change. One recommendation is to simply be explicit in a comment, like: + # + # <%# Helper Dependency Updated: May 6, 2012 at 6pm %> + # <%= some_helper_method(person) %> + # + # Now all you'll have to do is change that timestamp when the helper method changes. def cache(name = {}, options = nil, &block) if controller.perform_caching - safe_concat(fragment_for(name, options, &block)) + safe_concat(fragment_for(fragment_name_with_digest(name), options, &block)) else yield end @@ -58,6 +131,17 @@ module ActionView controller.write_fragment(name, fragment, options) end end + + def fragment_name_with_digest(name) + if @virtual_path + [ + *Array(name.is_a?(Hash) ? controller.url_for(name).split("://").last : name), + Digestor.digest(@virtual_path, formats.last.to_sym, lookup_context) + ] + else + name + end + end end end end |