| 26 | | # Page caching is an approach to caching where the entire action output of is stored as a HTML file that the web server |
|---|
| 27 | | # can serve without going through the Action Pack. This can be as much as 100 times faster than going through the process of dynamically |
|---|
| 28 | | # generating the content. Unfortunately, this incredible speed-up is only available to stateless pages where all visitors |
|---|
| 29 | | # are treated the same. Content management systems -- including weblogs and wikis -- have many pages that are a great fit |
|---|
| 30 | | # for this approach, but account-based systems where people log in and manipulate their own data are often less likely candidates. |
|---|
| 31 | | # |
|---|
| 32 | | # Specifying which actions to cache is done through the <tt>caches</tt> class method: |
|---|
| 33 | | # |
|---|
| 34 | | # class WeblogController < ActionController::Base |
|---|
| 35 | | # caches_page :show, :new |
|---|
| 36 | | # end |
|---|
| 37 | | # |
|---|
| 38 | | # This will generate cache files such as weblog/show/5 and weblog/new, which match the URLs used to trigger the dynamic |
|---|
| 39 | | # 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 |
|---|
| 40 | | # the Action Pack to generate it. |
|---|
| 41 | | # |
|---|
| 42 | | # Expiration of the cache is handled by deleting the cached file, which results in a lazy regeneration approach where the cache |
|---|
| 43 | | # is not restored before another hit is made against it. The API for doing so mimics the options from url_for and friends: |
|---|
| 44 | | # |
|---|
| 45 | | # class WeblogController < ActionController::Base |
|---|
| 46 | | # def update |
|---|
| 47 | | # List.update(params[:list][:id], params[:list]) |
|---|
| 48 | | # expire_page :action => "show", :id => params[:list][:id] |
|---|
| 49 | | # redirect_to :action => "show", :id => params[:list][:id] |
|---|
| 50 | | # end |
|---|
| 51 | | # end |
|---|
| 52 | | # |
|---|
| 53 | | # Additionally, you can expire caches using Sweepers that act on changes in the model to determine when a cache is supposed to be |
|---|
| 54 | | # expired. |
|---|
| 55 | | # |
|---|
| 56 | | # == Setting the cache directory |
|---|
| 57 | | # |
|---|
| 58 | | # The cache directory should be the document root for the web server and is set using Base.page_cache_directory = "/document/root". |
|---|
| 59 | | # For Rails, this directory has already been set to RAILS_ROOT + "/public". |
|---|
| 60 | | # |
|---|
| 61 | | # == Setting the cache extension |
|---|
| 62 | | # |
|---|
| 63 | | # 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 |
|---|
| 64 | | # something else, like .php or .shtml, just set Base.page_cache_extension. |
|---|
| 65 | | module Pages |
|---|
| 66 | | def self.included(base) #:nodoc: |
|---|
| 67 | | base.extend(ClassMethods) |
|---|
| 68 | | base.class_eval do |
|---|
| 69 | | @@page_cache_directory = defined?(RAILS_ROOT) ? "#{RAILS_ROOT}/public" : "" |
|---|
| 70 | | cattr_accessor :page_cache_directory |
|---|
| 71 | | |
|---|
| 72 | | @@page_cache_extension = '.html' |
|---|
| 73 | | cattr_accessor :page_cache_extension |
|---|
| 74 | | end |
|---|
| 75 | | end |
|---|
| 76 | | |
|---|
| 77 | | module ClassMethods |
|---|
| 78 | | # Expires the page that was cached with the +path+ as a key. Example: |
|---|
| 79 | | # expire_page "/lists/show" |
|---|
| 80 | | def expire_page(path) |
|---|
| 81 | | return unless perform_caching |
|---|
| 82 | | |
|---|
| 83 | | benchmark "Expired page: #{page_cache_file(path)}" do |
|---|
| 84 | | File.delete(page_cache_path(path)) if File.exist?(page_cache_path(path)) |
|---|
| 85 | | end |
|---|
| 86 | | end |
|---|
| 87 | | |
|---|
| 88 | | # Manually cache the +content+ in the key determined by +path+. Example: |
|---|
| 89 | | # cache_page "I'm the cached content", "/lists/show" |
|---|
| 90 | | def cache_page(content, path) |
|---|
| 91 | | return unless perform_caching |
|---|
| 92 | | |
|---|
| 93 | | benchmark "Cached page: #{page_cache_file(path)}" do |
|---|
| 94 | | FileUtils.makedirs(File.dirname(page_cache_path(path))) |
|---|
| 95 | | File.open(page_cache_path(path), "wb+") { |f| f.write(content) } |
|---|
| 96 | | end |
|---|
| 97 | | end |
|---|
| 98 | | |
|---|
| 99 | | # Caches the +actions+ using the page-caching approach that'll store the cache in a path within the page_cache_directory that |
|---|
| 100 | | # matches the triggering url. |
|---|
| 101 | | def caches_page(*actions) |
|---|
| 102 | | return unless perform_caching |
|---|
| 103 | | actions = actions.map(&:to_s) |
|---|
| 104 | | after_filter { |c| c.cache_page if actions.include?(c.action_name) } |
|---|
| 105 | | end |
|---|
| 106 | | |
|---|
| 107 | | private |
|---|
| 108 | | def page_cache_file(path) |
|---|
| 109 | | name = (path.empty? || path == "/") ? "/index" : URI.unescape(path.chomp('/')) |
|---|
| 110 | | name << page_cache_extension unless (name.split('/').last || name).include? '.' |
|---|
| 111 | | return name |
|---|
| 112 | | end |
|---|
| 113 | | |
|---|
| 114 | | def page_cache_path(path) |
|---|
| 115 | | page_cache_directory + page_cache_file(path) |
|---|
| 116 | | end |
|---|
| 117 | | end |
|---|
| 118 | | |
|---|
| 119 | | # Expires the page that was cached with the +options+ as a key. Example: |
|---|
| 120 | | # expire_page :controller => "lists", :action => "show" |
|---|
| 121 | | def expire_page(options = {}) |
|---|
| 122 | | return unless perform_caching |
|---|
| 123 | | |
|---|
| 124 | | if options.is_a?(Hash) |
|---|
| 125 | | if options[:action].is_a?(Array) |
|---|
| 126 | | options[:action].dup.each do |action| |
|---|
| 127 | | self.class.expire_page(url_for(options.merge(:only_path => true, :skip_relative_url_root => true, :action => action))) |
|---|
| 128 | | end |
|---|
| 129 | | else |
|---|
| 130 | | self.class.expire_page(url_for(options.merge(:only_path => true, :skip_relative_url_root => true))) |
|---|
| 131 | | end |
|---|
| 132 | | else |
|---|
| 133 | | self.class.expire_page(options) |
|---|
| 134 | | end |
|---|
| 135 | | end |
|---|
| 136 | | |
|---|
| 137 | | # Manually cache the +content+ in the key determined by +options+. If no content is provided, the contents of response.body is used |
|---|
| 138 | | # If no options are provided, the requested url is used. Example: |
|---|
| 139 | | # cache_page "I'm the cached content", :controller => "lists", :action => "show" |
|---|
| 140 | | def cache_page(content = nil, options = nil) |
|---|
| 141 | | return unless perform_caching && caching_allowed |
|---|
| 142 | | |
|---|
| 143 | | path = case options |
|---|
| 144 | | when Hash |
|---|
| 145 | | url_for(options.merge(:only_path => true, :skip_relative_url_root => true, :format => params[:format])) |
|---|
| 146 | | when String |
|---|
| 147 | | options |
|---|
| 148 | | else |
|---|
| 149 | | request.path |
|---|
| 150 | | end |
|---|
| 151 | | |
|---|
| 152 | | self.class.cache_page(content || response.body, path) |
|---|
| 153 | | end |
|---|
| 154 | | |
|---|
| 155 | | private |
|---|
| 156 | | def caching_allowed |
|---|
| 157 | | request.get? && response.headers['Status'].to_i == 200 |
|---|
| 158 | | end |
|---|
| 159 | | end |
|---|
| 160 | | |
|---|
| 161 | | # Action caching is similar to page caching by the fact that the entire output of the response is cached, but unlike page caching, |
|---|
| 162 | | # every request still goes through the Action Pack. The key benefit of this is that filters are run before the cache is served, which |
|---|
| 163 | | # allows for authentication and other restrictions on whether someone is allowed to see the cache. Example: |
|---|
| 164 | | # |
|---|
| 165 | | # class ListsController < ApplicationController |
|---|
| 166 | | # before_filter :authenticate, :except => :public |
|---|
| 167 | | # caches_page :public |
|---|
| 168 | | # caches_action :show, :feed |
|---|
| 169 | | # end |
|---|
| 170 | | # |
|---|
| 171 | | # In this example, the public action doesn't require authentication, so it's possible to use the faster page caching method. But both the |
|---|
| 172 | | # show and feed action are to be shielded behind the authenticate filter, so we need to implement those as action caches. |
|---|
| 173 | | # |
|---|
| 174 | | # Action caching internally uses the fragment caching and an around filter to do the job. The fragment cache is named according to both |
|---|
| 175 | | # 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 |
|---|
| 176 | | # "david.somewhere.com/lists/show/1". This allows the cacher to differentiate between "david.somewhere.com/lists/" and |
|---|
| 177 | | # "jamis.somewhere.com/lists/" -- which is a helpful way of assisting the subdomain-as-account-key pattern. |
|---|
| 178 | | # |
|---|
| 179 | | # Different representations of the same resource, e.g. <tt>http://david.somewhere.com/lists</tt> and <tt>http://david.somewhere.com/lists.xml</tt> |
|---|
| 180 | | # 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 |
|---|
| 181 | | # as <tt>:action => 'list', :format => :xml</tt>. |
|---|
| 182 | | # |
|---|
| 183 | | # 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 |
|---|
| 184 | | # for actions with multiple possible routes that should be cached differently. If a block is given, it is called with the current controller instance. |
|---|
| 185 | | # |
|---|
| 186 | | # class ListsController < ApplicationController |
|---|
| 187 | | # before_filter :authenticate, :except => :public |
|---|
| 188 | | # caches_page :public |
|---|
| 189 | | # caches_action :show, :cache_path => { :project => 1 } |
|---|
| 190 | | # caches_action :show, :cache_path => Proc.new { |controller| |
|---|
| 191 | | # controller.params[:user_id] ? |
|---|
| 192 | | # controller.send(:user_list_url, c.params[:user_id], c.params[:id]) : |
|---|
| 193 | | # controller.send(:list_url, c.params[:id]) } |
|---|
| 194 | | # end |
|---|
| 195 | | module Actions |
|---|
| 196 | | def self.included(base) #:nodoc: |
|---|
| 197 | | base.extend(ClassMethods) |
|---|
| 198 | | base.class_eval do |
|---|
| 199 | | attr_accessor :rendered_action_cache, :action_cache_path |
|---|
| 200 | | alias_method_chain :protected_instance_variables, :action_caching |
|---|
| 201 | | end |
|---|
| 202 | | end |
|---|
| 203 | | |
|---|
| 204 | | module ClassMethods |
|---|
| 205 | | # Declares that +actions+ should be cached. |
|---|
| 206 | | # See ActionController::Caching::Actions for details. |
|---|
| 207 | | def caches_action(*actions) |
|---|
| 208 | | return unless perform_caching |
|---|
| 209 | | around_filter(ActionCacheFilter.new(*actions)) |
|---|
| 210 | | end |
|---|
| 211 | | end |
|---|
| 212 | | |
|---|
| 213 | | def protected_instance_variables_with_action_caching |
|---|
| 214 | | protected_instance_variables_without_action_caching + %w(@action_cache_path) |
|---|
| 215 | | end |
|---|
| 216 | | |
|---|
| 217 | | def expire_action(options = {}) |
|---|
| 218 | | return unless perform_caching |
|---|
| 219 | | if options[:action].is_a?(Array) |
|---|
| 220 | | options[:action].dup.each do |action| |
|---|
| 221 | | expire_fragment(ActionCachePath.path_for(self, options.merge({ :action => action }))) |
|---|
| 222 | | end |
|---|
| 223 | | else |
|---|
| 224 | | expire_fragment(ActionCachePath.path_for(self, options)) |
|---|
| 225 | | end |
|---|
| 226 | | end |
|---|
| 227 | | |
|---|
| 228 | | class ActionCacheFilter #:nodoc: |
|---|
| 229 | | def initialize(*actions, &block) |
|---|
| 230 | | @options = actions.extract_options! |
|---|
| 231 | | @actions = Set.new actions |
|---|
| 232 | | end |
|---|
| 233 | | |
|---|
| 234 | | def before(controller) |
|---|
| 235 | | return unless @actions.include?(controller.action_name.intern) |
|---|
| 236 | | cache_path = ActionCachePath.new(controller, path_options_for(controller, @options)) |
|---|
| 237 | | if cache = controller.read_fragment(cache_path.path) |
|---|
| 238 | | controller.rendered_action_cache = true |
|---|
| 239 | | set_content_type!(controller, cache_path.extension) |
|---|
| 240 | | controller.send!(:render_for_text, cache) |
|---|
| 241 | | false |
|---|
| 242 | | else |
|---|
| 243 | | controller.action_cache_path = cache_path |
|---|
| 244 | | end |
|---|
| 245 | | end |
|---|
| 246 | | |
|---|
| 247 | | def after(controller) |
|---|
| 248 | | return if !@actions.include?(controller.action_name.intern) || controller.rendered_action_cache || !caching_allowed(controller) |
|---|
| 249 | | controller.write_fragment(controller.action_cache_path.path, controller.response.body) |
|---|
| 250 | | end |
|---|
| 251 | | |
|---|
| 252 | | private |
|---|
| 253 | | def set_content_type!(controller, extension) |
|---|
| 254 | | controller.response.content_type = Mime::Type.lookup_by_extension(extension).to_s if extension |
|---|
| 255 | | end |
|---|
| 256 | | |
|---|
| 257 | | def path_options_for(controller, options) |
|---|
| 258 | | ((path_options = options[:cache_path]).respond_to?(:call) ? path_options.call(controller) : path_options) || {} |
|---|
| 259 | | end |
|---|
| 260 | | |
|---|
| 261 | | def caching_allowed(controller) |
|---|
| 262 | | controller.request.get? && controller.response.headers['Status'].to_i == 200 |
|---|
| 263 | | end |
|---|
| 264 | | end |
|---|
| 265 | | |
|---|
| 266 | | class ActionCachePath |
|---|
| 267 | | attr_reader :path, :extension |
|---|
| 268 | | |
|---|
| 269 | | class << self |
|---|
| 270 | | def path_for(controller, options) |
|---|
| 271 | | new(controller, options).path |
|---|
| 272 | | end |
|---|
| 273 | | end |
|---|
| 274 | | |
|---|
| 275 | | def initialize(controller, options = {}) |
|---|
| 276 | | @extension = extract_extension(controller.request.path) |
|---|
| 277 | | path = controller.url_for(options).split('://').last |
|---|
| 278 | | normalize!(path) |
|---|
| 279 | | add_extension!(path, @extension) |
|---|
| 280 | | @path = URI.unescape(path) |
|---|
| 281 | | end |
|---|
| 282 | | |
|---|
| 283 | | private |
|---|
| 284 | | def normalize!(path) |
|---|
| 285 | | path << 'index' if path[-1] == ?/ |
|---|
| 286 | | end |
|---|
| 287 | | |
|---|
| 288 | | def add_extension!(path, extension) |
|---|
| 289 | | path << ".#{extension}" if extension |
|---|
| 290 | | end |
|---|
| 291 | | |
|---|
| 292 | | def extract_extension(file_path) |
|---|
| 293 | | # Don't want just what comes after the last '.' to accommodate multi part extensions |
|---|
| 294 | | # such as tar.gz. |
|---|
| 295 | | file_path[/^[^.]+\.(.+)$/, 1] |
|---|
| 296 | | end |
|---|
| 297 | | end |
|---|
| 298 | | end |
|---|
| 299 | | |
|---|
| 300 | | # Fragment caching is used for caching various blocks within templates without caching the entire action as a whole. This is useful when |
|---|
| 301 | | # certain elements of an action change frequently or depend on complicated state while other parts rarely change or can be shared amongst multiple |
|---|
| 302 | | # parties. The caching is doing using the cache helper available in the Action View. A template with caching might look something like: |
|---|
| 303 | | # |
|---|
| 304 | | # <b>Hello <%= @name %></b> |
|---|
| 305 | | # <% cache do %> |
|---|
| 306 | | # All the topics in the system: |
|---|
| 307 | | # <%= render :partial => "topic", :collection => Topic.find(:all) %> |
|---|
| 308 | | # <% end %> |
|---|
| 309 | | # |
|---|
| 310 | | # 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 |
|---|
| 311 | | # be able to invalidate it using <tt>expire_fragment(:controller => "topics", :action => "list")</tt>. |
|---|
| 312 | | # |
|---|
| 313 | | # This default behavior is of limited use if you need to cache multiple fragments per action or if the action itself is cached using |
|---|
| 314 | | # <tt>caches_action</tt>, so we also have the option to qualify the name of the cached fragment with something like: |
|---|
| 315 | | # |
|---|
| 316 | | # <% cache(:action => "list", :action_suffix => "all_topics") do %> |
|---|
| 317 | | # |
|---|
| 318 | | # 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 |
|---|
| 319 | | # 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 |
|---|
| 320 | | # cache names that we can refer to when we need to expire the cache. |
|---|
| 321 | | # |
|---|
| 322 | | # The expiration call for this example is: |
|---|
| 323 | | # |
|---|
| 324 | | # expire_fragment(:controller => "topics", :action => "list", :action_suffix => "all_topics") |
|---|
| 325 | | # |
|---|
| 326 | | # == Fragment stores |
|---|
| 327 | | # |
|---|
| 328 | | # By default, cached fragments are stored in memory. The available store options are: |
|---|
| 329 | | # |
|---|
| 330 | | # * FileStore: Keeps the fragments on disk in the +cache_path+, which works well for all types of environments and allows all |
|---|
| 331 | | # processes running from the same application directory to access the cached content. |
|---|
| 332 | | # * MemoryStore: Keeps the fragments in memory, which is fine for WEBrick and for FCGI (if you don't care that each FCGI process holds its |
|---|
| 333 | | # own fragment store). It's not suitable for CGI as the process is thrown away at the end of each request. It can potentially also take |
|---|
| 334 | | # up a lot of memory since each process keeps all the caches in memory. |
|---|
| 335 | | # * DRbStore: Keeps the fragments in the memory of a separate, shared DRb process. This works for all environments and only keeps one cache |
|---|
| 336 | | # around for all processes, but requires that you run and manage a separate DRb process. |
|---|
| 337 | | # * MemCacheStore: Works like DRbStore, but uses Danga's MemCache instead. |
|---|
| 338 | | # Requires the ruby-memcache library: gem install ruby-memcache. |
|---|
| 339 | | # |
|---|
| 340 | | # Configuration examples (MemoryStore is the default): |
|---|
| 341 | | # |
|---|
| 342 | | # ActionController::Base.fragment_cache_store = :memory_store |
|---|
| 343 | | # ActionController::Base.fragment_cache_store = :file_store, "/path/to/cache/directory" |
|---|
| 344 | | # ActionController::Base.fragment_cache_store = :drb_store, "druby://localhost:9192" |
|---|
| 345 | | # ActionController::Base.fragment_cache_store = :mem_cache_store, "localhost" |
|---|
| 346 | | # ActionController::Base.fragment_cache_store = MyOwnStore.new("parameter") |
|---|
| 347 | | module Fragments |
|---|
| 348 | | def self.included(base) #:nodoc: |
|---|
| 349 | | base.class_eval do |
|---|
| 350 | | @@fragment_cache_store = MemoryStore.new |
|---|
| 351 | | cattr_reader :fragment_cache_store |
|---|
| 352 | | |
|---|
| 353 | | # Defines the storage option for cached fragments |
|---|
| 354 | | def self.fragment_cache_store=(store_option) |
|---|
| 355 | | store, *parameters = *([ store_option ].flatten) |
|---|
| 356 | | @@fragment_cache_store = if store.is_a?(Symbol) |
|---|
| 357 | | store_class_name = (store == :drb_store ? "DRbStore" : store.to_s.camelize) |
|---|
| 358 | | store_class = ActionController::Caching::Fragments.const_get(store_class_name) |
|---|
| 359 | | store_class.new(*parameters) |
|---|
| 360 | | else |
|---|
| 361 | | store |
|---|
| 362 | | end |
|---|
| 363 | | end |
|---|
| 364 | | end |
|---|
| 365 | | end |
|---|
| 366 | | |
|---|
| 367 | | # Given a name (as described in <tt>expire_fragment</tt>), returns a key suitable for use in reading, |
|---|
| 368 | | # writing, or expiring a cached fragment. If the name is a hash, the generated name is the return |
|---|
| 369 | | # value of url_for on that hash (without the protocol). |
|---|
| 370 | | def fragment_cache_key(name) |
|---|
| 371 | | name.is_a?(Hash) ? url_for(name).split("://").last : name |
|---|
| 372 | | end |
|---|
| 373 | | |
|---|
| 374 | | # Called by CacheHelper#cache |
|---|
| 375 | | def cache_erb_fragment(block, name = {}, options = nil) |
|---|
| 376 | | unless perform_caching then block.call; return end |
|---|
| 377 | | |
|---|
| 378 | | buffer = eval(ActionView::Base.erb_variable, block.binding) |
|---|
| 379 | | |
|---|
| 380 | | if cache = read_fragment(name, options) |
|---|
| 381 | | buffer.concat(cache) |
|---|
| 382 | | else |
|---|
| 383 | | pos = buffer.length |
|---|
| 384 | | block.call |
|---|
| 385 | | write_fragment(name, buffer[pos..-1], options) |
|---|
| 386 | | end |
|---|
| 387 | | end |
|---|
| 388 | | |
|---|
| 389 | | # Writes <tt>content</tt> to the location signified by <tt>name</tt> (see <tt>expire_fragment</tt> for acceptable formats) |
|---|
| 390 | | def write_fragment(name, content, options = nil) |
|---|
| 391 | | return unless perform_caching |
|---|
| 392 | | |
|---|
| 393 | | key = fragment_cache_key(name) |
|---|
| 394 | | self.class.benchmark "Cached fragment: #{key}" do |
|---|
| 395 | | fragment_cache_store.write(key, content, options) |
|---|
| 396 | | end |
|---|
| 397 | | content |
|---|
| 398 | | end |
|---|
| 399 | | |
|---|
| 400 | | # Reads a cached fragment from the location signified by <tt>name</tt> (see <tt>expire_fragment</tt> for acceptable formats) |
|---|
| 401 | | def read_fragment(name, options = nil) |
|---|
| 402 | | return unless perform_caching |
|---|
| 403 | | |
|---|
| 404 | | key = fragment_cache_key(name) |
|---|
| 405 | | self.class.benchmark "Fragment read: #{key}" do |
|---|
| 406 | | fragment_cache_store.read(key, options) |
|---|
| 407 | | end |
|---|
| 408 | | end |
|---|
| 409 | | |
|---|
| 410 | | # Name can take one of three forms: |
|---|
| 411 | | # * String: This would normally take the form of a path like "pages/45/notes" |
|---|
| 412 | | # * Hash: Is treated as an implicit call to url_for, like { :controller => "pages", :action => "notes", :id => 45 } |
|---|
| 413 | | # * Regexp: Will destroy all the matched fragments, example: |
|---|
| 414 | | # %r{pages/\d*/notes} |
|---|
| 415 | | # Ensure you do not specify start and finish in the regex (^$) because |
|---|
| 416 | | # the actual filename matched looks like ./cache/filename/path.cache |
|---|
| 417 | | # Regexp expiration is only supported on caches that can iterate over |
|---|
| 418 | | # all keys (unlike memcached). |
|---|
| 419 | | def expire_fragment(name, options = nil) |
|---|
| 420 | | return unless perform_caching |
|---|
| 421 | | |
|---|
| 422 | | key = fragment_cache_key(name) |
|---|
| 423 | | |
|---|
| 424 | | if key.is_a?(Regexp) |
|---|
| 425 | | self.class.benchmark "Expired fragments matching: #{key.source}" do |
|---|
| 426 | | fragment_cache_store.delete_matched(key, options) |
|---|
| 427 | | end |
|---|
| 428 | | else |
|---|
| 429 | | self.class.benchmark "Expired fragment: #{key}" do |
|---|
| 430 | | fragment_cache_store.delete(key, options) |
|---|
| 431 | | end |
|---|
| 432 | | end |
|---|
| 433 | | end |
|---|
| 434 | | |
|---|
| 435 | | |
|---|
| 436 | | class UnthreadedMemoryStore #:nodoc: |
|---|
| 437 | | def initialize #:nodoc: |
|---|
| 438 | | @data = {} |
|---|
| 439 | | end |
|---|
| 440 | | |
|---|
| 441 | | def read(name, options=nil) #:nodoc: |
|---|
| 442 | | @data[name] |
|---|
| 443 | | end |
|---|
| 444 | | |
|---|
| 445 | | def write(name, value, options=nil) #:nodoc: |
|---|
| 446 | | @data[name] = value |
|---|
| 447 | | end |
|---|
| 448 | | |
|---|
| 449 | | def delete(name, options=nil) #:nodoc: |
|---|
| 450 | | @data.delete(name) |
|---|
| 451 | | end |
|---|
| 452 | | |
|---|
| 453 | | def delete_matched(matcher, options=nil) #:nodoc: |
|---|
| 454 | | @data.delete_if { |k,v| k =~ matcher } |
|---|
| 455 | | end |
|---|
| 456 | | end |
|---|
| 457 | | |
|---|
| 458 | | module ThreadSafety #:nodoc: |
|---|
| 459 | | def read(name, options=nil) #:nodoc: |
|---|
| 460 | | @mutex.synchronize { super } |
|---|
| 461 | | end |
|---|
| 462 | | |
|---|
| 463 | | def write(name, value, options=nil) #:nodoc: |
|---|
| 464 | | @mutex.synchronize { super } |
|---|
| 465 | | end |
|---|
| 466 | | |
|---|
| 467 | | def delete(name, options=nil) #:nodoc: |
|---|
| 468 | | @mutex.synchronize { super } |
|---|
| 469 | | end |
|---|
| 470 | | |
|---|
| 471 | | def delete_matched(matcher, options=nil) #:nodoc: |
|---|
| 472 | | @mutex.synchronize { super } |
|---|
| 473 | | end |
|---|
| 474 | | end |
|---|
| 475 | | |
|---|
| 476 | | class MemoryStore < UnthreadedMemoryStore #:nodoc: |
|---|
| 477 | | def initialize #:nodoc: |
|---|
| 478 | | super |
|---|
| 479 | | if ActionController::Base.allow_concurrency |
|---|
| 480 | | @mutex = Mutex.new |
|---|
| 481 | | MemoryStore.module_eval { include ThreadSafety } |
|---|
| 482 | | end |
|---|
| 483 | | end |
|---|
| 484 | | end |
|---|
| 485 | | |
|---|
| 486 | | class DRbStore < MemoryStore #:nodoc: |
|---|
| 487 | | attr_reader :address |
|---|
| 488 | | |
|---|
| 489 | | def initialize(address = 'druby://localhost:9192') |
|---|
| 490 | | super() |
|---|
| 491 | | @address = address |
|---|
| 492 | | @data = DRbObject.new(nil, address) |
|---|
| 493 | | end |
|---|
| 494 | | end |
|---|
| 495 | | |
|---|
| 496 | | begin |
|---|
| 497 | | require_library_or_gem 'memcache' |
|---|
| 498 | | class MemCacheStore < MemoryStore #:nodoc: |
|---|
| 499 | | attr_reader :addresses |
|---|
| 500 | | |
|---|
| 501 | | def initialize(*addresses) |
|---|
| 502 | | super() |
|---|
| 503 | | addresses = addresses.flatten |
|---|
| 504 | | addresses = ["localhost"] if addresses.empty? |
|---|
| 505 | | @addresses = addresses |
|---|
| 506 | | @data = MemCache.new(*addresses) |
|---|
| 507 | | end |
|---|
| 508 | | end |
|---|
| 509 | | rescue LoadError |
|---|
| 510 | | # MemCache wasn't available so neither can the store be |
|---|
| 511 | | end |
|---|
| 512 | | |
|---|
| 513 | | class UnthreadedFileStore #:nodoc: |
|---|
| 514 | | attr_reader :cache_path |
|---|
| 515 | | |
|---|
| 516 | | def initialize(cache_path) |
|---|
| 517 | | @cache_path = cache_path |
|---|
| 518 | | end |
|---|
| 519 | | |
|---|
| 520 | | def write(name, value, options = nil) #:nodoc: |
|---|
| 521 | | ensure_cache_path(File.dirname(real_file_path(name))) |
|---|
| 522 | | File.open(real_file_path(name), "wb+") { |f| f.write(value) } |
|---|
| 523 | | rescue => e |
|---|
| 524 | | Base.logger.error "Couldn't create cache directory: #{name} (#{e.message})" if Base.logger |
|---|
| 525 | | end |
|---|
| 526 | | |
|---|
| 527 | | def read(name, options = nil) #:nodoc: |
|---|
| 528 | | File.open(real_file_path(name), 'rb') { |f| f.read } rescue nil |
|---|
| 529 | | end |
|---|
| 530 | | |
|---|
| 531 | | def delete(name, options) #:nodoc: |
|---|
| 532 | | File.delete(real_file_path(name)) |
|---|
| 533 | | rescue SystemCallError => e |
|---|
| 534 | | # If there's no cache, then there's nothing to complain about |
|---|
| 535 | | end |
|---|
| 536 | | |
|---|
| 537 | | def delete_matched(matcher, options) #:nodoc: |
|---|
| 538 | | search_dir(@cache_path) do |f| |
|---|
| 539 | | if f =~ matcher |
|---|
| 540 | | begin |
|---|
| 541 | | File.delete(f) |
|---|
| 542 | | rescue SystemCallError => e |
|---|
| 543 | | # If there's no cache, then there's nothing to complain about |
|---|
| 544 | | end |
|---|
| 545 | | end |
|---|
| 546 | | end |
|---|
| 547 | | end |
|---|
| 548 | | |
|---|
| 549 | | private |
|---|
| 550 | | def real_file_path(name) |
|---|
| 551 | | '%s/%s.cache' % [@cache_path, name.gsub('?', '.').gsub(':', '.')] |
|---|
| 552 | | end |
|---|
| 553 | | |
|---|
| 554 | | def ensure_cache_path(path) |
|---|
| 555 | | FileUtils.makedirs(path) unless File.exist?(path) |
|---|
| 556 | | end |
|---|
| 557 | | |
|---|
| 558 | | def search_dir(dir, &callback) |
|---|
| 559 | | Dir.foreach(dir) do |d| |
|---|
| 560 | | next if d == "." || d == ".." |
|---|
| 561 | | name = File.join(dir, d) |
|---|
| 562 | | if File.directory?(name) |
|---|
| 563 | | search_dir(name, &callback) |
|---|
| 564 | | else |
|---|
| 565 | | callback.call name |
|---|
| 566 | | end |
|---|
| 567 | | end |
|---|
| 568 | | end |
|---|
| 569 | | end |
|---|
| 570 | | |
|---|
| 571 | | class FileStore < UnthreadedFileStore #:nodoc: |
|---|
| 572 | | def initialize(cache_path) |
|---|
| 573 | | super(cache_path) |
|---|
| 574 | | if ActionController::Base.allow_concurrency |
|---|
| 575 | | @mutex = Mutex.new |
|---|
| 576 | | FileStore.module_eval { include ThreadSafety } |
|---|
| 577 | | end |
|---|
| 578 | | end |
|---|
| 579 | | end |
|---|
| 580 | | end |
|---|
| 581 | | |
|---|
| 582 | | # Sweepers are the terminators of the caching world and responsible for expiring caches when model objects change. |
|---|
| 583 | | # They do this by being half-observers, half-filters and implementing callbacks for both roles. A Sweeper example: |
|---|
| 584 | | # |
|---|
| 585 | | # class ListSweeper < ActionController::Caching::Sweeper |
|---|
| 586 | | # observe List, Item |
|---|
| 587 | | # |
|---|
| 588 | | # def after_save(record) |
|---|
| 589 | | # list = record.is_a?(List) ? record : record.list |
|---|
| 590 | | # expire_page(:controller => "lists", :action => %w( show public feed ), :id => list.id) |
|---|
| 591 | | # expire_action(:controller => "lists", :action => "all") |
|---|
| 592 | | # list.shares.each { |share| expire_page(:controller => "lists", :action => "show", :id => share.url_key) } |
|---|
| 593 | | # end |
|---|
| 594 | | # end |
|---|
| 595 | | # |
|---|
| 596 | | # The sweeper is assigned in the controllers that wish to have its job performed using the <tt>cache_sweeper</tt> class method: |
|---|
| 597 | | # |
|---|
| 598 | | # class ListsController < ApplicationController |
|---|
| 599 | | # caches_action :index, :show, :public, :feed |
|---|
| 600 | | # cache_sweeper :list_sweeper, :only => [ :edit, :destroy, :share ] |
|---|
| 601 | | # end |
|---|
| 602 | | # |
|---|
| 603 | | # In the example above, four actions are cached and three actions are responsible for expiring those caches. |
|---|
| 604 | | module Sweeping |
|---|
| 605 | | def self.included(base) #:nodoc: |
|---|
| 606 | | base.extend(ClassMethods) |
|---|
| 607 | | end |
|---|
| 608 | | |
|---|
| 609 | | module ClassMethods #:nodoc: |
|---|
| 610 | | def cache_sweeper(*sweepers) |
|---|
| 611 | | return unless perform_caching |
|---|
| 612 | | configuration = sweepers.extract_options! |
|---|
| 613 | | sweepers.each do |sweeper| |
|---|
| 614 | | ActiveRecord::Base.observers << sweeper if defined?(ActiveRecord) and defined?(ActiveRecord::Base) |
|---|
| 615 | | sweeper_instance = Object.const_get(Inflector.classify(sweeper)).instance |
|---|
| 616 | | |
|---|
| 617 | | if sweeper_instance.is_a?(Sweeper) |
|---|
| 618 | | around_filter(sweeper_instance, :only => configuration[:only]) |
|---|
| 619 | | else |
|---|
| 620 | | after_filter(sweeper_instance, :only => configuration[:only]) |
|---|
| 621 | | end |
|---|
| 622 | | end |
|---|
| | 49 | def cache_configured? |
|---|
| | 50 | perform_caching && cache_store |
|---|