Fragement Caching Tip

Posted by Jonathan

We are in the process of testing the new version of MeinProf.de and crossed a very interesting bug.

We are using fragment caching to speed up the serving of the university homepages. So far this is working great and helps a lot with coping with extra load.

The university homepages are served by unis/show.html.erb:

  ...
  <%= render :partial => 'shared/user_info' %>
  ...
  <% cache "uni_page_homepage_#{@uni.id}", :expires_in => 1.hour do %>
    <%= render :partial => 'unis/header' %>
    <%= render :partial => 'tabs_header' %>
    <div class="uni_desc">
      <div class="tab-content">
       ...
       <!-- a lot of uni content here-->
  <% end %>

The partial unis/_header.html.erb looks like this:

  ...
<% content_for("breadcrumb") do %>
  <%= uni_breadcrumb(uni, department) %>
  <div class="breakcrumb">
    ...
  </div>
<% end %>

<div class="subcolumns">
  <h1><%=h uni.name %></h1>
  <div class="c08l badge">
  <p>
    <strong>Homepage:</strong>....
   ....
  </p>

So far so good. When we deployed to our staging host we noticed that the university header was not always right. On the first request it was correct and on every subsequent request the header was missing the breadcrumb. Errors like this sounds like caching issues and it took us a while to figure it out.

The problem is the content_for block inside the partial. Once you think about it, it makes a lot of sense. During the first request the show-template gets evaluated. It calls the partial and the partial executes the content_for block. This block access the template binding/variables to inject content. The partial finishes rendering and stores the rendered content in memcached. Everything fine.

On the next request Rails loads up the action and renders the view. During the view rendering the fragment will be loaded from memcached. No bug here.

So why is the breadcrumb not showing up? Because the content_for block will not be executed. It is a side effect of the partial-rendering and the partial will not be rendered once it is cached. Only the resulting HTML-fragment will be loaded from the cache but the code-block is not executed. This means that there is not call to inject extra content to the main view.

So before you wrap some view code in fragment caching remember: do not fragment cache content_for-blocks. They will not be called. Always check cached views for content_for-blocks and move them outside the cache-call.

Time-based Fragment Caching with MemCache

Posted by Jonathan

For the sidebar content in MeinProf.de we use fragment caching. One problem with caching is that expiring entries can get really messy. Time-based caching can solve this problem but the current caching implementation in Rails does not support this.

While poking around in the MemCache-client implementation from the Robotcoop I saw that MemCache itself does support time-based expiry of cached entries. Thanks to Ruby I just re-implemented the write method in ActionController::Caching::Fragments::MemCacheStore so that we could expire our entries after some given time:

class ActionController::Caching::Fragments::MemCacheStore
<br />  def write(name, value, options=nil)
<br />    @data.set(name, value, 30.minutes)
<br />  end
<br />end

Now all fragments expire after 30 minutes. If you want to have different live-times for your caches you have to distinct by the name of the fragment. Normally fragments created with the <% cache do > call in views are named after the controller and action, e.g. controller/actionname. You can also specify a name like < cache(“UniPage_#{uni.id}”) do %>.

<blockquote>
class ActionController::Caching::Fragments::MemCacheStore
<br />  def write(name, value, options=nil)
<br />    if name =~ %r{^UniPage}
<br />      @data.set(name, value, 30.minutes)
<br />    elsif name == "mycontroller/myaction" 
<br />      @data.set(name, value, 45.minutes)
<br />    else
<br />      @data.set(name, value, 60.minutes)
<br />    end
<br />  end
<br />end

Not the cleanest solution but it works very well for us.

If you want to also save your sessions in MemCache with the memcache-client library from the Robotcoop, add this code to ActionController::Caching::Fragments::MemCacheStore:

class ActionController::Caching::Fragments::MemCacheStore
<br />  def data=(cache)
<br />    @data = cache
<br />  end
<br />end

In summary our code in config/environments/production.rb looks like this:

### MemCached Server ###
<br />CACHE = MemCache.new :c_threshold =&gt; 10_000, :compression =&gt; true,\<br /> :debug =&gt; false, :namespace =&gt; 'meinprof_de', :readonly =&gt; false, :urlencode =&gt; false<br />
<br />CACHE.servers = '127.0.0.1:11211'
<br />
<br />### Sessions in MemCached ###
<br />session_options = {
<br />    :database_manager =&gt; CGI::Session::MemCacheStore,
<br />    :cache =&gt; CACHE
<br />}
<br />
<br />ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS.update session_options
<br />
<br />### FragmentCaching im MemCached ###
<br /># Allow us to set the CACHE as the Fragment Cache store
<br />class ActionController::Caching::Fragments::MemCacheStore
<br />  def data=(cache)
<br />    @data = cache
<br />  end
<br />  
<br />  def write(name, value, options=nil)
<br />    if name =~ %r{^Random_TopFlop}
<br />      @data.set(name, value, 30.minutes)
<br />    elsif name =~ %r{^RegionPage}
<br />      @data.set(name, value, 60.minutes)
<br />    elsif name =~ %r{^UniPage}
<br />      @data.set(name, value, 60.minutes)    
<br />    else
<br />      @data.set(name, value, 120.minutes)
<br />    end
<br />  end
<br />end<br />
<br />ActionController::Base.fragment_cache_store = :mem_cache_store ,{}
<br />ActionController::Base.fragment_cache_store.data = CACHE