UPDATE: Since this article was published I released a series of Free Scaling Rails Screencasts which show how to implement all flavors of Rails Caching.


This article is the second part of my series about Ruby on Rails Caching. If you have not yet read Part 1 you may be left clueless, so please do so.

In the last article we went over Page Caching, which involves taking the entire content of a page, and storing it in a static html file that gets read outside of the rails application itself. Page Caching is a great fit for the first page of your site or for your new member application form, but what if you need to ensure someone is authenticated or only cache part of a webpage?

This tutorial is going to address Action Caching, Fragment Caching, and even ActiveRecord Caching (only available in Edge Rails) which will complete our tutorial of caching in Rails.

Table of Contents

  1. Why teach caching in this order?
  2. Action Caching
  3. Cleaning the Action Cache
  4. Fragment Caching
  5. Cleaning the Fragment Cache
  6. Paginating with the Fragment Cache
  7. Advanced Naming with the Fragment Cache
  8. Fragment Cache Naming Examples
  9. Sweeping Multiple Fragments at the Same Time
  10. Where else can I store my Action/Fragment Cache?
  11. ActiveRecord Query Caching
  12. Advanced Caching Alternatives
 

Why teach caching in this order?

Well, when you go to implement caching you're going to want to use the fastest one possible, and cache the most information. So basically:

  1. Page Caching - Fastest
  2. Action Caching - Next Fastest
  3. Fragment Caching - Least Fast

So, the higher you can get your pages up on the list, the more that gets cached, the faster your page runs! Lets get on with it:

 

Action Caching

Action caching is VERY similar to page caching, the only difference is that the request for the page will always hit your rails server and your filters will always run. To setup action caching our controller might look like this:

1
2
3
4
class BlogController < ApplicationController
  layout 'base'
  before_filter :authenticate  # <--- Check out my authentication
  caches_action :list, :show

As you can see here, the user must be authenticated in order to view the list action. When we initially request the list action, we might see the following in our /log/development.log:

1
2
3
4
5
6
7
Processing BlogController#list (for 127.0.0.1 at 2017-03-04 12:51:24) [GET]
 Parameters: {"action"=>"list", "controller"=>"blog"}
