Created
April 23, 2014 13:27
-
-
Save jgaskins/11215195 to your computer and use it in GitHub Desktop.
Backporting Rails 4 caching to Rails 3.2
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Add these two gems | |
gem 'thread_safe' | |
gem 'cache_digests' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require 'thread_safe' | |
require 'monitor' | |
module ActionView | |
# = Action View Cache Helper | |
module Helpers | |
module CacheHelper | |
# This helper exposes a method for caching fragments of a view | |
# rather than an entire action or page. This technique is useful | |
# caching pieces like menus, lists of new topics, static HTML | |
# fragments, and so on. This method takes a block that contains | |
# the content you wish to cache. | |
# | |
# The best way to use this is by doing key-based cache expiration | |
# on top of a cache store like Memcached that'll automatically | |
# 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 dependency as the name of the cache, like so: | |
# | |
# <% cache project do %> | |
# <b>All the topics on this project</b> | |
# <%= render project.topics %> | |
# <% end %> | |
# | |
# 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/projects/123-20120806214154/7a1156131a6928cb0026877f8b749ac9 | |
# ^class ^id ^updated_at ^template tree digest | |
# | |
# 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. | |
# | |
# The digestor can be bypassed by passing skip_digest: true as an option to the cache call: | |
# | |
# <% cache project, skip_digest: true do %> | |
# <b>All the topics on this project</b> | |
# <%= render project.topics %> | |
# <% end %> | |
# | |
# ==== 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(cache_fragment_name(name, options), options, &block)) | |
else | |
yield | |
end | |
nil | |
end | |
# Cache fragments of a view if +condition+ is true | |
# | |
# <%= cache_if admin?, project do %> | |
# <b>All the topics on this project</b> | |
# <%= render project.topics %> | |
# <% end %> | |
def cache_if(condition, name = {}, options = nil, &block) | |
if condition | |
cache(name, options, &block) | |
else | |
yield | |
end | |
nil | |
end | |
# Cache fragments of a view unless +condition+ is true | |
# | |
# <%= cache_unless admin?, project do %> | |
# <b>All the topics on this project</b> | |
# <%= render project.topics %> | |
# <% end %> | |
def cache_unless(condition, name = {}, options = nil, &block) | |
cache_if !condition, name, options, &block | |
end | |
# This helper returns the name of a cache key for a given fragment cache | |
# call. By supplying skip_digest: true to cache, the digestion of cache | |
# fragments can be manually bypassed. This is useful when cache fragments | |
# cannot be manually expired unless you know the exact key which is the | |
# case when using memcached. | |
def cache_fragment_name(name = {}, options = nil) | |
skip_digest = options && options[:skip_digest] | |
if skip_digest | |
name | |
else | |
fragment_name_with_digest(name) | |
end | |
end | |
private | |
def fragment_name_with_digest(name) #:nodoc: | |
if @virtual_path | |
names = Array(name.is_a?(Hash) ? controller.url_for(name).split("://").last : name) | |
digest = Digestor.digest name: @virtual_path, finder: lookup_context, dependencies: view_cache_dependencies | |
[ *names, digest ] | |
else | |
name | |
end | |
end | |
# TODO: Create an object that has caching read/write on it | |
def fragment_for(name = {}, options = nil, &block) #:nodoc: | |
read_fragment_for(name, options) || write_fragment_for(name, options, &block) | |
end | |
def read_fragment_for(name, options) #:nodoc: | |
controller.read_fragment(name, options) | |
end | |
def write_fragment_for(name, options) #:nodoc: | |
# VIEW TODO: Make #capture usable outside of ERB | |
# This dance is needed because Builder can't use capture | |
pos = output_buffer.length | |
yield | |
output_safe = output_buffer.html_safe? | |
fragment = output_buffer.slice!(pos..-1) | |
if output_safe | |
self.output_buffer = output_buffer.class.new(output_buffer) | |
end | |
controller.write_fragment(name, fragment, options) | |
end | |
end | |
end | |
end | |
module ActionView | |
class Digestor | |
cattr_reader(:cache) | |
@@cache = ThreadSafe::Cache.new | |
@@digest_monitor = Monitor.new | |
class << self | |
# Supported options: | |
# | |
# * <tt>name</tt> - Template name | |
# * <tt>finder</tt> - An instance of ActionView::LookupContext | |
# * <tt>dependencies</tt> - An array of dependent views | |
# * <tt>partial</tt> - Specifies whether the template is a partial | |
def digest(options) | |
options.assert_valid_keys(:name, :finder, :dependencies, :partial) | |
cache_key = ([ options[:name], options[:finder].details_key.hash ].compact + Array.wrap(options[:dependencies])).join('.') | |
# this is a correctly done double-checked locking idiom | |
# (ThreadSafe::Cache'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, options) | |
end | |
end | |
end | |
private | |
def compute_and_store_digest(cache_key, options) # called under @@digest_monitor lock | |
klass = if options[:partial] || options[: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 | |
digest = klass.new(options).digest | |
# Store the actual digest if config.cache_template_loading is true | |
@@cache[cache_key] = stored_digest = digest if ActionView::Resolver.caching? | |
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 | |
attr_reader :name, :finder, :options | |
def initialize(options) | |
@name, @finder = options.values_at(:name, :finder) | |
@options = options.except(:name, :finder) | |
end | |
def digest | |
Digest::MD5.hexdigest("#{source}-#{dependency_digest}").tap do |digest| | |
logger.try :info, " Cache digest for #{template.inspect}: #{digest}" | |
end | |
rescue ActionView::MissingTemplate | |
logger.try :error, " Couldn't find template for digesting: #{name}" | |
'' | |
end | |
def dependencies | |
DependencyTracker.find_dependencies(name, template) | |
rescue ActionView::MissingTemplate | |
[] # File doesn't exist, so no dependencies | |
end | |
def nested_dependencies | |
dependencies.collect do |dependency| | |
dependencies = PartialDigestor.new(name: dependency, finder: finder).nested_dependencies | |
dependencies.any? ? { dependency => dependencies } : dependency | |
end | |
end | |
private | |
def logger | |
ActionView::Base.logger | |
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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment