End Point

News

Welcome to End Point's blog

Ongoing observations by End Point people.

A Cache Expiration Strategy in RailsAdmin

I've been blogging about RailsAdmin a lot lately. You might think that I think it's the best thing since sliced bread. It's a great configurable administrative interface compatible with Ruby on Rails 3. It provides a configurable architecture for CRUD (create, update, delete, view) management of resources with many additional user-friendly features like search, pagination, and a flexible navigation. It integrates nicely with CanCan, an authorization library. RailsAdmin also allows you to introduce custom actions such as import, and approving items.

Whenever you are working with a gem that introduces admin functionality (RailsAdmin, ActiveAdmin, etc.), the controllers that provide resource management do not live in your code base. In Rails, typically you will see cache expirations in the controller that provides the CRUD functionality. For example, in the code below, a PagesController will specify caching and sweeping of the page which expires when a page is updated or destroyed:

class PagesController < AdminController
  caches_action :index, :show
  cache_sweeper :page_sweeper, :only => [ :update, :destroy ]

  ...
end

While working with RailsAdmin, I've come up with a different solution for expiring caches without extending the RailsAdmin functionality. Here are a couple of examples:

Page Caching

On the front-end, I have standard full page caching on static pages. In this case, the config/routes.rb maps wildcard paths to the pages controller and show action.

match '*path' => 'pages#show'

The controller calls the standard caches_page method:

class PagesController < ApplicationController
  caches_page :show

  def show
    @page = Page.find_by_slug(params[:path])
    
    ...
  end
end

A simple ActiveRecord callback is added to clear the page cache:

class Page < ActiveRecord::Base
  ...

  after_update :clear_cache

  def clear_cache
    ActionController::Base.expire_page("/#{self.slug}")
  end
end

Fragment Caching

When a page can't be fully cached, I might cache a view shared across the application. In the example below, the shared view is included in the layout – it's generated dynamically but the data does not change often, which makes it suitable for fragment caching.

<% cache "navigation" do -%>
  <% Category.each do |category| -%>
    <%= link_to category.name, category_url(category) %>
  <% end -%>
<% end -%>

Inside the model, I add the following to clear the fragment cache when a category is created, updated, or destroyed:

class Category < ActiveRecord::Base
  after_create :clear_cache
  after_update :clear_cache
  before_destroy :clear_cache

  def clear_cache
    ActionController::Base.new.expire_fragment("navigation")
  end
end

Conclusion

One thing that's noteworthy is that expire_page requires a class method on ActionController::Base while expire_fragment requires an instance method (see here versus here). Action cache expiration with ActiveRecord callbacks should work similarly with action caching, as a class method (reference).

An alternative approach here would be to extend the generic RailsAdmin admin controller to introduce a generic sweeper. However, the sweeper would have to determine what model was modified and what to expire it. This can be implemented and abstracted elegantly, but in my application I preferred to use simple ActiveRecord callbacks because the caching was limited to a small number of models.

3 comments:

Anonymous said...

Just to add: for Active Admin it's apparently quite easy to add to the controller to allow page expiration: https://github.com/gregbell/active_admin/issues/530

Haven't tried to verify jet btw...

Steph Skardal said...

That's great for Active Admin.

There's not an easy way to add it in RailsAdmin from reading through the documentation, other than the method described in this article.

Anonymous said...

Hi, thanks for the article. I tried your approach in Rails 4 and couldn't get it to work.

I haven't spent much time investigating, but I believe the problem is the following. Rails 4 stores each each cached fragment with a hash appended, like so:

views/1383234242.8060944/7711b13ad82e2fbf633ef74d0a21bd41

For some reason, when using ActionController::Base.new.expire_fragment, no such hash is generated. (Maybe because a new instance of ActionController is used, instead of a "real" controller.)

Either way, I did this to get fragment caching to work with Rails Admin in Rails 4:

https://gist.github.com/sepastian/7252012