aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack/lib/action_controller/caching
diff options
context:
space:
mode:
authorDavid Heinemeier Hansson <david@loudthinking.com>2008-01-03 21:05:12 +0000
committerDavid Heinemeier Hansson <david@loudthinking.com>2008-01-03 21:05:12 +0000
commit2a9ad9ccbc706e546bf02ec95f864944e7d7983b (patch)
tree868624e91f037840bfbf0aca30bb2ea1c9d78701 /actionpack/lib/action_controller/caching
parent288553540b5b2f37497cb19357b25ac12e0498fd (diff)
downloadrails-2a9ad9ccbc706e546bf02ec95f864944e7d7983b.tar.gz
rails-2a9ad9ccbc706e546bf02ec95f864944e7d7983b.tar.bz2
rails-2a9ad9ccbc706e546bf02ec95f864944e7d7983b.zip
Moved the caching stores from ActionController::Caching::Fragments::* to ActiveSupport::Cache::*. If you're explicitly referring to a store, like ActionController::Caching::Fragments::MemoryStore, you need to update that reference with ActiveSupport::Cache::MemoryStore [DHH] Deprecated ActionController::Base.fragment_cache_store for ActionController::Base.cache_store [DHH] All fragment cache keys are now by default prefixed with the 'views/' namespace [DHH] Added ActiveRecord::Base.cache_key to make it easier to cache Active Records in combination with the new ActiveSupport::Cache::* libraries [DHH] Added ActiveSupport::Gzip.decompress/compress(source) as an easy wrapper for Zlib [Tobias Luetke] Included MemCache-Client to make the improved ActiveSupport::Cache::MemCacheStore work out of the box [Bob Cottrell, Eric Hodel] Added config.cache_store to environment options to control the default cache store (default is FileStore if tmp/cache is present, otherwise MemoryStore is used) [DHH]
git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@8546 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
Diffstat (limited to 'actionpack/lib/action_controller/caching')
-rw-r--r--actionpack/lib/action_controller/caching/actions.rb148
-rw-r--r--actionpack/lib/action_controller/caching/fragments.rb153
-rw-r--r--actionpack/lib/action_controller/caching/pages.rb141
-rw-r--r--actionpack/lib/action_controller/caching/sql_cache.rb18
-rw-r--r--actionpack/lib/action_controller/caching/sweeping.rb90
5 files changed, 550 insertions, 0 deletions
diff --git a/actionpack/lib/action_controller/caching/actions.rb b/actionpack/lib/action_controller/caching/actions.rb
new file mode 100644
index 0000000000..4410e47eb3
--- /dev/null
+++ b/actionpack/lib/action_controller/caching/actions.rb
@@ -0,0 +1,148 @@
+require 'set'
+
+module ActionController #:nodoc:
+ module Caching
+ # 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 is allowed 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.
+ #
+ # Different representations of the same resource, e.g. <tt>http://david.somewhere.com/lists</tt> and <tt>http://david.somewhere.com/lists.xml</tt>
+ # are treated like separate requests and so are cached separately. Keep in mind when expiring an action cache that <tt>:action => 'lists'</tt> is not the same
+ # as <tt>:action => 'list', :format => :xml</tt>.
+ #
+ # You can set modify the default action cache path by passing a :cache_path option. This will be passed directly to ActionCachePath.path_for. This is handy
+ # for actions with multiple possible routes that should be cached differently. If a block is given, it is called with the current controller instance.
+ #
+ # class ListsController < ApplicationController
+ # before_filter :authenticate, :except => :public
+ # caches_page :public
+ # caches_action :show, :cache_path => { :project => 1 }
+ # caches_action :show, :cache_path => Proc.new { |controller|
+ # controller.params[:user_id] ?
+ # controller.send(:user_list_url, c.params[:user_id], c.params[:id]) :
+ # controller.send(:list_url, c.params[:id]) }
+ # end
+ module Actions
+ def self.included(base) #:nodoc:
+ base.extend(ClassMethods)
+ base.class_eval do
+ attr_accessor :rendered_action_cache, :action_cache_path
+ alias_method_chain :protected_instance_variables, :action_caching
+ end
+ end
+
+ module ClassMethods
+ # Declares that +actions+ should be cached.
+ # See ActionController::Caching::Actions for details.
+ def caches_action(*actions)
+ return unless cache_configured?
+ around_filter(ActionCacheFilter.new(*actions))
+ end
+ end
+
+ protected
+ def protected_instance_variables_with_action_caching
+ protected_instance_variables_without_action_caching + %w(@action_cache_path)
+ end
+
+ def expire_action(options = {})
+ return unless cache_configured?
+
+ if options[:action].is_a?(Array)
+ options[:action].dup.each do |action|
+ expire_fragment(ActionCachePath.path_for(self, options.merge({ :action => action })))
+ end
+ else
+ expire_fragment(ActionCachePath.path_for(self, options))
+ end
+ end
+
+ class ActionCacheFilter #:nodoc:
+ def initialize(*actions, &block)
+ @options = actions.extract_options!
+ @actions = Set.new(actions)
+ end
+
+ def before(controller)
+ return unless @actions.include?(controller.action_name.intern)
+
+ cache_path = ActionCachePath.new(controller, path_options_for(controller, @options))
+
+ if cache = controller.read_fragment(cache_path.path)
+ controller.rendered_action_cache = true
+ set_content_type!(controller, cache_path.extension)
+ controller.send!(:render_for_text, cache)
+ false
+ else
+ controller.action_cache_path = cache_path
+ end
+ end
+
+ def after(controller)
+ return if !@actions.include?(controller.action_name.intern) || controller.rendered_action_cache || !caching_allowed(controller)
+ controller.write_fragment(controller.action_cache_path.path, controller.response.body)
+ end
+
+ private
+ def set_content_type!(controller, extension)
+ controller.response.content_type = Mime::Type.lookup_by_extension(extension).to_s if extension
+ end
+
+ def path_options_for(controller, options)
+ ((path_options = options[:cache_path]).respond_to?(:call) ? path_options.call(controller) : path_options) || {}
+ end
+
+ def caching_allowed(controller)
+ controller.request.get? && controller.response.headers['Status'].to_i == 200
+ end
+ end
+
+ class ActionCachePath
+ attr_reader :path, :extension
+
+ class << self
+ def path_for(controller, options)
+ new(controller, options).path
+ end
+ end
+
+ def initialize(controller, options = {})
+ @extension = extract_extension(controller.request.path)
+ path = controller.url_for(options).split('://').last
+ normalize!(path)
+ add_extension!(path, @extension)
+ @path = URI.unescape(path)
+ end
+
+ private
+ def normalize!(path)
+ path << 'index' if path[-1] == ?/
+ end
+
+ def add_extension!(path, extension)
+ path << ".#{extension}" if extension
+ end
+
+ def extract_extension(file_path)
+ # Don't want just what comes after the last '.' to accommodate multi part extensions
+ # such as tar.gz.
+ file_path[/^[^.]+\.(.+)$/, 1]
+ end
+ end
+ end
+ end
+end \ No newline at end of file
diff --git a/actionpack/lib/action_controller/caching/fragments.rb b/actionpack/lib/action_controller/caching/fragments.rb
new file mode 100644
index 0000000000..868af19780
--- /dev/null
+++ b/actionpack/lib/action_controller/caching/fragments.rb
@@ -0,0 +1,153 @@
+module ActionController #:nodoc:
+ module Caching
+ # 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 do %>
+ # All the topics in the system:
+ # <%= render :partial => "topic", :collection => Topic.find(:all) %>
+ # <% end %>
+ #
+ # This cache will bind to the name of the action that called it, so if this code was part of the view for the topics/list action, you would
+ # be able to invalidate it using <tt>expire_fragment(:controller => "topics", :action => "list")</tt>.
+ #
+ # This default behavior is of limited use if you need to cache multiple fragments per action or if the action itself is cached using
+ # <tt>caches_action</tt>, so we also have the option to qualify the name of the cached fragment with something like:
+ #
+ # <% cache(:action => "list", :action_suffix => "all_topics") do %>
+ #
+ # That would result in a name such as "/topics/list/all_topics", avoiding conflicts with the action cache and with any fragments that use a
+ # different suffix. Note that the URL doesn't have to really exist or be callable - the url_for system is just used to generate unique
+ # cache names that we can refer to when we need to expire the cache.
+ #
+ # The expiration call for this example is:
+ #
+ # expire_fragment(:controller => "topics", :action => "list", :action_suffix => "all_topics")
+ module Fragments
+ def self.included(base) #:nodoc:
+ base.class_eval do
+ class << self
+ def fragment_cache_store=(store_option) #:nodoc:
+ ActiveSupport::Deprecation.warn('The fragment_cache_store= method is now use cache_store=')
+ self.cache_store = store_option
+ end
+
+ def fragment_cache_store #:nodoc:
+ ActiveSupport::Deprecation.warn('The fragment_cache_store method is now use cache_store')
+ cache_store
+ end
+ end
+
+ def fragment_cache_store=(store_option) #:nodoc:
+ ActiveSupport::Deprecation.warn('The fragment_cache_store= method is now use cache_store=')
+ self.cache_store = store_option
+ end
+
+ def fragment_cache_store #:nodoc:
+ ActiveSupport::Deprecation.warn('The fragment_cache_store method is now use cache_store')
+ cache_store
+ end
+ end
+ end
+
+ # Given a key (as described in <tt>expire_fragment</tt>), returns a key suitable for use in reading,
+ # writing, or expiring a cached fragment. If the key is a hash, the generated key is the return
+ # value of url_for on that hash (without the protocol). All keys are prefixed with "views/" and uses
+ # ActiveSupport::Cache.expand_cache_key for the expansion.
+ def fragment_cache_key(key)
+ ActiveSupport::Cache.expand_cache_key(key.is_a?(Hash) ? url_for(key).split("://").last : key, :views)
+ end
+
+ def fragment_for(block, name = {}, options = nil) #:nodoc:
+ unless perform_caching then block.call; return end
+
+ buffer = yield
+
+ if cache = read_fragment(name, options)
+ buffer.concat(cache)
+ else
+ pos = buffer.length
+ block.call
+ write_fragment(name, buffer[pos..-1], options)
+ end
+ end
+
+ # Called by CacheHelper#cache
+ def cache_rxml_fragment(block, name = {}, options = nil) #:nodoc:
+ fragment_for(block, name, options) do
+ eval('xml.target!', block.binding)
+ end
+ end
+
+ # Called by CacheHelper#cache
+ def cache_rjs_fragment(block, name = {}, options = nil) #:nodoc:
+ fragment_for(block, name, options) do
+ begin
+ debug_mode, ActionView::Base.debug_rjs = ActionView::Base.debug_rjs, false
+ eval('page.to_s', block.binding)
+ ensure
+ ActionView::Base.debug_rjs = debug_mode
+ end
+ end
+ end
+
+ # Called by CacheHelper#cache
+ def cache_erb_fragment(block, name = {}, options = nil) #:nodoc:
+ fragment_for(block, name, options) do
+ eval(ActionView::Base.erb_variable, block.binding)
+ end
+ end
+
+ # Writes <tt>content</tt> to the location signified by <tt>key</tt> (see <tt>expire_fragment</tt> for acceptable formats)
+ def write_fragment(key, content, options = nil)
+ return unless cache_configured?
+
+ key = fragment_cache_key(key)
+
+ self.class.benchmark "Cached fragment miss: #{key}" do
+ cache_store.write(key, content, options)
+ end
+
+ content
+ end
+
+ # Reads a cached fragment from the location signified by <tt>key</tt> (see <tt>expire_fragment</tt> for acceptable formats)
+ def read_fragment(key, options = nil)
+ return unless cache_configured?
+
+ key = fragment_cache_key(key)
+
+ self.class.benchmark "Cached fragment hit: #{key}" do
+ cache_store.read(key, options)
+ end
+ end
+
+ # Name can take one of three forms:
+ # * String: This would normally take the form of a path like "pages/45/notes"
+ # * Hash: Is treated as an implicit call to url_for, like { :controller => "pages", :action => "notes", :id => 45 }
+ # * Regexp: Will destroy all the matched fragments, example:
+ # %r{pages/\d*/notes}
+ # Ensure you do not specify start and finish in the regex (^$) because
+ # the actual filename matched looks like ./cache/filename/path.cache
+ # Regexp expiration is only supported on caches that can iterate over
+ # all keys (unlike memcached).
+ def expire_fragment(key, options = nil)
+ return unless cache_configured?
+
+ key = key.is_a?(Regexp) ? key : fragment_cache_key(key)
+
+ if key.is_a?(Regexp)
+ self.class.benchmark "Expired fragments matching: #{key.source}" do
+ cache_store.delete_matched(key, options)
+ end
+ else
+ self.class.benchmark "Expired fragment: #{key}" do
+ cache_store.delete(key, options)
+ end
+ end
+ end
+ end
+ end
+end \ No newline at end of file
diff --git a/actionpack/lib/action_controller/caching/pages.rb b/actionpack/lib/action_controller/caching/pages.rb
new file mode 100644
index 0000000000..4307f39583
--- /dev/null
+++ b/actionpack/lib/action_controller/caching/pages.rb
@@ -0,0 +1,141 @@
+require 'fileutils'
+require 'uri'
+
+module ActionController #:nodoc:
+ module Caching
+ # Page caching is an approach to caching where the entire action output of is stored as a HTML file that the web server
+ # can serve without going through the Action Pack. This can be as much as 100 times faster than going through the process of dynamically
+ # generating the content. Unfortunately, this incredible speed-up is only available to stateless pages where all visitors
+ # are treated the same. Content management systems -- including weblogs and wikis -- have many pages that are a great fit
+ # for this approach, but account-based systems where people log in and manipulate their own data are often less likely candidates.
+ #
+ # Specifying which actions to cache is done through the <tt>caches</tt> class method:
+ #
+ # class WeblogController < ActionController::Base
+ # 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
+ # generation. This is how the web server is able pick up a cache file when it exists and otherwise let the request pass on to
+ # the Action Pack to generate it.
+ #
+ # Expiration of the cache is handled by deleting the cached file, which results in a lazy regeneration approach where the cache
+ # is not restored before another hit is made against it. The API for doing so mimics the options from url_for and friends:
+ #
+ # class WeblogController < ActionController::Base
+ # def update
+ # List.update(params[:list][:id], params[:list])
+ # expire_page :action => "show", :id => params[:list][:id]
+ # redirect_to :action => "show", :id => params[:list][: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.
+ #
+ # == Setting the cache directory
+ #
+ # The cache directory should be the document root for the web server and is set using Base.page_cache_directory = "/document/root".
+ # For Rails, this directory has already been set to RAILS_ROOT + "/public".
+ #
+ # == Setting the cache extension
+ #
+ # By default, the cache extension is .html, which makes it easy for the cached files to be picked up by the web server. If you want
+ # something else, like .php or .shtml, just set Base.page_cache_extension.
+ module Pages
+ def self.included(base) #:nodoc:
+ base.extend(ClassMethods)
+ base.class_eval do
+ @@page_cache_directory = defined?(RAILS_ROOT) ? "#{RAILS_ROOT}/public" : ""
+ cattr_accessor :page_cache_directory
+
+ @@page_cache_extension = '.html'
+ cattr_accessor :page_cache_extension
+ end
+ end
+
+ module ClassMethods
+ # Expires the page that was cached with the +path+ as a key. Example:
+ # expire_page "/lists/show"
+ def expire_page(path)
+ return unless perform_caching
+
+ benchmark "Expired page: #{page_cache_file(path)}" do
+ File.delete(page_cache_path(path)) if File.exist?(page_cache_path(path))
+ end
+ end
+
+ # Manually cache the +content+ in the key determined by +path+. Example:
+ # cache_page "I'm the cached content", "/lists/show"
+ def cache_page(content, path)
+ return unless perform_caching
+
+ benchmark "Cached page: #{page_cache_file(path)}" do
+ FileUtils.makedirs(File.dirname(page_cache_path(path)))
+ File.open(page_cache_path(path), "wb+") { |f| f.write(content) }
+ end
+ end
+
+ # Caches the +actions+ using the page-caching approach that'll store the cache in a path within the page_cache_directory that
+ # matches the triggering url.
+ def caches_page(*actions)
+ return unless perform_caching
+ actions = actions.map(&:to_s)
+ after_filter { |c| c.cache_page if actions.include?(c.action_name) }
+ end
+
+ private
+ def page_cache_file(path)
+ name = (path.empty? || path == "/") ? "/index" : URI.unescape(path.chomp('/'))
+ name << page_cache_extension unless (name.split('/').last || name).include? '.'
+ return name
+ end
+
+ def page_cache_path(path)
+ page_cache_directory + page_cache_file(path)
+ end
+ end
+
+ # Expires the page that was cached with the +options+ as a key. Example:
+ # expire_page :controller => "lists", :action => "show"
+ def expire_page(options = {})
+ return unless perform_caching
+
+ if options.is_a?(Hash)
+ if options[:action].is_a?(Array)
+ options[:action].dup.each do |action|
+ self.class.expire_page(url_for(options.merge(:only_path => true, :skip_relative_url_root => true, :action => action)))
+ end
+ else
+ self.class.expire_page(url_for(options.merge(:only_path => true, :skip_relative_url_root => true)))
+ end
+ else
+ self.class.expire_page(options)
+ end
+ end
+
+ # Manually cache the +content+ in the key determined by +options+. If no content is provided, the contents of response.body is used
+ # If no options are provided, the requested url is used. Example:
+ # cache_page "I'm the cached content", :controller => "lists", :action => "show"
+ def cache_page(content = nil, options = nil)
+ return unless perform_caching && caching_allowed
+
+ path = case options
+ when Hash
+ url_for(options.merge(:only_path => true, :skip_relative_url_root => true, :format => params[:format]))
+ when String
+ options
+ else
+ request.path
+ end
+
+ self.class.cache_page(content || response.body, path)
+ end
+
+ private
+ def caching_allowed
+ request.get? && response.headers['Status'].to_i == 200
+ end
+ end
+ end
+end \ No newline at end of file
diff --git a/actionpack/lib/action_controller/caching/sql_cache.rb b/actionpack/lib/action_controller/caching/sql_cache.rb
new file mode 100644
index 0000000000..139be6100d
--- /dev/null
+++ b/actionpack/lib/action_controller/caching/sql_cache.rb
@@ -0,0 +1,18 @@
+module ActionController #:nodoc:
+ module Caching
+ module SqlCache
+ def self.included(base) #:nodoc:
+ if defined?(ActiveRecord) && ActiveRecord::Base.respond_to?(:cache)
+ base.alias_method_chain :perform_action, :caching
+ end
+ end
+
+ protected
+ def perform_action_with_caching
+ ActiveRecord::Base.cache do
+ perform_action_without_caching
+ end
+ end
+ end
+ end
+end \ No newline at end of file
diff --git a/actionpack/lib/action_controller/caching/sweeping.rb b/actionpack/lib/action_controller/caching/sweeping.rb
new file mode 100644
index 0000000000..eda4459cda
--- /dev/null
+++ b/actionpack/lib/action_controller/caching/sweeping.rb
@@ -0,0 +1,90 @@
+module ActionController #:nodoc:
+ module Caching
+ # 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 < ActionController::Caching::Sweeper
+ # observe List, Item
+ #
+ # def after_save(record)
+ # list = record.is_a?(List) ? record : record.list
+ # expire_page(:controller => "lists", :action => %w( show public feed ), :id => list.id)
+ # expire_action(:controller => "lists", :action => "all")
+ # list.shares.each { |share| expire_page(:controller => "lists", :action => "show", :id => share.url_key) }
+ # end
+ # end
+ #
+ # The sweeper is assigned in 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 for expiring those caches.
+ module Sweeping
+ def self.included(base) #:nodoc:
+ base.extend(ClassMethods)
+ end
+
+ module ClassMethods #:nodoc:
+ def cache_sweeper(*sweepers)
+ return unless perform_caching
+ configuration = sweepers.extract_options!
+ sweepers.each do |sweeper|
+ ActiveRecord::Base.observers << sweeper if defined?(ActiveRecord) and defined?(ActiveRecord::Base)
+ sweeper_instance = Object.const_get(Inflector.classify(sweeper)).instance
+
+ if sweeper_instance.is_a?(Sweeper)
+ around_filter(sweeper_instance, :only => configuration[:only])
+ else
+ after_filter(sweeper_instance, :only => configuration[:only])
+ end
+ end
+ end
+ end
+ end
+
+ if defined?(ActiveRecord) and defined?(ActiveRecord::Observer)
+ class Sweeper < ActiveRecord::Observer #:nodoc:
+ attr_accessor :controller
+
+ def before(controller)
+ self.controller = controller
+ callback(:before)
+ end
+
+ def after(controller)
+ callback(:after)
+ # Clean up, so that the controller can be collected after this request
+ self.controller = nil
+ end
+
+ protected
+ # gets the action cache path for the given options.
+ def action_path_for(options)
+ ActionController::Caching::Actions::ActionCachePath.path_for(controller, options)
+ end
+
+ # Retrieve instance variables set in the controller.
+ def assigns(key)
+ controller.instance_variable_get("@#{key}")
+ end
+
+ private
+ def callback(timing)
+ controller_callback_method_name = "#{timing}_#{controller.controller_name.underscore}"
+ action_callback_method_name = "#{controller_callback_method_name}_#{controller.action_name}"
+
+ send!(controller_callback_method_name) if respond_to?(controller_callback_method_name, true)
+ send!(action_callback_method_name) if respond_to?(action_callback_method_name, true)
+ end
+
+ def method_missing(method, *arguments)
+ return if @controller.nil?
+ @controller.send!(method, *arguments)
+ end
+ end
+ end
+ end
+end \ No newline at end of file