aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--actionpack/lib/action_controller/caching.rb225
-rw-r--r--actionpack/lib/action_view/helpers/cache_helper.rb3
2 files changed, 184 insertions, 44 deletions
diff --git a/actionpack/lib/action_controller/caching.rb b/actionpack/lib/action_controller/caching.rb
index 45d162d2be..cdfcafa3d7 100644
--- a/actionpack/lib/action_controller/caching.rb
+++ b/actionpack/lib/action_controller/caching.rb
@@ -4,8 +4,7 @@ module ActionController #:nodoc:
module Caching #:nodoc:
def self.append_features(base)
super
- base.send(:include, Pages)
- base.send(:include, Fragments)
+ base.send(:include, Pages, Actions, Fragments, Sweeping)
end
# Page caching is an approach to caching where the entire action output of is stored as a HTML file that the web server
@@ -17,7 +16,7 @@ module ActionController #:nodoc:
# Specifying which actions to cach is done through the <tt>caches</tt> class method:
#
# class WeblogController < ActionController::Base
- # caches :show, :new
+ # caches_page :show, :new
# end
#
# This will generate cache files such as weblog/show/5 and weblog/new, which match the URLs used to trigger the dynamic
@@ -35,14 +34,8 @@ module ActionController #:nodoc:
# end
# end
#
- # Additionally, you can expire caches -- or even record new caches -- from outside of the controller, such as from a Active
- # Record observer:
- #
- # class PostObserver < ActiveRecord::Observer
- # def after_update(post)
- # WeblogController.expire_page "/weblog/show/#{post.id}"
- # end
- # end
+ # Additionally, you can expire caches using Sweepers that act on changes in the model to determine when a cache is supposed to be
+ # expired.
module Pages
def self.append_features(base)
super
@@ -65,7 +58,7 @@ module ActionController #:nodoc:
logger.info "Expired page: #{path}" unless logger.nil?
end
- def caches(*actions)
+ def caches_page(*actions)
actions.each do |action|
class_eval "after_filter { |c| c.cache_page if c.action_name == '#{action}' }"
end
@@ -73,7 +66,13 @@ module ActionController #:nodoc:
end
def expire_page(options = {})
- self.class.expire_page(url_for(options.merge({ :only_path => true })))
+ if options[:action].is_a?(Array)
+ options[:action].dup.each do |action|
+ self.class.expire_page(url_for(options.merge({ :only_path => true, :action => action })))
+ end
+ else
+ self.class.expire_page(url_for(options.merge({ :only_path => true })))
+ end
end
# Expires more than one page at the time. Example:
@@ -90,6 +89,86 @@ module ActionController #:nodoc:
end
end
+ # Action caching is similar to page caching by the fact that the entire output of the response is cached, but unlike page caching,
+ # every request still goes through the Action Pack. The key benefit of this is that filters are run before the cache is served, which
+ # allows for authentication and other restrictions on whether someone are supposed to see the cache. Example:
+ #
+ # class ListsController < ApplicationController
+ # before_filter :authenticate, :except => :public
+ # caches_page :public
+ # caches_action :show, :feed
+ # end
+ #
+ # In this example, the public action doesn't require authentication, so it's possible to use the faster page caching method. But both the
+ # show and feed action are to be shielded behind the authenticate filter, so we need to implement those as action caches.
+ #
+ # Action caching internally uses the fragment caching and an around filter to do the job. The fragment cache is named according to both
+ # the current host and the path. So a page that is accessed at http://david.somewhere.com/lists/show/1 will result in a fragment named
+ # "david.somewhere.com/lists/show/1". This allows the cacher to differentiate between "david.somewhere.com/lists/" and
+ # "jamis.somewhere.com/lists/" -- which is a helpful way of assisting the subdomain-as-account-key pattern.
+ module Actions
+ def self.append_features(base)
+ super
+ base.extend(ClassMethods)
+ base.send(:attr_accessor, :rendered_action_cache)
+ end
+
+ module ClassMethods
+ def caches_action(*actions)
+ around_filter(ActionCacheFilter.new(*actions))
+ end
+ end
+
+ def expire_action(options = {})
+ expire_fragment(url_for(options).split("://").last)
+ end
+
+ class ActionCacheFilter
+ def initialize(*actions)
+ @actions = actions
+ end
+
+ def before(controller)
+ return unless @actions.include?(controller.action_name.intern)
+ if cache = controller.read_fragment(controller.url_for.split("://").last)
+ controller.rendered_action_cache = true
+ controller.send(:render_text, cache)
+ false
+ end
+ end
+
+ def after(controller)
+ return if !@actions.include?(controller.action_name.intern) || controller.rendered_action_cache
+ controller.write_fragment(controller.url_for.split("://").last, controller.response.body)
+ end
+ end
+ end
+
+ # Fragment caching is used for caching various blocks within templates without caching the entire action as a whole. This is useful when
+ # certain elements of an action change frequently or depend on complicated state while other parts rarely change or can be shared amongst multiple
+ # parties. The caching is doing using the cache helper available in the Action View. A template with caching might look something like:
+ #
+ # <b>Hello <%= @name %></b>
+ # <% cache(binding) do %>
+ # All the topics in the system:
+ # <%= render_collection_of_partials "topic", Topic.find_all %>
+ # <% end %>
+ #
+ # This cache will bind to the name of action that called it. So you would be able to invalidate it using
+ # <tt>expire_fragment(:controller => "topics", :action => "list")</tt> -- if that was the controller/action used. This is not too helpful
+ # if you need to cache multiple fragments per action or if the action itself is cached using <tt>caches_action</tt>. So instead we should
+ # qualify the name of the action used with something like:
+ #
+ # <% cache(binding, :action => "list", :action_suffix => "all_topics") do %>
+ #
+ # That would result in a name such as "/topics/list/all_topics", which wouldn't conflict with any action cache and neither with another
+ # fragment using a different suffix. Note that the URL doesn't have to really exist or be callable. We're just using the url_for system
+ # to generate unique cache names that we can refer to later for expirations. The expiration call for this example would be
+ # <tt>expire_fragment(:controller => "topics", :action => "list", :action_suffix => "all_topics")</tt>.
+ #
+ # == Fragment stores
+ #
+ # TO BE WRITTEN...
module Fragments
def self.append_features(base)
super
@@ -99,22 +178,41 @@ module ActionController #:nodoc:
end
end
- def cache_fragment(binding, name, key = nil)
+ # Called by CacheHelper#cache
+ def cache_erb_fragment(binding, name = {}, options = {})
buffer = eval("_erbout", binding)
- if cache = fragment_cache_store.read(name, key)
+
+ name = url_for(name) if name.is_a?(Hash)
+ if cache = read_fragment(name, options)
buffer.concat(cache)
- logger.info "Fragment hit: #{name}/#{key}" unless logger.nil?
else
pos = buffer.length
yield
- fragment_cache_store.write(name, key, buffer[pos..-1])
- logger.info "Cached fragment: #{name}/#{key}" unless logger.nil?
+ write_fragment(name, buffer[pos..-1], options)
end
end
-
- def expire_fragment(name, key = nil)
- fragment_cache_store.delete(name, key)
- logger.info "Expired fragment: #{name}/#{key}" unless logger.nil?
+
+ def write_fragment(name, content, options = {})
+ name = url_for(name) if name.is_a?(Hash)
+ fragment_cache_store.write(name, content, options)
+ logger.info "Cached fragment: #{name}" unless logger.nil?
+ content
+ end
+
+ def read_fragment(name, options = {})
+ name = url_for(name) if name.is_a?(Hash)
+ if cache = fragment_cache_store.read(name, options)
+ logger.info "Fragment hit: #{name}" unless logger.nil?
+ cache
+ else
+ false
+ end
+ end
+
+ def expire_fragment(name, options = {})
+ name = url_for(name) if name.is_a?(Hash)
+ fragment_cache_store.delete(name, options)
+ logger.info "Expired fragment: #{name}" unless logger.nil?
end
class MemoryStore
@@ -122,25 +220,20 @@ module ActionController #:nodoc:
@data = { }
end
- def read(name, key)
+ def read(name, options = {}) #:nodoc:
begin
- key ? @data[name][key] : @data[name]
+ @data[name]
rescue
nil
end
end
- def write(name, key, value)
- if key
- @data[name] ||= {}
- @data[name][key] = value
- else
- @data[name] = value
- end
+ def write(name, value, options = {}) #:nodoc:
+ @data[name] = value
end
- def delete(name, key)
- key ? @data[name].delete(key) : @data.delete(name)
+ def delete(name, options = {}) #:nodoc:
+ @data.delete(name)
end
end
@@ -155,32 +248,78 @@ module ActionController #:nodoc:
@cache_path = cache_path
end
- def write(name, key, value)
- ensure_cache_path(File.dirname(cache_file_path(name, key)))
- File.open(cache_file_path(name, key), "w+") { |f| f.write(value) }
+ def write(name, value, options = {}) #:nodoc:
+ begin
+ ensure_cache_path(File.dirname(real_file_path(name)))
+ File.open(real_file_path(name), "w+") { |f| f.write(value) }
+ rescue => e
+ Base.logger.info "Couldn't create cache directory: #{name} (#{e.message})" unless Base.logger.nil?
+ end
end
- def read(name, key)
+ def read(name, options = {}) #:nodoc:
begin
- IO.read(cache_file_path(name, key))
+ IO.read(real_file_path(name))
rescue
nil
end
end
- def delete(name, key)
- File.delete(cache_file_path(name, key)) if File.exist?(cache_file_path(name, key))
+ def delete(name, options) #:nodoc:
+ File.delete(real_file_path(name)) if File.exist?(real_file_path(name))
end
private
- def cache_file_path(name, key)
- key ? "#{@cache_path}/#{name}/#{key}" : "#{@cache_path}/#{name}"
+ def real_file_path(name)
+ "#{@cache_path}/#{name}"
end
-
+
def ensure_cache_path(path)
FileUtils.makedirs(path) unless File.exists?(path)
end
end
end
+
+ module Sweeping #:nodoc:
+ def self.append_features(base) #:nodoc:
+ super
+ base.extend(ClassMethods)
+ end
+
+ # Sweepers are the terminators of the caching world and responsible for expiring caches when model objects change.
+ # They do this by being half-observers, half-filters and implementing callbacks for both roles. A Sweeper example:
+ #
+ # class ListSweeper < ActiveRecord::Observer
+ # observe List, Item
+ #
+ # def after_save(record)
+ # @list = record.is_a?(List) ? record : record.list
+ # end
+ #
+ # def filter(controller)
+ # controller.expire_page(:controller => "lists", :action => %w( show public feed ), :id => @list.id)
+ # controller.expire_action(:controller => "lists", :action => "all")
+ # @list.shares.each { |share| controller.expire_page(:controller => "lists", :action => "show", :id => share.url_key) }
+ # end
+ # end
+ #
+ # The sweeper is assigned on the controllers that wish to have its job performed using the <tt>cache_sweeper</tt> class method:
+ #
+ # class ListsController < ApplicationController
+ # caches_action :index, :show, :public, :feed
+ # cache_sweeper :list_sweeper, :only => [ :edit, :destroy, :share ]
+ # end
+ #
+ # In the example above, four actions are cached and three actions are responsible of expiring those caches.
+ module ClassMethods
+ def cache_sweeper(*sweepers)
+ configuration = sweepers.last.is_a?(Hash) ? sweepers.pop : {}
+ sweepers.each do |sweeper|
+ observer(sweeper)
+ after_filter(Object.const_get(Inflector.classify(sweeper)).instance, :only => configuration[:only])
+ end
+ end
+ end
+ 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 ca5a746c26..530e38e81c 100644
--- a/actionpack/lib/action_view/helpers/cache_helper.rb
+++ b/actionpack/lib/action_view/helpers/cache_helper.rb
@@ -1,8 +1,9 @@
module ActionView
module Helpers
+ # See ActionController::Caching::Fragments for usage instructions.
module CacheHelper
def cache(binding, name, key = nil)
- @controller.cache_fragment(binding, name, key) { yield }
+ @controller.cache_erb_fragment(binding, name, key) { yield }
end
end
end