mirror of
https://github.com/github/rails.git
synced 2026-04-26 03:00:59 -04:00
Updated caching to include action caching as well and simplified the name/key reference to just be name
git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@368 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user