Browse files

Added first stab at page and fragment caching

git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@346 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
  • Loading branch information...
1 parent 48f590a commit df79e135ac10e3570415628d14e4e82c32dde855 @dhh dhh committed Jan 8, 2005
View
2 actionpack/lib/action_controller.rb
@@ -39,6 +39,7 @@
require 'action_controller/helpers'
require 'action_controller/cookies'
require 'action_controller/cgi_process'
+require 'action_controller/caching'
ActionController::Base.class_eval do
include ActionController::Filters
@@ -51,6 +52,7 @@
include ActionController::Helpers
include ActionController::Cookies
include ActionController::Session
+ include ActionController::Caching
end
require 'action_view'
View
10 actionpack/lib/action_controller/base.rb
@@ -4,6 +4,7 @@
require 'action_controller/support/class_attribute_accessors'
require 'action_controller/support/class_inheritable_attributes'
require 'action_controller/support/inflector'
+require 'drb'
module ActionController #:nodoc:
class ActionControllerError < StandardError #:nodoc:
@@ -183,7 +184,6 @@ class Base
:buffer_size => 4096
}
-
# Determines whether the view has access to controller internals @request, @response, @session, and @template.
# By default, it does.
@@view_controller_internals = true
@@ -359,7 +359,7 @@ def render(template_name = nil, status = nil) #:doc:
# <tt>render_action "show_many"</tt> in WeblogController#display will render "#{template_root}/weblog/show_many.rhtml" or
# "#{template_root}/weblog/show_many.rxml".
def render_action(action_name, status = nil) #:doc:
- render default_template_name(action_name), status
+ render(default_template_name(action_name), status)
end
# Works like render, but disregards the template_root and requires a full path to the template that needs to be rendered. Can be
@@ -390,6 +390,12 @@ def render_text(text = nil, status = nil, &block) #:doc:
@response.body = block_given? ? block : text
@performed_render = true
end
+
+ # Renders an empty response that can be used when the request is only interested in triggering an effect. Do note that good
+ # HTTP manners mandate that you don't use GET requests to trigger data changes.
+ def render_nothing(status = nil)
+ render_text "", status
+ end
# Sends the file by streaming it 4096 bytes at a time. This way the
# whole file doesn't need to be read into memory at once. This makes
View
186 actionpack/lib/action_controller/caching.rb
@@ -0,0 +1,186 @@
+require 'fileutils'
+
+module ActionController #:nodoc:
+ module Caching #:nodoc:
+ def self.append_features(base)
+ super
+ base.send(:include, Pages)
+ base.send(:include, Fragments)
+ end
+
+ # 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 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 cach is done through the <tt>caches</tt> class method:
+ #
+ # class WeblogController < ActionController::Base
+ # caches :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 -- 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
+ module Pages
+ def self.append_features(base)
+ super
+ base.extend(ClassMethods)
+ base.class_eval do
+ @@page_cache_directory = defined?(RAILS_ROOT) ? "#{RAILS_ROOT}/public" : ""
+ cattr_accessor :page_cache_directory
+ end
+ end
+
+ module ClassMethods
+ def cache_page(content, path)
+ FileUtils.makedirs(File.dirname(page_cache_directory + path))
+ File.open(page_cache_directory + path, "w+") { |f| f.write(content) }
+ logger.info "Cached page: #{path}" unless logger.nil?
+ end
+
+ def expire_page(path)
+ File.delete(page_cache_directory + path) if File.exists?(page_cache_directory + path)
+ logger.info "Expired page: #{path}" unless logger.nil?
+ end
+
+ def caches(*actions)
+ actions.each do |action|
+ class_eval "after_filter { |c| c.cache_page if c.action_name == '#{action}' }"
+ end
+ end
+ end
+
+ def expire_page(options = {})
+ self.class.expire_page(url_for(options.merge({ :only_path => true })))
+ end
+
+ # Expires more than one page at the time. Example:
+ # expire_pages(
+ # { :controller => "lists", :action => "public", :id => list_id },
+ # { :controller => "lists", :action => "show", :id => list_id }
+ # )
+ def expire_pages(*options)
+ options.each { |option| expire_page(option) }
+ end
+
+ def cache_page(content = nil, options = {})
+ self.class.cache_page(content || @response.body, url_for(options.merge({ :only_path => true })))
+ end
+ end
+
+ module Fragments
+ def self.append_features(base)
+ super
+ base.class_eval do
+ @@cache_store = MemoryStore.new
+ cattr_accessor :fragment_cache_store
+ end
+ end
+
+ def cache_fragment(binding, name, key = nil)
+ buffer = eval("_erbout", binding)
+ if cache = fragment_cache_store.read(name, key)
+ 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?
+ end
+ end
+
+ def expire_fragment(name, key = nil)
+ fragment_cache_store.delete(name, key)
+ logger.info "Expired fragment: #{name}/#{key}" unless logger.nil?
+ end
+
+ class MemoryStore
+ def initialize
+ @data = { }
+ end
+
+ def read(name, key)
+ begin
+ key ? @data[name][key] : @data[name]
+ rescue
+ nil
+ end
+ end
+
+ def write(name, key, value)
+ if key
+ @data[name] ||= {}
+ @data[name][key] = value
+ else
+ @data[name] = value
+ end
+ end
+
+ def delete(name, key)
+ key ? @data[name].delete(key) : @data.delete(name)
+ end
+ end
+
+ class DRbStore < MemoryStore
+ def initialize(address = 'druby://localhost:9192')
+ @data = DRbObject.new(nil, address)
+ end
+ end
+
+ class FileStore
+ def initialize(cache_path)
+ @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) }
+ end
+
+ def read(name, key)
+ begin
+ IO.read(cache_file_path(name, key))
+ rescue
+ nil
+ end
+ end
+
+ def delete(name, key)
+ File.delete(cache_file_path(name, key)) if File.exist?(cache_file_path(name, key))
+ end
+
+ private
+ def cache_file_path(name, key)
+ key ? "#{@cache_path}/#{name}/#{key}" : "#{@cache_path}/#{name}"
+ end
+
+ def ensure_cache_path(path)
+ FileUtils.makedirs(path) unless File.exists?(path)
+ end
+ end
+ end
+ end
+end
View
9 actionpack/lib/action_view/helpers/cache_helper.rb
@@ -0,0 +1,9 @@
+module ActionView
+ module Helpers
+ module CacheHelper
+ def cache(binding, name, key = nil)
+ @controller.cache_fragment(binding, name, key) { yield }
+ end
+ end
+ end
+end

0 comments on commit df79e13

Please sign in to comment.