Lazy proxy in Ruby

I’m a total newbie when it comes to Ruby evaluation tricks, so when I learned this today I felt it was a good thing to share :-)

The problem: speeding up a Rails application. When all is said and done, you need to cache page fragments in order to speed up an application significantly. For instance: you start with

class ProductsController < ApplicationController
  def category
    @products = Product.find_by_category(params[:id])
  end
end

...

<div id="products">
  <% for product in @products do %>
    <!-- some complicated html code -->
  <% end %>
</div>

and then add fragment caching in the view with

<% cache "category-#{params[:id]}" do %>
  <div id="products">
    <% for product in @products do %>
      <!-- some complicated html code -->
    <% end %>
  </div>
<% end %>

OK, this speeds up view rendering. But we are still executing the query in the controller, to obtain a list of products we are not even using. The standard Rails solution to this is

  class ProductsController < ApplicationController
    def category
      unless fragment_exist? "category-#{params[:id]}"
        @products = Product.find_by_category(params[:id])
      end
    end
  end

This is nice enough. But one things is worrying me, is there might be a race condition between the “unless fragment_exists?” test and the call to “cache” in the view. If the cron job that cleans the cache directory executes between the two, the user will see an error.

I thought to myself, wouldn’t it be nice to give the view a lazy proxy in place of the array of results? The lazy proxy will only execute the query if it is needed. The controller becomes:

class ProductsController < ApplicationController
  def category
    @products = LazyProxy.new do
      Product.find_by_category(params[:id])
    end
  end
end

The LazyProxy magic is surprisingly simple:

class LazyProxy < Delegator
  def initialize(&block)
    @block = block
  end

  def __getobj__
    @delegate ||= @block.call
  end
end  

The block given to the constructor is saved, and not used immediately. The Delegator class from the standard library delegates all calls to the object returned by the __getobj__ method. The “||=” trick makes sure that the result of @block.call will be saved in an instance variable, so that the query is executed at most once.

So the idea is that the view will be given a lazy proxy for a query. If the fragment exists, the view code will not be evaluated and the proxy will not be used. No query. If the fragment does not exist, the lazy proxy is used and a query is executed. There is no race condition, for there is no test to see if the fragment exists.

What do you think?

Update One additional advantage of the lazy proxy is that you no longer need to make sure that the fragment key is the same on both view and controller.

4 Responses to “Lazy proxy in Ruby”

  1. Paolo "Nusco" Perrotta Says:

    I love lazy proxies. :)
    Your example, however, may contain a bug (either that, or I need more sleep): it only checks the :id parameter on the first call to category(). From the second time on, it still returns the @products for the first :id. Maybe you can use a hash to bind ids to products?

    At that point, though, you basically have a cache that never expires. If you come up with ways to make it expire, then you’ll be to be careful about race conditions again. This is not to detract from your very elegant solution – just be careful. :)

  2. matteo Says:

    Hi Paolo,

    thanks for your comment. I don’t think there is a bug, as for every request a new proxy will be created. Either that, or I misunderstand you :-)

    It turns out, however, that the lazy proxy behaviour is now standard in Rails 3. This technique might still be useful for Rails 2 though.

  3. Renzo Says:

    Elegant solution that works!

    I was thinking that maybe you’re better off expiring the cache when a new product is added to the category instead of blindly let the cron doing it (unless you know categories are always updated at some point in time). You can use an after_update observer on the Category model, you can check if there are changes in the products association and invalidate the cache. If you do this, you don’t need the lazy proxy. But as I said, it depends on the situation. Good luck!

  4. matteo Says:

    Hi Renzo,

    thanks! My reasoning is that the performance hit from erasing the cache every, say, 15m should not be that much. And I like very much the peace of mind of knowing that, no matter what, the cache will not stay out-of-date for long.

    The downside is that the admin people will not be able to see their modification immediately and they might think the application is buggy. Perhaps the best solution would be to use both cron job and after_update observer?

Leave a Reply