Not that the first version was all that bad, but the second version is much better. Filters make all the difference (also some bug fixes):
class ItemsController < ApplicationController
before_filter :item, :only=>[:show, :update]
if_modified :item, :only=>:show
if_unmodified :item, :only=>:update
def show
end
def update
item.update! params[:item]
render :action=>'show'
end
private
def item
@item ||= Item.find(params[:id])
end
end
if_modified
The if_modified filter calculates ETag and Last-Modified values from the instance variable, and compares those to the conditional GET If-Modified-Since/If-None-Match headers. If the data changed since the last request, it performs the action and sets new headers on the response. If the data didn’t change, it sends back 304 (Not Modified).
That means you only need some minimal information to figure out whether or not the action should run its course. For heavy stuff, this can save you from loading a lot of data and rendering the response. And of course you can do it entirely from Memcached and save a trip to the database.
if_unmodified
The if_unmodified filter calculates ETag and Last-Modified values from the instance variable, and compares those to the conditional PUT If-Unmodified-Since/If-Match headers. If the data didn’t change since the last request, it performs the action and sets new headers based on the new (post-update) values. If the data did change, we have a conflicting update, and it sends back 412 (Precondition Failed). That’s sign for the client to retrieve the resource again and attempt another update.
You can use this one to solve the lost update problem. If two clients are updating the same resource concurrently, one gets served with a Notice Of Conflict, so it can safely run the update again.
It can also be used to reliably create a resource, which I’ll cover in a future post.
Usage
The first argument to either filter can be a method name (:item) or an instance variable name (:@item), or you can use the :using option with method name, variable name or a block.
Last-Modified is calculated from the update_at value, most recent if the value is an array. ETag is calculated by calling the etag method, a combined hash in case of an array.
This also means adding an etag method to your ActiveRecord. One is provided by default and will calculate a unique tag from the object’s attributes, or when using optimistic locks, the record’s id and version column.
Content type is also included in the ETag hash, so two representations of the same resource will not conflict in the cache. But as a side note, if you are using the same action to serve a page and a partial (for XHR), you’ll get two ETags and neither copy will be cached, so try using the .js suffix for the XHR URL.
Code is over here, and to install:
./script/plugin install http://labnotes.org/svn/public/ruby/rails_plugins/if_modified

Labnotes » if_modified: infinite cache size and optimistic locks
Labnotes » Rounded Corners 213 – LaunchPad Chicken