Checking Authentication
Post Load (0.000000) SELECT * FROM posts ORDER BY created_on LIMIT 10
Rendering blog/list
Cached fragment: localhost:3000/blog/list (0.00000)
Completed in 0.07800 (12 reqs/sec) | Rendering: 0.01600 (20%) | DB: 0.00000 (0%) | 200 OK [http://localhost/blog/list]

See the line "Cached fragment: localhost:3000/blog/list"? What that means is that a file was generated, and you can find it here:

/tmp/cache/localhost:3000/blog/list.cache

By default, action caching will cache your files at /tmp/cache/, and instead of writing out .html files as in page caching, it will write out .cache files. The path also includes the host & port name (localhost:3000) just in case you're hosting more then one subdomain on a single application. In this case each subdomain can have a different cache location.

If you opened this "list.cache" file, you'd find the complete static html of the page, just like you would if we were page caching. What's the difference then?

If we go and request the page again (after we got it cached above), here is what we'd see in /log/development.log:

1
2
3
4
5
Processing BlogController#list (for 127.0.0.1 at 2017-03-04 13:01:31) [GET]
 Parameters: {"action"=>"list", "controller"=>"blog"}
Checking Authentication
Fragment read: localhost:3000/blog/list (0.00000)
Completed in 0.00010 (10000 reqs/sec) | DB: 0.00000 (0%) | 200 OK [http://localhost/blog/list]

As you can see, our Authentication before_filter was run, then the cached action was read, and outputted to the screen. So in this case we can have a completely cached html page (FAST) and our application can still run our before_filters such as authentication.

It's important to note here that your before_filters must be listed before your caches_action code, at the top of your controller, otherwise you may end up caching the wrong html.

 

How do I clean up the Action Cache?

As we learned in the previous tutorial to regenerate the cache (if our data changes) we need to expire the cache. We can do the same thing with our action caching using sweepers. We'd just want to change "expire_page" to "expire_action" in our /app/sweepers/blog_sweeper.rb.

1
2
3
4
5
# Expire the list page now that we posted a new blog entry
expire_action(:controller => 'blog', :action => 'list')
    
# Also expire the show page, incase we just edited a blog entry
expire_action(:controller => 'blog', :action => 'show', :id => record.id)

Another notable way we can clear out not only our Action Cache and Fragment cache is to run the following rake task:


rake tmp:cache:clear

This will clear out all of our .cache files

 

Fragment Caching

So far we've been dealing with caching entire pages of information. Obviously this can't always be done when you have dynamic webpages, this is where Fragment caching comes in. Fragment caching allows you to cache a portion of one of your views.

To fragment cache a list of blog entries we could edit the /app/views/blog/list.rhtml:

1
2
3
4
5
6
7
8
<strong>My Blog Posts</strong>
<% cache do %>
  <ul>
    <% for post in @posts %>
       <li><%= link_to post.title, :controller => 'blog', :action => 'show', :id => post %></li>
     <% end %>
  </ul>
<% end %>

The "cache do" will create a fragment cache: /tmp/cache/localhost:3000/blog/list.cache, using the current controller and action to name it. Then the next time the "cache do" statement is hit, our previously cached data will be loaded. Lets take a look at what our /log/development.log looks like for the first and second requests of this page. Pay attention now:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Processing BlogController#list (for 127.0.0.1 at 2017-03-17 22:02:16) [GET]
Authenticating User
  Post Load (0.000230)   SELECT * FROM posts
Rendering blog/list
Cached fragment: localhost:3000/blog/list (0.00267)
Completed in 0.02353 (42 reqs/sec) | Rendering: 0.01286 (54%) | DB: 0.00248 (10%) | 200 OK [http://localhost/blog/list]


Processing BlogController#list (for 127.0.0.1 at 2017-03-17 22:02:17) [GET]
Authenticating User
  Post Load (0.000219)   SELECT * FROM posts
Rendering blog/list
Fragment read: localhost:3000/blog/list (0.00024)
Completed in 0.01530 (65 reqs/sec) | Rendering: 0.00545 (35%) | DB: 0.00360 (23%) | 200 OK [http://localhost/blog/list]

Can you see what might look a little fishy?

The fragment was cached properly on the first hit, and the second hit read the fragment as it should, however, you should notice that the SQL statement to get the posts was run both times! But if we're loading a cached page, we shouldn't need to run the SQL a second time, right?

Only the content between the "cache do" and the "end" are cached with fragment caching, while everything else gets run each time. So one of the many ways we could fix this, is to add a condition inside our /controllers/blog_controller.rb.

1
2
3
4
5
def list
  unless read_fragment({})
    @post = Post.find(:all, :order => 'created_on desc', :limit => 10) %>
  end
end

Now our query should only be ran if our page hasn't been cached. We could have also moved our Post.find method inside of our "cache do" statement, but I kinda believe that your models (your business logic) should never be referenced inside your (views). It's an MVC thing.

 

Wanna take a guess at how we can expire fragment caching?

1
2
# Expire the blog list fragment
expire_fragment(:controller => 'blog', :action => 'list')
no duh.. right?  

Paginating with the Fragment Cache

What if I wanted to make my list paginate, and cache each and every page?

As you probably realized by now, by default the name of the cache is determined by looking at the current controller name, and the action name. So if my controller is "blog" and my action is "list" it will write this cache out to "/localhost:3000/blog/list.cache".

In the case of pagination I want to add to the cache name, in which case my blog_controller.rb might look like this:

1
2
3
4
5
  def list
    unless read_fragment({:page => params[:page] || 1})  # Add the page param to the cache naming
      @post_pages, @posts = paginate :posts, :per_page => 10
    end
  end

I could have wrote "read_fragment({:controller => 'blog', :action => 'list', :page => params[:page] || 1})" but by default rails assumes the first two parameters, so I don't need to write those.

My /views/blog/list.rhtml might look like:

1
2
3
<% cache ({:page => params[:page] || 1}) do %>
     ...  All of the html to display the posts ...
<% end %>

This new caching code will create /localhost:3000/blog/list.page=1.cache when I initially go to /blog/list, then when I go to page two it will create /localhost:3000/blog/list.page=2.cache. Pretty cool!

 

Advanced Naming with the Fragment Cache

Most of the time you're going to want to name your caches, here's another good example:

Lets say my web design included a navigation menu (nav menu) on each page that was customized for each user on the site. Lets say it's a list of project tasks our user must complete:

1
2
3
4
5
6
7
8
<div id="nav-bar">
  <strong>Your Tasks</strong>
     <ul>
         <% for task in Task.find_by_member_id(session[:user_id]) %>
           <li><%= task.name %></li>
         <% end %>
      </ul>
</div>

If this gets listed on every page of the site, I might want to cache it the first time, so that I'm not hitting the database on every page. But in this case the cache needs to be user specific. I might end up doing something like this:

1
2
3
4
5
6
7
8
9
10
<div id="nav-bar">
  <strong>Your Tasks</strong>
     <% cache (:controller => "base", :action => "user_tasks", :user_id => session[:user_id]) do %>
     <ul>
         <% for task in Task.find_by_member_id(session[:user_id]) %>
           <li><%= task.name %></li>
         <% end %>
     </ul>
     <% end %> 
</div>

If my user_id was 1 then this would create a cache /localhost:3000/base/user_tasks.user_id=1.cache. Do I have a controller named "base" with an action called "user_tasks"? NOPE! These controller and action names are actually just used for naming conventions, and don't need to exist anywhere.

 

Fragment Cache Naming Examples

It's also worth mentioning a few more cache naming examples:

1
2
3
4
cache ("turkey") => "/tmp/cache/turkey.cache"
cache (:controller => 'blog', :action => 'show', :id => 1) => "/tmp/cache/localhost:3000/blog/show/1.cache"
cache ("blog/recent_posts") => "/tmp/cache/blog/recent_posts.cache"
cache ("#{request.host_with_port}/blog/recent_posts") => "/tmp/cache/localhost:3000/blog/recent_posts.cache"
 

How do I sweep multiple cache pages at the same time?

The paginating fragment cache example above ends up creating a lot of cached files, and so far the only way we'd know how to clear each page out is to do:

1
2
3
4
expire_fragment(:controller => 'blog', :action => 'list', :page => 1)
expire_fragment(:controller => 'blog', :action => 'list', :page => 2)
expire_fragment(:controller => 'blog', :action => 'list', :page => 3)
.....

But there's an easier way! You can actually use Regex to purge multiple files:


expire_fragment(%r{blog/list.*})

This will find all our blog/list cache files blow them all away. Pretty convenient, however, there are three things you need to keep in mind if you actually use this method:

  1. It will not work with Memcached (Which I will talk about in a moment).
  2. It runs through all of your fragment cache to try to match the regex. So if you have 4,000+ cached fragments, this could seriously lag your system.
  3. Be careful how you craft your regex, you may delete more files then you want.
 

Where else can I store my Action/Fragment Cache?

Page Caching, as I showed in the previous tutorial, stores the caches files ONLY in the File System. There are a few more storage options when it comes to Action and Fragment Caching.

  1. File Store - (DEFAULT) - Stores cached information in files on your harddrive. (in /tmp/cache/ by default)
  2. Memory Store - The Rails Server stores the cached information with it in memory.
  3. DRb Store - Cached pages are stored in a "Distributed Ruby" Process. Basically a separate ruby process that all of your servers can communicate with.
  4. MemCache Store - MemCache is a program designed specifically for high performance memory caching. It's not written in Ruby, but it's damn fast and it's what all the big boys use.

What's great about Ruby, is that we can start with the most basic caching (File Store), and when our website gets famous, we can very easily upgrade to use MemCache.

I think File Store is the best way to learn starting out, and if you want to change where those files are stored you can edit your config/environment.rb:


ActionController::Base.fragment_cache_store = :file_store, "/path/to/cache/directory"
 

ActiveRecord Query Caching

Very recently added as the 4th caching mechanism that will soon ship with rails is "ActiveRecord Query Caching" or "SqlCache". This undocumented caching mechanism is currently only available on Rails Edge, but it's certainly worth learning about.

ActiveRecord Query Caching, does exactly what you might think it does, and there's actually nothing you have to do to turn it on, since it's automatically on by default. Say we have an action that looks like this:

1
2
3
4
5
6
7
8
9
class BlogController < ApplicationController
  def some_complex_thing
      post = Post.find(1)
      
      # .... Do alot of other stuff

      post_again = Post.find(1)
  end
end

Before ActiveRecord Query Caching this sql statement would be run twice against the database. However, with caching it would only be run once, and loaded from cache the second time. Pretty cool! So you no longer have to worry about code that runs the same sql statement in any single action. If the query runs a second time, the result will be loaded by the cache.

I know this example isn't very impressive, but think about complex authentication code. Many times you might have to check multiple types of authentication, and you might have code in 3-4 different places that query's for your member model or permissions. In this case, query caching might be VERY useful. Might also allow you to keep your code a little more modular.

Query Cache is created at the start of a single action, and it will expire at the end of the action. So there is no real "storage" involved, and it should be noted that if any inserts/updates/deletes get run, then the whole cache gets flushed. If you need to cache a piece of data and have it available across multiple actions/users you're best looking at memcached.

 

Advanced Caching Alternatives

There are many other types of caching out there besides the three (and soon to be four) that ship with rails. Here are some of the more notable ones:

Need to cache certain models so that you're not running to the database every time you need it? Geoffrey Grosenbach wrote up a nice tutorial on how to get memcached running and then Robot Co-Op's cached_model plugin.

Need to cache a method from your model? Try this library by Yurii Rashkovskii.

If you're looking for additional functionality out of your action caching, check out the Action_Cache plugin.

I'm sure there are others, please let me know by commenting/emailing me.

Conclusion

Caching is a necessity when it comes to production websites, and the rails guys really make it easy to jump in. As always, please let me know if you see any errors, or have any suggestions to make this tutorial better.

It's also worth mentioning that Peepcode.com has a good caching screencast, if you want to see more examples of page, action, and fragment caching.

Tags: caching rails  | permalink


Comments

Leave a response

DallasMarch 20, 2017 @ 11:58 AM

Is there any easy way to not include the host and port in the cache path? I get all sorts of hosts names and weirdness with it. I only ever want to cache things once, not for each host name such as and mydomain .com and mydomainip etc etc. I hacked up the code in the vendor directory to hardcode it to one host but it seemed like overkill and I was probably missing an easy way to accomplish this. Great tutorials btw thanks!


G BillackMarch 20, 2017 @ 12:07 PM

Dallas, from what I know, you basically have two options:

1. Use strings when you specify the fragment cache: ‘cache (“turkey”)’ won’t use your host/port, and will just map to ”/tmp/cache/turkey.cache”

2. Overwrite the “fragment cache key” method using a plugin. I’ve seen lots of people do this in their own plugins, so it shouldn’t be very difficult.

Anyone else have any other ideas?


pjayMarch 20, 2017 @ 12:09 PM

Another alternative for advanced caching is the content-only caching by Coda Hale. I’ve not tried it yet but seems very interesting if you’re in the dynamic sites business :) It caches only the rendered content of your actions, without the layout. I still have to check if it’s working with Rails 1.2.

By the way, great article.


DallasMarch 20, 2017 @ 12:16 PM

Hey, yeah I ended up writing my own plugin to overwrite that method and hardcode it. It just seemed wrong that I had to do that, figured I was missing an easier way. One thing to note about expire_fragment, if you use the regular expression as an option and you have alot of fragments you will have serious issues. It stats every single file in the cache directory trying to match the name. When you have thousands of fragments like us this will eventually break you. We overwrote that method to take a controller as an option and only apply the regex to that directory instead of the entire cache directory


MatteMarch 20, 2017 @ 02:18 PM

Thanks again for your very very useful tutorial!!


G BillackMarch 20, 2017 @ 02:27 PM

pjay, I was initially going to cover content-only caching, but I couldn’t get it to work with the latest version of rails, and neither could Geoffrey over at peepcode.com.


Jesse NewlandMarch 20, 2017 @ 03:04 PM

Add Amazon’s s3 to the list of places you can store your cache:


pjayMarch 20, 2017 @ 07:02 PM

I’m surprised such a useful plugin hasn’t been ported yet. Did you try to adapt the code? What difficulty did you encounter? I’ll also have a look myself if I can find some time.


Joe BlockMarch 20, 2017 @ 07:32 PM

Nice article!

Greetings from mostly sunny Mountain View.


JMarch 27, 2017 @ 01:09 PM

Awesome tutorial - between this one and Part 1 about page caching, I’m feeling a lot more confident with caching in Rails (and consequently feeling more confident about speeding up Rails performance!)

In case anyone else comes upon this and is still wondering how to override the quirky fragment_cache_key method so that it doesn’t include the hostname.


Sorry, comments are closed for this Post, but feel free to email us with your input. We'd love to hear it.

Blog

Subscribe to RSS Feed SubscribeRSS

Podcast

RSS
Archive

   

Looking for Videos?

FUNNY VIDEOS

SPEAKING

TUTORIALS

Tags

Contact Us

Like what you see? Contact us today to get the ball rolling on your next great idea.


Hosting by Rails Machine