1. Dec 14th, 2007

    if_modified, second round

    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
    1. Dec 14th, 2007

      Labnotes » if_modified: infinite cache size and optimistic locks

      [...] Update: Moved to its very own plugin, with an updated API. [...]

    2. Dec 16th, 2007

      http://eric.wahlforss.com/

      Looks really good! Will definitely try putting this plugin into use.

    3. Mar 4th, 2008

      http://www.technorati.com/people/technorati/matlin

      Why is it so hard for the Rails community to say things like “This works with Rails 2.0″?

      We are not running 2.0 and therefor does not have extract_options! or slice at our fingertips.

      Really annoying since I can’t get the plugin to work.
      All I get is nil.
      I want it to work!

    4. Mar 4th, 2008

      Assaf

      Well, the second version of if_modified (this one) was released 7 days after Rails 2.0 hit the streets. I didn’t test it with any other version.

      Why not copy extract_options! and slice from 2.0 to an older version?

    5. Apr 15th, 2008

      Tim

      Does if_unmodified work with DELETE?

    6. Apr 16th, 2008

      Assaf

      @Tim, it would definitely work with DELETE, though I haven’t seen anyone use conditional DELETE before. What did you have in mind?

    7. Apr 17th, 2008

      Tim

      I have a RESTian protocol for sync’ing data about a user between two separate systems. The client runs at the “slave”. It does a GET to /distribution/users.xml to fetch a list of user URIs. For each URI, it does a GET, e.g., /distribution/user/1.xml, to fetch the user’s data to sync. When it’s done it does a DELETE to remove that resource from the list of users who need sync’ing (the action just clears a flag on the user record, but from this protocols perspective the resource is gone). The DELETE is conditional so the flag is only reset if there hasn’t been a further change that requires additional sync’ing. Hence my interest in conditional DELETE.

      One other question: am I right in thinking that checking for HTTP_IF_MATCH will only work if you are dispatching via FCGI? If you’re using HTTP all the way back to a mongrel, don’t you need to look for If-Match – or does Rails handle that difference automatically?

      Thanks!

    8. Apr 18th, 2008

      Assaf

      Interesting, thanks for sharing.

      The headers used by Rails are always uppercase, those that come from the HTTP request are prefixed with HTTP_. I don’t know if it’s Rails or Mongrel that handle the conversion, but it’s the same name, whether or not you’re using Mongrel.

    9. May 15th, 2008

      dkubb

      Assaf, I used the earlier version of your plugin to implement Conditional DELETE with the Lypp API (http://lypp.com/api). It also handles Conditional GET and PUT too.

    10. Aug 13th, 2008

      Labnotes » Rounded Corners 213 – LaunchPad Chicken

      [...] Getting there. Rails finally gets conditional GET. Bad pun, what can I say. It’s still horribly verbose by Ruby standards, and does nothing useful for PUT request, but progress it is. Maybe by year-end I’ll be able to deprecateĀ if_modifed. [...]

    Your comment, here ⇓