Interlock An optimal-efficiency caching plugin for Rails. == License Copyright 2007 Cloudburst, LLC. Licensed under the AFL 3; see the included LICENSE file. Portions copyright 2006 Chris Wanstrath and used with permission. The public certificate for the gem is at http://rubyforge.org/frs/download.php/25331/evan_weaver-original-public_cert.pem. == Requirements * Memcached (http://www.danga.com/memcached) * memcache-client gem == What it does Interlock makes your view fragments and associated controller blocks march along together. If a fragment is fresh, the controller behavior won't run. This eliminates duplicate effort from your request cycle. Your controller blocks run so infrequently that you can use regular ActiveRecord finders and not worry about object caching at all. Interlock automatically tracks invalidation dependencies based on the model lifecyle, and supports arbitrary levels of scoping per-block. == Installation First, compile and install memcached itself. Get a memcached server running. You also need the memcache-client gem: sudo gem install memcache-client Then, install the plugin: script/plugin install -x svn://rubyforge.org/var/svn/fauna/interlock/trunk Lastly, configure your Rails app for memcached by creating a config/memcached.yml file. The format is compatible with Cache_fu: defaults: namespace: myapp sessions: false development: servers: - localhost:11211 # Default port production: servers - 10.12.128.1 - 10.12.128.2 Now you're ready to go. == Usage Interlock provides two similar caching methods: behavior_cache for controllers and view_cache for views. The both accept an optional list or hash of model dependencies, and an optional :tag keypair. view_cache also accepts a ttl. The simplest usage doesn't require any parameters. In the controller: class ItemsController < ActionController::Base def slow_action behavior_cache do @items = Item.find(:all, :conditions => "be slow") end end end Now, in the view, wrap the largest section of ERB you can find that uses data from @items (but not from any other instance variables) in a view_cache block. <% @title = "My Sweet Items" %> <% view_cache do %> <% @items.each do |item| %>

<%= item.name %>

<% end %> <% end %> You have to do them both. This automatically registers a caching dependency on Item for slow_action. The controller block won't run if the slow_action view fragment is fresh, and the view fragment will only get invalidated when an Item is changed. You can use multiple instance variables in one block, of course. Just make sure the behavior_cache provides whatever the view_cache uses. == Declaring dependencies You can declare non-default invalidation dependencies by passing models to behavior_cache (you can also pass them to view_cache, but you should only do that if you are caching a fragment without an associated behavior block in the controller). No dependencies (cache never invalidates): behavior_cache nil do end Invalidate on any Media change: behavior_cache Media do end Invalidate on any Media or Item change: behavior_cache Media, Item do end Invalidate on Item changes if the Item id matches the current params[:id] value: behavior_cache Item => :id do end You do not have to pass the same dependencies to behavior_cache and view_cache even for the same action. The set union of both dependency lists will be used. == Narrowing scope and caching multiple blocks Sometimes you need to cache multiple blocks in a controller, or otherwise get a more fine-grained scope. Interlock provides the :tag key for this purpose. :tag accepts either an array of symbols, which are mapped to params values, or an arbitrary object, which is converted to a string identifier. Your corresponding behavior caches and view caches must have identical :tag values for the interlocking to take effect. Note that :tag can be used to scope caches. You can simultaneously cache different versions of the same block, differentiating based on params or other logic. This is great for caching per-user, for example: def profile @user = current_user behavior_cache :tag => @user do @items = Item.find(:all, :conditions => "be slow") end end In the view, use the same :tag value (@user). Note that @user must be set outside of the behavior block in the controller, because its contents are used to decide whether to run the block in the first place. This way each user will see only their own cache. Pretty neat. == Broadening scope Sometimes the default scope (controller, action, params[:id]) is too narrow. For example, you might share a partial across actions, and set up its data via a filter. By default, Interlock will cache a separate version of it for each action. To avoid this, you can use the :ignore key, which lets you list parts of the default scope to ignore: before_filter :recent private def recent behavior_cache :ignore => :action do @recent = Item.find(:all, :limit => 5, :order => 'updated_at DESC') end end Valid values for :ignore are :controller, :action, :id, and :all. You can pass an array of multiple values. Just like with :tag, your corresponding behavior caches and view caches must have identical :ignore values. Note that cache blocks with :ignore values still obey the regular invalidation rules. A good way to get started is to just use the default scope. Then grep in the production log for interlock and see what keys are being set and read. If you see lots of different keys go by for data that you know is the same, then set some :ignore values. == View-only caching It's fine to use a view_cache block without a behavior_cache block. For example, to mimic regular fragment cache behavior, but take advantage of Memcached's :ttl support, call: <% view_cache nil, :ignore => :all, :tag => 'sidebar', :ttl => 5.minutes %>

On the side

<% end %> Remember that nil disables invalidation rules. This is a nice trick for keeping your caching strategy unified. == Gotchas You will not see any actual cache reuse in development mode unless you set config.action_controller.perform_caching = true in config/environments/development.rb. If you have custom render calls in the controller, they must be outside the behavior_cache blocks. No exceptions. For example: def profile behavior_cache do @items = Item.find(:all, :conditions => "be slow") end render :action => 'home' end You can write custom invalidation rules if you really want to, but try hard to avoid it; it has a significant cost in long-term maintainability. Also, Interlock obeys the ENV['RAILS_ASSET_ID'] setting, so if you need to blanket-invalidate all your caches, just change RAILS_ASSET_ID (for example, you could have it increment on every deploy). == Reporting problems * http://rubyforge.org/forum/forum.php?forum_id=18926 Patches and contributions are very welcome. Please note that contributors are required to assign copyright for their additions to Cloudburst, LLC.