Wednesday, October 10, 2007

timed out fragment caching in Rails

As you can probably tell I am a little into optimizations. In Rails, caching is one of those things that is made pretty easy, but can also be difficult when you want to do complex tasks with the caching -- such as expiring it. There are two ways with expire_fragment within the logic of your controllers or using an Observers. The Observers are useful, but require that much little effort to set things up, which personally I don't want to maintain. The expire fragment was my next target, but yet again a struggle because I had to write logic for it in certain places.

Hence the creation of my next caching mechanism. Rails supports many types of caching like file, memory, memcache, etc. The problem I have found is that memcache is the only that supports auto timeouts of the cache data. Tell me why I cannot have the same functionality built into Rails for the other caching mechanisms? I want to cache fragments in local memory of the server, which require quick access, and are small enough where they don't need to be shared like memcache.

For example I want to cache a fragment for 5 minutes on a page:


This is last 5 minutes of posts from our forums:
<%cache_timeout('forum_listing',5.minutes.from_now) do %>
<ul>
<% for post in @posts %>
<li><b><%=post.title%></b> - by <%=post.author.username%> at <%=post.created_at></p></li>
<%end%>
</ul>
<%end%>


This fragment will timeout after the page that contains it is loaded after the 5 minute mark and then will be reloaded. As you will noticed @posts are loaded, so is_cache_expired? method is provided in the controller and view to make sure if the fragment hasn't expired. This allows data to loaded only when needed.

The following is the code written for the timeout caching. Check back soon for plugin location.

module ActionController
module Cache
module TimedCache
#used to store the associated timeout time of cache key
@@cache_timeout_values = {}
#handles standard ERB fragments used in RHTML
def cache_timeout(name={}, expire = 10.minutes.from_now, &block)
unless perform_caching then block.call; return end
key = fragment_cache_key(name)
if is_cache_expired?(key,true)
expire_timeout_fragment(key)
@@cache_timeout_values[key] = expire
end
cache_erb_fragment(block,name)
end
#handles the expiration of timeout fragment
def expire_timeout_fragment(key)
@@cache_timeout_value[key] = nil
expire_fragment(key)
end
#checks to see if a cache has fully expired
def is_cache_expired?(name, is_key = false)
key = is_key ? fragment_cache_key(name) : name
return (!@@cache_timeout_values.has_key?(key)) || (@@cache_timeout_values[key] < Time.now)
end
end
end
end


module ActionView
module Helpers
module TimedCacheHelper
def is_cache_expired?(name = nil)
return false if name.nil?
key = fragment_cache_key(name)
return @controller.send('is_cache_expired?', key)
end
def cache_timeout(name,expire=10.minutes.from_now, &block)
@controller.cache_timeout(name,expire,&block)
end
end
end
end

#add to the respective controllers
ActionView::Base.send(:include, ActionView::Helpers::TimedCacheHelper)
ActionController::Base.send(:include, ActionController::Cache::TimedCache)


If anyone has any suggestions or comments please feel free to leave them in the comments.

Update: This is the link to the plugin.

7 comments:

Matt Allen said...

Gday;

I've taken a slightly different approach in the fact that my cache re-caches itself out of bandwidth.

Blog post is over here

JT said...

Matt: I was reading through your blog entry you provide. From my perspective, it seems that you did alot of extra work in expanding the cache method to handle expiry and also having the same logic in your controller. Why not have the logic in the controller and expire the fragment from there instead within the View call?

Delameko said...

Hi mate,
this is just what I've been looking for, thanks a lot.

There's an error in the source code on Google though. The @@cache_timeout_values in the expire_timeout_fragment method is missing the s at the end.

Had me scratching my head for a while, but alls good now. Thanks.

jt archie said...

delameko: Noted and fixed. This is why I need to learn to write tests for my code before I submit it. :)

Anonymous said...

Pretty awesome work dude.

Aaron Diers said...

Having trouble with this after upgrading to Rails 2.0. Any thoughts?

jt archie said...

Currently there have not been any plans to upgrade the code to Rails 2.1 and above. Only because I no longer use it myself. I might be able to take a look at it this weekend. rails 2.1> change the way caching is handle, so my plugins have been outdated.