RSS

CEK.io

Chris EK, on life as a continually learning software engineer.

Caching in Rails

Summary: caching temporarily stores data so future requests don’t take as much time. There are multiple caching techniques that can be easily implemented in Rails applications.



Caching/cashing in Rails. I’m sure that’s a pun that’s never been made before.1 In this post, I overview the fundamental types of caching techniques, and walk through some effective ways of using them in Rails.

The first section covers some background on caching. If you prefer to skip that, jump straight to the how-to section.

Caching Background

Caching?

Caching defined:

In computing, a cache (/ˈkæʃ/ kash) is a component that transparently stores data so that future requests for that data can be served faster.

And why cache? According to the RailsGuide on the topic, caching can help with “avoiding that expensive round-trip to your database and returning what you need to return to the web clients in the shortest time possible.” So let’s figure out how to avoid that expensive trip.

Types of Caching

A few types of caching to know (going from highest to lowest level):

  • HTTP Caching: HTTP caching occurs when the browser stores local copies of web resources for faster retrieval the next time the resource is required. It uses HTTP headers to determine if the browser can use a locally stored version of the response or if it needs to request a fresh copy from the origin server.
  • Page Caching: Save entire pages of an application without hitting the stack by returning cached prerendered pages. Good in applications without authentication and other highly dynamic aspects.
  • Action Caching: Works like Page Caching, except the incoming web request hits the Rails stack. This means that before filters (like authentication or other restrictions) can be run on it before the cache is served.
  • Fragment Caching: Allows fragments of view logic, for example partials or other bits of HTML that are independent from other parts, to be wrapped in a cache block and served out of the cache store when the next request comes in.
  • Rails.cache: All cached content except cached pages are stored in the Rails.cache.

Specifics in Rails

Note that I said these go from highes to lowest level. More specifically, these techniques build on top of each other. HTTP and Page Caching are unique in that they do not use Rails.cache, but the rest build on top of each other (Action Caching depends on Fragment Caching, which depends on Rails.cache).

Even more specifically, it is ActiveSupport::Cache that provides the foundation for interacting with the cache in Rails. The ActiveSupport::Cache module includes multiple classes, each a different cache store:2

  • ActiveSupport::Cache::Store
  • ActiveSupport::Cache::MemoryStore
  • ActiveSupport::Cache::FileStore
  • ActiveSupport::Cache::MemCacheStore
  • ActiveSupport::Cache::EhcacheStore
  • ActiveSupport::Cache::NullStore.

MemCacheStore is most popular for large production websites. Custom cache stores can be created by extending ActiveSupport::Cache::Store and implementing the appropriate methods.

How To

How to Cache in Rails

So Rails has a lot of [active] support for caching built in, but how do we use it? I’ll quote Ryan Bates on this one:

Rails’ cache store functionality is very modular. It uses the file system to store the cache by default but we can easily change this to store it elsewhere. Rails comes with several cache store options that we can choose from. The default used to be a memory store which stored the cache in local memory of that Rails process. This issue with this is that in production we often have multiple Rails instances running and each of these will have their own cache store which isn’t a good use of resources. The file store works well for smaller applications but isn’t very efficient as reading from and writing to the hard drive is relatively slow. If we use this for a cache that’s accessed frequently we’d be better off using something else.

This brings us to the memcache store which offers the best of both worlds.

Taking Ryan’s advice, let’s jump straight to ActiveSupport::Cache::MemCacheStore.

Memcached and Dalli

According to its website, Memcached is:

  • Free & open source, high-performance, distributed memory object caching system, generic in nature, but intended for use in speeding up dynamic web applications by alleviating database load.
  • Memcached is an in-memory key-value store for small chunks of arbitrary data (strings, objects) from results of database calls, API calls, or page rendering.
  • Memcached is simple yet powerful. Its simple design promotes quick deployment, ease of development, and solves many problems facing large data caches. Its API is available for most popular languages.

Memcached works well in combination with Dalli, “a high performance pure Ruby client for accessing memcached servers”.


Set-up

If you don’t already have memcached installed (it’s preinstalled on OS X), you can run brew install memcached, and add gem ‘dalli’ to your Gemfile. Dalli is default in Rails 4 (but still needs to appear in the Gemfile).

Next, set up the development config file (/config/development.rb) as follows:

(dalli_development.rb) download
1
2
3
config.consider_all_requests_local       = true
config.action_controller.perform_caching = true
config.cache_store = :dalli_store

Run memcached from the terminal to start a memcached server. If you see DalliError: No server available, this is likely the problem.

You can now open a Rails console (rails c) and run Rails.cache, which should return an instance of DalliStore (like #<ActiveSupport::Cache::DalliStore:0x00000105924a18 …).

At this point, to simulate the caching that one wuld implement on a web application, we can run Rails.cache.write(:cache_key, “cache value”) (should return Cache write: cache_key and a long integer), followed by Rails.cache.read(:cache_key) (should return “cache value”). Of course, :cache_key and “cache value” can be replaced with any symbol (as key) and value you choose.


Actual example

Imagine a web page that is a gallery of images. We want to save ourselves the long trip to the database everytime, and instead cache the images so that they render more quickly. How can we do this?

A simple controller, rendering an index page with all the images. Note line 2—this will store the page in a file-based cache separate from the Memcached cache store. This is because page caching is one of the few caching techniques that down not use the cache store.

(images_controller.rb) download
1
2
3
4
5
6
7
class ImagesController < ApplicationController
  caches_page :index

  def index
    @images = Image.all
  end
end

Layering on fragment caching in addition to or instead of page caching, we can go to the view, call cache, and pass a block. Everything inside the block will now be cached, namely each image. Pay attention to line 3—in addition to simply calling cache, we can pass certain options like expires_in. This helps Memcached serve its purpose as a temporary data store. If the data were intended to be persisted, it should be persisted to a traditional database.

(images_index.html.erb) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<h1>Images</h1>

<% cache "images", expires_in: 5.minutes do %>
  <table id="images">
    <tr>
      <th>Image Name</th>
      <th>Image</th>
    </tr>
  <% @images.each do |image| %>
    <tr>
      <td><%= link_to(image.name, image) %></td>
      <td><img src="<%= image.url %>"></td>
    </tr>
  <% end %>
  </table>
<% end %>

And there you have it! A simple caching solution to a slow-loading page. We could continue even deeper to caching at the model level, but fragment caching at the view level suffices as a simple solution.

Of the five types of caching I introduced at the beginning of this post, I’ve covered three: page caching, fragment caching, and Rails.cache. For more on the other two, see the resources below.

Resources

Some further reading, which informed this blog post:

Footnotes:


  1. It has.

  2. The ActiveSupport::Cache documentation gives a good overview of the entire module and its subclasses.