Ruby on Rails | Screencasts | Download | Documentation | Weblog | Community | Source

root/branches/1-2-stable/actionpack/lib/action_controller/caching.rb

Revision 7185, 24.8 kB (checked in by nzkoz, 1 year ago)

Improve performance of action caching. Closes #8231 [skaes]

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