From de5c48c4e36ef477b641bf11d0f883f074164415 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 10 Jan 2005 18:20:58 +0000 Subject: 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 --- actionpack/lib/action_controller/caching.rb | 225 +++++++++++++++++---- actionpack/lib/action_view/helpers/cache_helper.rb | 3 +- 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 caches 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: + # + # Hello <%= @name %> + # <% 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 + # expire_fragment(:controller => "topics", :action => "list") -- 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 caches_action. 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 + # expire_fragment(:controller => "topics", :action => "list", :action_suffix => "all_topics"). + # + # == 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 cache_sweeper 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 -- cgit v1.2.3