mirror of
https://github.com/github/rails.git
synced 2026-04-26 03:00:59 -04:00
Added first stab at page and fragment caching
git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@346 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
This commit is contained in:
@@ -39,6 +39,7 @@ require 'action_controller/scaffolding'
|
||||
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 @@ ActionController::Base.class_eval do
|
||||
include ActionController::Helpers
|
||||
include ActionController::Cookies
|
||||
include ActionController::Session
|
||||
include ActionController::Caching
|
||||
end
|
||||
|
||||
require 'action_view'
|
||||
|
||||
@@ -4,6 +4,7 @@ require 'action_controller/url_rewriter'
|
||||
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 @@ module ActionController #:nodoc:
|
||||
: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 @@ module ActionController #:nodoc:
|
||||
# <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 @@ module ActionController #:nodoc:
|
||||
@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
|
||||
|
||||
186
actionpack/lib/action_controller/caching.rb
Normal file
186
actionpack/lib/action_controller/caching.rb
Normal file
@@ -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
|
||||
9
actionpack/lib/action_view/helpers/cache_helper.rb
Normal file
9
actionpack/lib/action_view/helpers/cache_helper.rb
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user