1. Sep 5th, 2007

    if_modified: infinite cache size and optimistic locks

    As far as Web servers go, life is simple. Caching either happens on the server, or out there on the client.

    Server-side caching works well if you’re serving the same content to a lot of clients — Web speak for browsers and anything else with an HTTP library — and generally each client makes requests slower than data changes. But if you’re serving individual content to each client, soon enough you’re going to run out of cache space.

    You can either buy more memory, or have the clients cache on their side, which opens up a new world of infinite cache size. Super.

    HTTP has two methods for caching. If you know when changes are about to happen, you can tell the client how long to cache data using Expires or Cache-Control max-age. They work extremely well, as long as you can make an educated guess. But if you’re wrong, and users start asking where’s that item I just added to my order, then it ends up worse than not caching.

    This article involves no guess work. Instead, we’ll use conditional GET.

    Conditional GET

    Basically, get(request) if unmodified.

    First time the client makes a request, you send back the response along with two headers. Either or both, we’ll get to talk about the difference later on. The first header is Last-Modified and contains a timestamp of the last modification. The second header is ETag and contains a hash, we’ll use MD5 here, of the content.

    There’s also Cache-Control which tells the client it must-revalidate, important but that’s all I have to say about it.

    The second time the client makes a request, it sends along the value of Last-Modified in the If-Unmodified-Since header, and the value of ETag in the If-None-Match header. Again either or both, depending on which headers it got in the first request.

    At this point the server does a quick comparison and decides what to do next. That’s where it gets to be conditional. If it notices that the data has updated between the two requests, it serves up a new response along with new Last-Modified and ETag headers. If it knows the data is the same, it doesn’t bother creating a response, and instead sends back the status code 304 (Not Modified). That tells the client to pick up the response from its cache.

    The payoff? Instead of rendering and sending a 50KB page, which you already sent once, you get to answer the second request with a few headers, all neatly packed into a single TCP packet.

    Fast is what we’re looking for here.

    Rails ETag vs if_modified

    Rails 2.0 already has a feature that uses ETag to great effect. At the end of each request it calculates an ETag from the response. The first time it will send the entire response, the second time it sees that the ETag matches the If-None-Match headed and sends back just 304.

    By not sending the entire request the second time around, it gets both server and client off the hook early on. In quantitative terms, you get better throughput on the server by turning around requests faster, and better response time on the client. It can also lower your bandwidth bill.

    So what’s not to love? Well, you’re still rendering the entire response before Rails can decide whether to send it all, or just the headers. So there’s a hit on the database, there’s controller logic, there’s view rendering going on. We want to do better.

    Also, would be cool to have conditional PUT. More about this later.

    if_modified works it magic before serving the request. It uses the data for the request — some, not all, and that’s an important distinction — to decide whether there’s any difference between what the client saw before, and what we would be serving if we didn’t know any better.

    If there’s a difference, it goes right ahead and renders the new response, and will include Last-Modified and ETag headers. If it’s all the same, it sends back a 304 without passing through view, without collecting $200.

    # Render only if modified since last GET/PUT.
    def show()
      @item = Item.find(params[:id])
      if_modified @item do
        render :action=>”item”
      end
    end

    You still need to load something, but that’s far less work than running the entire controller/view logic. And you only need to load that which signifies change. Say my blog page includes the last 30 posts, with comments, categories for the side bar, and all other good things. But I only need to grab the last published post to decide between content and 304.

    It’s not free, but it’s a hell of a lot cheaper.

    Conditional PUT

    A what?

    Let me introduce you to another nifty feature of HTTP. It’s commonly known as conditional PUT, but its “what do I get out of this?” nickname is optimistic concurrency. For HTTP.

    Here’s how it works. The GET part is the same as before, client gets Last-Modified and/or ETag and holds on to them. Client them makes some changes to the data on its side, doesn’t know or care what happens on the server. Next, happy with the new values, it updates the server with a PUT. But this time, it sends back Last-Modified as If-Unmodified-Since, and ETag as If-Match.

    Like before, the server looks at these headers, compares them to what it knows, and decides. But now it makes a different decision. If it detects no change, both client and server agree that the original data is the same, it does the update. If it detects a change, which happens if someone updated the data before the client got around to doing a PUT, it sends back 412 (Precondition Failed).

    In response, the client can give up, but it can also GET the data again, make the same change, and PUT back a new update. Such conflicts are rare, so we’re not too concerned about the performance cost. And we’re no longer concerned about race conditions, where one update overwrites the other, or end up with an inconsistency.

    Basically, if you ever stopped to wonder “how do these people manage using HTTP without transactions?” or were just about to invent your own HTTP-TX protocol, most likely you just needed a conditional PUT.

    # Update only if not modified since last GET/PUT.
    def update()
      @item = Item.find(params[:id])
      if_unmodified @item do
        @item.update_from params
        render :action=>”item”
      end
    end

    Sources of Influence

    For me that would be the HTTP protocol, but this section is about influencing if_modified and if_unmodified.

    You feed them any number of arguments, arrays and nils are just fine, they all get flattened and compacted. What matters is the list of objects after compacting, which may be empty. And if it is empty, there is no Last-Modified, and ETag is calculated from nil.

    If there are objects to deal with, Last-Modified is calculated as the last (most recent) timestamp by calling updated_on or updated_at on each argument. That you already have, if your ActiveRecords use timestamps.

    The ETag is calculated by calling etag on each of the objects, we’ll get to that in a second, but only if all the objects respond to that method. Important distinction, calling ETag on some argument would be a weak tag which is not currently supported.

    All the values are then combined with the response format to create a hash. The hash depends on the order, so two lists of objects are identical only if they contain the same objects in the same order. Consistency. The response format is used to guarantee that if you’re asking for HTML first, and XML later, you don’t get back a 304. Semantically they’re the same, but the responses are different.

    The implementation assumes ETag is a strong identity check, so you want something that changes when the object changes. You can implement the etag method yourself, but this is about making life easier, so there’s also an etag method on ActiveRecord::Base.

    Typically it just runs over all the attributes and calculates a signature from the name/value pairs. If you’re using optimistic locks, it gets lazy and just combines the record ID with lock_version. Either way, it will also include all eager loaded associations.

    Do We Need Both?

    No. But Last-Modified is quicker to calculate so it’s used first, and typically will be enough to reach a decision without having to calculate ETag.

    Sometimes not. Obviously it cannot detect sub-second modifications so that’s one reason to use both. Another one are associations. Say you created a relation between Manager and Employee, without modifying either one. So the timestamps are still the same, but the ETag calculated from the association is different.

    Odds & Ends

    Both if_modified and un_modified will send back new headers along with the response. It’s very rare, but if you need to send back headers from a different set of arguments, or want to roll your own, you can always call modified yourself.

    Another good to know. Both If-Match and If-None-Match can take the special value ‘*’. In a GET, that will render a response if there are any objects, and 304 if there are none. Theoretically useful, but I’ve yet to establish for what.

    In a PUT, this will perform the update only if there is no object, otherwise return a 412. That’s equivalent to saying “please create this resource if it doesn’t exist, but don’t change it if it does”. An idempotent way to create new resources, and the subject of another interesting feature called create_once. But more about that in a later post.

    Getting It

    if_modified is part of restfully_yours, once you install the plugin all these are added to your ActionController/ActiveRecord:

    ./script/plugin install http://labnotes.org/svn/public/ruby/rails_plugins/restfully_yours

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

    Flexible Rails
    • Flex 3 and Ruby on Rails 2 integrated with HTTPService and XML
    • RESTful Rails controllers that support Flex and HTML clients
    • Coverage of how to use Cairngorm to architect larger Flex applications
    • A full application--not just a toy--developed and refactored iteratively
    1. Sep 6th, 2007

      Chris Anderson

      Great article! This is probably the clearest explanation of ETags and Last-Modified as it pertains to Rails apps I’ve seen. I don’t recall seeing it in the article, so I’ll use the phrase “Deep ETags” for teh goog. I can’t wait for your writeups of the remaining restfully_yours features.

    2. Sep 7th, 2007

      Assaf

      Thanks, Chris.

    3. Sep 10th, 2007

      Morning Brew #73

      [...] if_modified - infinite cache size and optimistic locks. [...]

    4. Oct 8th, 2007

      Labnotes » n-Tier Apps and Client-Side Caching

      [...] if_modified. Now that you’re bona fide caching all over the place, or as often as you possibly can, you’ll want to eliminate those round trips to the database by checking for changes before doing any work. Something like if_modified: [...]

    5. Mar 21st, 2008

      From draft to launch: Helpful tools in Rails development (3rd tranche of Rails links) « Arnold Funken on social site building

      [...] ETags, a HTTP/1.1 feature to reduce the number of delivered web pages to a minimum. Chad Fowler and Assaf Arkin introduce you into the matters of ETags in two distinct but nevertheless brilliant [...]

    Leave a Reply | Trackback | Track with co.mments

    Where's my comment? I get too much comment spam, so I have to moderate comments. Damn those spammers. If you don't see your comment immediately, be patient. I'll approve it the minute I see it. Want to know when your comment shows up, or check if anyone responded? Track it.

    Or using OpenID