The Rails 4 Way (2014)
Chapter 17. Caching and Performance
Watch me lean then watch me rock.
—Soulja Boy
Historically Rails has suffered from an unfair barrage of criticisms over perceived weaknesses in scalability. Luckily, the continued success of Rails in ultra high traffic usage at companies such as Groupon has made liars of the critics. Nowadays, you can make your Rails application very responsive and scalable with ease. The mechanisms used to squeeze maximum performance out of your Rails apps are the subject of this chapter.
View caching lets you specify that anything from entire pages down to fragments of the page should be captured to disk as HTML files and sent along by your web server on future requests with minimal involvement from Rails itself. ETag support means that in best-case scenarios, it’s not even necessary to send any content at all back to the browser, beyond a couple of HTTP headers.
17.1 View Caching
ActiveView’s templating system is both flexible and powerful. However, it is decidedly not very fast, even in the best case scenarios. Therefore, once you get the basic functionality of your app coded, it’s worth doing a pass over your views and figuring out how to cache their content to achieve maximum performance. Sometimes, just rendering a page can consume 80% of the average request processing time.87
Historically, there have been three types of view caching in Rails. As of Rails 4, two of those types, action and page caching, were extracted into officially-supported, but separate gems. Even though a consensus is emerging that “Russian Doll” caching using fragment caching is enough, we briefly cover the other two methods here for the sake of completeness:
Page caching
The output of an entire controller action is cached to disk, with no further involvement by the Rails dispatcher.
Action caching
The output of an entire controller action is cached, but the Rails dispatcher is still involved in subsequent requests, and controller filters are executed.
Fragment caching
Arbitrary reusable bits and pieces of your page’s output are cached to prevent having to render them again in the future.
Knowing that your application will eventually require caching should influence your design decisions. Projects with optional authentication often have controller actions that are impossible to page or action-cache, because they handle both login states internally.
Most of the time, you won’t have too many pages with completely static content that can be cached using caches_page or caches_action, and that’s where fragment caching comes into play. It’s also the main reason that these two pieces of functionality were extracted out of core Rails.
For scalability reasons, you might be tempted to page cache skeleton markup, or content that is common to all users, then use Ajax to subsequently modify the page. It works, but I can tell you from experience that it’s difficult to develop and maintain, and probably not worth the effort for most applications. |
17.1.1 Page Caching
The simplest form of caching is page caching, triggered by use of the caches_page macro-style method in a controller. It tells Rails to capture the entire output of the request to disk so that it is served up directly by the web server on subsequent requests without the involvement of the dispatcher. On subsequent requests, nothing will be logged to the Rails log, nor will controller filters be triggered—absolutely nothing to do with Rails will happen, just like the static HTML what happens with files served from your project’s public directory.
1 classHomepageController < ApplicationController
2 caches_page :index
3
4 def index
5 ...
In Rails 4, if you want to use page caching you need to add a gem to your Gemfile:
gem 'actionpack-page_caching'
Next, include the module and specify the folder in which to store cached pages in ApplicationController:
1 classApplicationController < ActionController::Base
2 include ActionController::Caching::Pages
3 self.page_cache_directory = "#{Rails.root.to_s}/public/cache/pages"
4 end
For classic Rails behavior, you may set the page_cache_directory to the public root, but if you don’t, then ensure that your webserver knows where to find cached versions.88
Sample Nginx/Puma configuration file with page caching enabled
1 upstream puma_server_domain_tld {
2 server unix:/path/to/the/puma/socket;
3 }
4 server {
5 listen 80;
6 server_name domain.tld;
7 root /path/to/the/app;
8 location / {
9 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
10 proxy_set_header Host $http_host;
11 proxy_redirect off;
12 # try the $uri, than the uri inside the cache folder, than the puma socket
13 try_files $uri /page_cache/$uri /page_cache/$uri.html @puma;
14 }
15 location @puma{
16 proxy_pass http://puma_server_domain_tld;
17 break;
18 }
19 }
17.1.2 Action Caching
By definition, if there’s anything that has to change on every request or specific to an end user’s view of that page, page caching is not an option. On the other hand, if all we need to do is run some filters that check conditions before displaying the page requested, the caches_action method will work. It’s almost like page caching, except that controller filters are executed prior to serving the cached HTML file. That gives you the option to do some extra processing, redirect, or even blow away the existing action cache and re-render if necessary.
As with page caching, this functionality has been extracted from Rails 4, so you need to add the official action caching gem to your Gemfile in order to use it:
gem 'actionpack-action_caching'
Action caching is implemented with fragment caching (covered later in this chapter) and an around_action controller callback. The output of the cached action is keyed based on the current host and the path, which means that it will still work even with Rails applications serving multiple subdomains using a DNS wildcard. Also, different representations of the same resource, such as HTML and XML, are treated like separate requests and cached separately.
Listing 17.1 (like most of the listings in this chapter) is taken from a dead-simple blog application with public and private entries. On default requests, we run a filter that figures out whether the visitor is logged in and redirects them to the public action if not.
Listing 17.1: A controller that uses page and action caching
1 classEntriesController < ApplicationController
2 before_action :check_logged_in, only: [:index]
3
4 caches_page :public
5 caches_action :index
6
7 def public
8 @entries = Entry.where(private: false).limit(10)
9 render :index
10 end
11
12 def index
13 @entries = Entry.limit(10)
14 end
15
16 private
17
18 def check_logged_in
19 redirect_to action: 'public' unless logged_in?
20 end
21
22 end
The public action displays only the public entries and is visible to anyone, which is what makes it a candidate for page caching. However, since it doesn’t require its own template, we just call render :index explicitly at the end of the public action.
Caching in Development Mode? I wanted to mention up front that caching is disabled in development mode. If you want to play with caching during development, you’ll need to edit the following setting in the config/environments/development.rb file: config.action_controller.perform_caching = false Of course, remember to change it back before checking it back into your project repository, or you might face some very confusing errors down the road. In his great screencast on the subject, Geoffrey Grosenbach suggests adding another environment mode to your project named development_with_caching, with caching turned on just for experimentationhttp://peepcode.com/products/page-action-and-fragment-caching |
17.1.3 Fragment Caching
Users are accustomed to all sorts of dynamic content on the page, and your application layout will be filled with things like welcome messages and notification counts. Fragment caching allows us to capture parts of the rendered page and serve them up on subsequent requests without needing to render their content again. The performance improvement is not quite as dramatic as with page or action caching, since the Rails dispatcher is still involved in serving the request, and often the database is still hit with requests. However, automatic key expiration means that “sweeping” old cached content is significantly easier than with page or action caching. And actually, the best way to use fragment caching is on top of a cache store like Memcached that’ll automatically kick out old entries, meaning there’s little to no sweeping required.89
17.1.3.1 The cache Method
Fragment caching is by its very nature something that you specify in your view template rather than at the controller level. You do so using the cache view helper method of the ActionView::Helpers::CacheHelper module. In addition to its optional parameters, the method takes a block, which allows you to easily wrap content that should be cached.
Once we log in to the sample application reflected in Listing 17.1, the header section should probably display information about the user, so action-caching the index page is out of the question. We’ll remove the caches_action directive from the EntriesController, but leave cache_page in place for the public action. Then we’ll go into the entries/index.html.haml template and add fragment caching, as shown in Listing 17.2:
Listing 17.2 The index template with cache directive
1 %h1#{@user.name}'s Journal
2 %ul.entries
3 - cache do
4 = render partial: 'entry', collection: @entries
Just like that, the HTML that renders the collection of entries is stored as a cached fragment associated with the entries page. Future requests will not need to re-render the entries. Here’s what it looks like when Rails checks to see whether the content is already in the cache:
"get" "views/localhost:3000/entries/d57823a936b2ee781687c74c44e056a0"
The cache was not warm on the first request, so Rails renders the content and sets it into the cache for future use:
"setex" "views/localhost:3000/entries/d57823a936b2ee781687c74c44e056a0"
"5400" "\x04\bo: ActiveSupport::Cache::Entry\b:\x0b@valueI\"\x02\xbbf
<li class="entry">...
If you analyze the structure of the keys being sent to the cache (in this case Redis) you’ll notice that they are composed of several distinct parts.
views/
Indicates that we are doing some view caching
hostname/
The host and port serving up the content. Note that this doesn’t break with virtual hostnames since the name of the server itself is used.
type/
In the case of our example it’s entries, but that spot in the key would contain some indicator of the type of data being rendered. If you do not provide a specific key name, it will be set to the name of the controller serving up the content.
digest/
the remaining hexadecimal string is an MD5 hash of the template content, so that changing the content of the template busts the cache. This is new functionality in Rails 4 that eliminates the need for homebrewed template versioning schemes. Most template dependencies can be derived from calls to render in the template itself.90
Despite the nifty cache-busting behavior of adding template digests to your cache keys automatically, there are some situations where changes to the way you’re generating markup will not bust the cache correctly. The primary case is when you have markup generated in a helper method, and you change the body of that helper method. The digest hash generated for templates that use that helper method will not change, they have no way of knowing to do so. There is no super elegant solution to this problem. Rails core suggests adding a comment to the template where they helper is used and modifying it whenever the behavior of the helper changes.91 |
17.1.3.2 Fragment cache keys
The cache method takes an optional name parameter that we left blank in Listing 17.2. That’s an acceptable solution when there is only one cached fragment on a page. Usually there’ll be more than one. Therefore, it’s a good practice to identify the fragment in a way that will prevent collisions with other fragments whether they are on the same page or not. Listing 17.3 is an enhanced version of the entries page. Since this blog handles content for multiple users, we’re keying the list of entries off the user object itself.
Listing 17.3 Enhanced version of the entries page
1 %h1#{@user.name}'s Journal
2
3 - cache @user do
4 %ul.entries
5 = render partial: 'entry', collection: @entries
6
7 - content_for :sidebar do
8 - cache [@user, :recent_comments] do
9 = render partial: 'comment', collection: @recent_comments
Notice that we’ve also added recent comments in the sidebar and named those fragment cache accordingly to show how to namespace cache keys. Also note the use of an array in place of a name or single object for those declarations, to create a compound key.
After the code in Listing 17.3 is rendered, there will be at least two fragments in the cache, keyed as follows:
views/users/1-20131126171127/1e4adb3067d5a7598ea1d0fd0f7b7ff1
views/users/1-20131126171127/recent_comments/1f440155af81f1358d8f97a099395802
Note that the recent comments are correctly identified with a suffix. We’ll also add a suffix to the cache of entries, to make sure that we don’t have future conflicts.
1 - cache [@user, :entries] do
2 %ul.entries
3 = render partial: 'entry', collection: @entries
4 ...
17.1.3.3 Accounting for URL parameters
Earlier versions of Rails transparently used elements of the page’s URL to key fragments in the cache. It was an elegant solution to a somewhat difficult problem of caching pages that take parameters. Consider for instance, what would happen if you added pagination, filtering or sorting to your list of blog entries in our sample app: the cache directive would ignore the parameters, because it’s keying strictly on the identity of the user object. Therefore, we need to add any other relevant parameters to a compound key for that page content.
For example, let’s expand our compound key for user entries by adding the page number requested:
1 - cache [@user, :entries, page: params[:page]] do
2 %ul.entries
3 = render partial: 'entry', collection: @entries
The key mechanism understands hashes as part of the compound key and adds their content using a slash delimiter.
views/users/1-20131126171127/entries/page/1/1e4adb3067d5a7598ea1d0fd0f7b7ff1
views/users/1-20131126171127/entries/page/2/1e4adb3067d5a7598ea1d0fd0f7b7ff1
views/users/1-20131126171127/entries/page/3/1e4adb3067d5a7598ea1d0fd0f7b7ff1
etc...
If your site is localized, you probably want to include the user’s locale in the compound key so that you don’t serve up the wrong languages to visitors from different places.
1 - cache [@user, :entries, locale: @user.locale, page: params[:page]] do
2 %ul.entries
3 = render partial: 'entry', collection: @entries
As you can tell, construction of cache keys can get complicated, and that’s a lot of logic to be carrying around in our view templates. DRY up your code if necessary by extracting into a view helper, and/or overriding the key object’s cache_key method.
1 classUser
2 def cache_key
3 [super, locale].join '-'
4 end
Object keys As you’ve seen in our examples so far, the cache method accepts objects, whether by themselves or in an array as its name parameter. When you do that, it’ll call cache_key or to_param on the object provided to get a name for the fragment. By default, ActiveRecord and Mongoid objects respond to cache_key with a dashed combination of their id and updated_at timestamp (if available). |
17.1.3.4 Global Fragments
Sometimes, you’ll want to fragment-cache content that is not specific to single part of your application. To add globally keyed fragments to the cache, simply use the name parameter of the cache helper method, but give it a string identifier instead of an object or array.
In Listing 17.4, we cache the site stats partial for every user, using simply :site_stats as the key.
Listing 17.4 Caching the stats partial across the site
1 %h1#{@user.name}'s Journal
2
3 - cache [@user, :entries, page: params[:page]] do
4 %ul.entries
5 = render partial: 'entry', collection: @entries
6
7 - content_for :sidebar do
8 - cache(:site_stats) do
9 = render partial: 'site_stats'
10 ...
Now, requesting the page results in the following key being added to the cache:
views/site_stats/1e4adb3067d5a7598ea1d0fd0f7b7ff1
17.1.4 Russian-Doll Caching
If you nest calls to the cache method and provide objects as key names, you get a strategy referred to as “russian-doll” caching by David92 and others93.
To take advantage of this strategy, let’s update our example code, assuming that a user has many entries (and remembering that this is a simple blog application).
Listing 17.5 Russian-doll nesting
1 %h1#{@user.name}'s Journal
2
3 - cache [@user, :entries, page: params[:page]] do
4 %ul.entries
5 = render partial: 'entry', collection: @entries
6
7 - content_for :sidebar do
8 - cache(:site_stats) do
9 = render partial: 'site_stats'
10
11 # entries/_entry.html.haml
12
13 - cache entry do
14 %li[entry]
15 %p.content= entry.content
16 ...
Now we retain fast performance even if the top-level cache is busted. For instance, adding a new entry would update the timestamp of the @user, but only the new entry has to be rendered. The rest of the content already exists as smaller fragments that are not invalid and can get reused.
Listing 17.6 Example of using touch to invalidate a parent record’s cache key
1 classUser < ActiveRecord::Base
2 has_many :entries
3 end
4
5 classEntry < ActiveRecord::Base
6 belongs_to: user, touch: true
7 end
For this to work correctly, there has to be a way for the parent object (@user in the case of the example) to be updated automatically when one of its dependent objects changes. That’s where the touch functionality of ActiveRecord and other object mapper libraries comes in, as demonstrated in Listing 17.6.
Outside of the Rails world, the russian doll strategy is also known as generational caching.
I have found that using this strategy can dramatically improve application performance and lessen database load considerably. It can save tons of expensive table scans from happening in the database. By sparing the database of these requests, other queries that do hit the database can be completed more quickly.
In order to maintain cache consistency this strategy is conservative in nature, this results in keys being expired that don’t necessarily need to be expired. For example if you update a post in a particular category, this strategy will expire all the keys for all the categories. While this may seem somewhat inefficient and ripe for optimization, I’ve often found that most applications are so read-heavy that these types of optimization don’t make a noticeable overall performance difference. Plus, the code to implement those optimizations then become application or model specific, and more difficult to maintain.
…in this strategy nothing is ever explicitly deleted from the cache. This has some implications with respect to the caching tool and eviction policy that you use. This strategy was designed to be used with caches that employ a Least Recently Used (LRU) eviction policy (like Memcached). An LRU policy will result in keys the with old generations being evicted first, which is precisely what you want. Other eviction policies can be used (e.g. FIFO) although they may not be as effective.
Jonathan Kupferman discussing web application caching strategies94
Later in the chapter, we discuss how to configure Memcached as your application’s cache.
David details an extreme form of Russian-Doll caching in his seminal blog post How Basecamp Next got to be so damn fast without using much client-side UI. The level of detail he goes into is too much for this book, but we recommend his strategy of aggressively cached reuse of identical bits of markup in many different contexts of his app. CSS modifies the display of the underlying markup to fit its context properly.
17.1.5 Conditional caching
Rails provides cache_if and cache_unless convenience helpers that wrap the cache method and add a boolean parameter.
- cache_unless current_user.admin?, @expensive_stats_to_calculate do
...
17.1.6 Expiration of Cached Content
Whenever you use caching, you need to consider any and all situations that will cause the cache to become stale, out of date. As we’ve seen, so-called generational caching attempts to solve cache expiry by tying the keys to information about the versions of the underlying objects. But if you don’t use generational caching, then you need to write code that manually sweeps away old cached content, or makes it time-out, so that new content to be cached in its place.
17.1.6.1 Time-based expiry
The simplest strategy for cache invalidation is simply time-based, that is, tell the cache to automatically invalidate content after a set time period. All of the Rails cache providers (Memcached, Redis, etc) accept an option for time-based expiry. Just add :expires_in to your fragment cache directive:
- cache @entry, expire_in: 2.hours do
= render @post
We can tell you from experience that this kind of cache invalidation is only good for a narrow set of circumstances. Most of the time, you only want to invalidate when underlying data changes state.
17.1.6.2 Expiring Pages and Actions
The expire_page and expire_action controller methods let you explicitly delete content from the cache in your action, so that it is regenerated on the next request. There are various ways to identify the content to expire, but one of them is by passing a hash with url_for conventions used elsewhere in Rails. Since this topic is now=now esoteric in Rails 4, we leave it as a research exercise for the motivated reader.
17.1.6.3 Expiring Fragments
The sample blogging app we’ve been playing with has globally cached content to clear out, for which we’ll be using the expire_fragment method.
1 def create
2 @entry = @user.entries.build(params[:entry])
3 if @entry.save
4 expire_fragment(:site_stats)
5 redirect_to entries_path(@entry)
6 else
7 render action: 'new'
8 end
9 end
This isn’t the greatest or most current Rails code in the world. All it’s doing is showing you basic use of expire_fragment. Remember that the key you provide to expire_fragment needs to match the key you used to set the cache in the first place. The difficulty in maintaining this kind of code is the reason that key invalidation is considered one of the hardest problems in computer science!
Occasionally, you might want to blow away any cached content that references a particular bit of data. Luckily, the expire_fragment method also understands regular expressions. In the following example, we invalidate anything related to a particular user:
expire_fragment(%r{@user.cache_key})
The big gotcha with regular expressions and expire_fragment is that it is not supported with the most common caching service used on Rails production systems: Memcached. |
17.1.7 Automatic Cache Expiry with Sweepers
Since caching is a unique concern, it tends to feel like something that should be applied in an aspect-oriented fashion instead of procedurally.
A Sweeper class is kind of like an ActiveRecord Observer object, except that it’s specialized for use in expiring cached content. When you write a sweeper, you tell it which of your models to observe for changes, just as you would with callback classes and observers.
Remember that observers are no longer included in Rails 4 by default, so if you need sweepers, you’ll have to add the official observers gem to your Gemfile. gem 'rails-observers' |
Listing 17.7 Moving expiry logic out of controller into a Sweeper class
1 classEntrySweeper < ActionController::Caching::Sweeper
2 observe Entry
3
4 def expire_cached_content(entry)
5 expire_page controller: 'entries', action: 'public'
6 expire_fragment(:site_stats)
7 end
8
9 alias_method :after_commit, :expire_cached_content
10 alias_method :after_destroy, :expire_cached_content
11
12 end
Once you have a Sweeper class written, you still have to tell your controller to use that sweeper in conjunction with its actions. Here’s the top of the revised entries controller:
1 classEntriesController < ApplicationController
2 caches_page :public
3 cache_sweeper :entry_sweeper, only: [:create, :update, :destroy]
4 ...
Like many other controller macros, the cache_sweeper method takes :only and :except options. There’s no need to bother the sweeper for actions that can’t modify the state of the application, so we do indeed include the :only option in our example.
17.1.8 Avoiding Extra Database Activity
Once you have fragments of your view cached, you might think to yourself that it no longer makes sense to do the database queries that supply those fragments with their data. After all, the results of those database queries will not be used again until the cached fragments are expired. Thefragment_exist? method lets you check for the existence of cached content, and takes the same parameters that you used with the associated cache method.
Here’s how we would modify the index action accordingly:
1 def index
2 unless fragment_exist? [@user, :entries, page: params[:page]]
3 @entries = Entry.all.limit(10)
4 end
5 end
Now the finder method will only get executed if the cache needs to be refreshed. However, as Tim pointed out in previous editions of this book, the whole issue is moot if you use Decent Exposure95 to make data available to your views via methods, not instance variables. Because decent exposure method invocations are inside the templates instead of your controllers, inside the blocks passed to the cache method, the problem solves itself.
We actually disputed whether to even include this section in the current edition. Since view rendering is so much slower than database access, avoidance of database calls represents a minor additional optimization on top of the usual fragment caching. Meaning you should only have to worry about this if you’re trying to squeeze every last bit of performance out of your application, and even then, we advise you to really think about it.
17.1.9 Cache Logging
If you’ve turned on caching during development, you can actually monitor the Rails console or development log for messages about caching and expiration.
Write fragment views/pages/52781671756e6bd2fa060000-20131110153647/
stats/1f440155af81f1358d8f97a099395802 (1.4ms)
Cache digest for pages/_page.html: 1f440155af81f1358d8f97a099395802
Read fragment views/pages/52781604756e6bd2fa050000-20131104214748/
stats/1f440155af81f1358d8f97a099395802 (0.3ms)
17.1.10 Cache Storage
You can set up your application’s default cache store by calling config.cache_store= in the Application definition inside your config/application.rb file or in an environment specific configuration file. The first argument will be the cache store to use and the rest of the argument will be passed as arguments to the cache store constructor.
By default, Rails gives you three different options for storage of action and fragment cache data. Other options require installation of third-party gems96.
ActiveSupport::Cache::FileStore
Keeps the fragments on disk in the cache_path, which works well for all types of environments (except Heroku) and shares the fragments for all the web server processes running off the same application directory.
ActiveSupport::Cache::MemoryStore
Keeps fragments in process memory, in a threadsafe fashion. This store can potentially consume an unacceptable amount of memory if you do not limit it and implement a good expiration strategy. The cache store has a bounded size specified by the :size options to the initializer (default is 32.megabytes). When the cache exceeds the allotted size, a cleanup will occur and the least recently used entries will be removed. Note that only small Rails applications that are deployed on a single process will ever benefit from this configuration.
ActiveSupport::Cache::MemCacheStore
Keeps the fragments in a separate process using a proven cache server named memcached.
17.1.10.1 Configuration Examples
The :memory_store option is enabled by default. Unlike session data, which is limited in size, fragment-cached data can grow to be quite large, which means you almost certainly don’t want to use this default option in production.
config.cache_store = :memory_store, expire_in: 1.minute, compress: true
config.cache_store = :file_store, "/path/to/cache/directory"
All cache stores take the following hash options as their last parameter:
expires_in
Supply a time for items to be expired from the cache.
compress
Specify to use compression or not.
compress_threshold
Specify the threshold at which to compress, with the default being 16k.
namespace
if your application shares a cache with others, this option can be used to create a namespace for it.
race_condition_ttl
This option is used in conjunction with the :expires_in option on content that is accessed and updated heavily. It prevents multiple processes from trying to simultaneously repopulate the same key. The value of the option sets the number of seconds that an expired entry can be reused (be stale) while a new value is being regenerated.
17.1.10.2 Limitations of File-Based Storage
As long as you’re hosting your Rails application on a single server, setting up caching is fairly straightforward and easy to implement (of course, coding it is a different story).
If you think about the implications of running a cached application on a cluster of distinct physical servers, you might realize that cache invalidation is going to be painful. Unless you set up the file storage to point at a shared filesystem such as NFS or GFS, it won’t work.
17.2 Data Caching
Each of the caching mechanisms described in the previous section is actually using an implementation of an ActiveSupport::Cache::Store, covered in detail inside Appendix B, Active Support API Reference.
Rails actually always exposes its default cache store via the Rails.cache method, and you can use it anywhere in your application, or from the console:
1 >> Rails.cache.write(:color, :red)
2 => true
3 >> Rails.cache.read :color
4 => :red
17.2.1 Eliminating Extra Database Lookups
One of the most common patterns of simple cache usage is to eliminate database lookups for commonly accessed data, using the cache’s fetch method. For the following example, assume that your application’s user objects are queried very often by id. The fetch method takes a block that is executed and used to populate the cache when the lookup misses, that is, a value is not already present.
Listing 17.8
1 classUser < ActiveRecord::Base
2 defself.fetch(id)
3 Rails.cache.fetch("user_#{id}") { User.find(id) }
4 end
5
6 def after_commit
7 Rails.cache.write("user_#{id}", self)
8 end
9
10 def after_destroy
11 Rails.cache.delete("city_#{id}")
12 end
13 end
With relatively little effort, you could convert the code in Listing 17.8 into a Concern, and include it wherever needed.
17.2.2 Initializing New Caches
We can also initialize a new cache directly, or through ActiveSupport::Cache.lookup_store if we want to use different caches for different reasons. (Not that we recommend doing that.) Either one of these methods of creating a new cache takes the same expiration and compression options as mentioned previously, and the same three stores exist as for fragment caching: FileStore, MemoryStore, and MemCacheStore.
1 ActiveSupport::Cache::MemCacheStore.new(
2 expire_in: 5.seconds
3 )
4 ActiveSupport::Cache.lookup_store(
5 :mem_cache_store, compress: true
6 )
Once you have your cache object, you can read and write to it via its very simple API and any Ruby object that can be serialized can be cached, including nils.
1 cache = ActiveSupport::Cache::MemoryStore.new
2 cache.write(:name, "John Doe")
3 cache.fetch(:name) # => "John Doe"
17.2.3 fetch Options
There are several now-familiar options that can be passed to fetch in order to provide different types of behavior for each of the different stores. Additional options than those listed here are available based on the individual cache implementations.
:compress
Use compression for this request.
:expire_in
Tell an individual key in the cache to expire in n seconds.
:force
If set to true will force the cache to delete the supplied key.
:race_condition_ttl
Supply seconds as an integer and a block. When an item in the cache is expired for less than the number of seconds, its time gets updated and its value is set to the result of the block.
There are other available functions on caches, and additional options can be passed depending on the specific cache store implementation.
delete(name, options)
Delete a value for the key.
exist?(name, options)
Will return true if a value exists for the provided key.
read(name, options)
Get a value for the supplied key or return nil if none found.
read_multi(*names)
Return the values for the supplied keys as a hash of key/value pairs.
write(name, value, options)
Write a value to the cache.
17.3 Control of Web Caching
Action Controller offers a pair of methods for easily setting HTTP 1.1 Cache-Control headers. Their default behavior is to issue a private instruction, so that intermediate caches (web proxies) must not cache the response. In this context, private only controls where the response may be cached and not the privacy of the message content.
The public setting indicates that the response may be cached by any cache or proxy and should never be used in conjunction with data served up for a particular end user.
Using curl --head we can examine the way that these methods affect HTTP responses. For reference, let’s examine the output of a normal index action.
1 $ curl --head localhost:3000/reports
2 HTTP/1.1 200 OK
3 Etag: "070a386229cd857a15b2f5cb2089b987"
4 Connection: Keep-Alive
5 Content-Type: text/html; charset=utf-8
6 Date: Wed, 15 Sep 2010 04:01:30 GMT
7 Server: WEBrick/1.3.1 (Ruby/1.8.7/2009-06-12)
8 X-Runtime: 0.032448
9 Content-Length: 0
10 Cache-Control: max-age=0, private, must-revalidate
11 Set-Cookie: ...124cc92; path=/; HttpOnly
Don’t get confused by the content length being zero. That’s only because curl --head issues a HEAD request. If you’re experimenting with your own Rails app, try curl -v localhost:3000 to see all the HTTP headers plus the body content.
17.3.1 expires_in(seconds, options = )
This method will overwrite an existing Cache-Control header.97
Examples include
expires_in 20.minutes
expires_in 3.hours, public: true
expires in 3.hours, 'max-stale' => 5.hours, public: true
Setting expiration to 20 minutes alters our reference output as follows:
Cache-Control: max-age=1200, private
17.3.2 expires_now
Sets a HTTP 1.1 Cache-Control header of the response to no-cache informing web proxies and browsers that they should not cache the response for subsequent requests.
17.4 ETags
The bulk of this chapter deals with caching content so that the server does less work than it would have to do otherwise, but still incurs the cost of transporting page data to the browser. The ETags scheme, where E stands for entity, allows you to avoid sending any content to the browser at all if nothing has changed on the server since the last time a particular resource was requested. A properly implemented ETags scheme is one of the most significant performance improvements that can be implemented on a high traffic website.98
Rendering automatically inserts the Etag header on 200 OK responses, calculated as an MD5 hash of the response body. If a subsequent request comes in that has a matching Etag99, the response will be changed to a 304 Not Modified and the response body will be set to an empty string.
The key to performance gains is to short circuit the controller action and prevent rendering if you know that the resulting Etag is going to be the same as the one associated with the current request. I believe you’re actually being a good Internet citizen by paying attention to proper use of ETags in your application. According to RFC 2616100, “the preferred behavior for an HTTP/1.1 origin server is to send both a strong entity tag and a Last-Modified value.”
Rails does not set a Last-Modified response header by default, so it’s up to you to do so using one of the following methods.
17.4.1 fresh_when(options)
Sets ETag and/or Last-Modified headers and renders a 304 Not Modified response if the request is already fresh. Freshness is calculated using the cache_key method of the object (or array of objects) passed as the :etag option.
For example, the following controller action shows a public article.
1 expose(:article)
2
3 def show
4 fresh_when(etag: article,
5 last_modified: article.created_at.utc,
6 public: true)
7 end
This code will only render the show template when necessary. As you can tell, this is superior even to view caching because there is no need to check the server’s cache, and data payload delivered to the brower is almost completely eliminated.
17.4.2 stale?(options)
Sets the ETag and/or Last-Modified headers on the response and checks them against the client request (using fresh_when). If the request doesn’t match the options provided, the request is considered stale and should be generated from scratch.
You want to use this method instead of fresh_when if there is additional logic needed at the controller level in order to render your view.
1 expose(:article)
2
3 expose(:statistics) do
4 article.really_expensive_operation_to_calculate_stats
5 end
6
7 def show
8 if stale?(etag: article,
9 last_modified: article.created_at.utc,
10 public: true)
11 # decent_exposure memoizes the result, later used by the view
12 statistics()
13
14 respond_to do |format|
15 ...
16 end
17 end
18 end
The normal rendering workflow is only triggered inside of the stale? conditional, if needed.
17.5 Conclusion
We’ve just covered a fairly complicated subject: Caching. Knowing how to use caching will really save your bacon when you work on Rails applications that need to scale. Indeed, developers of high-traffic Rails websites tend to see Rails as a fancy HTML generation platform with which to create content ripe for caching.