Skip to content

Instantly share code, notes, and snippets.

@schneems
Created March 18, 2012 23:13
Show Gist options
  • Save schneems/2084471 to your computer and use it in GitHub Desktop.
Save schneems/2084471 to your computer and use it in GitHub Desktop.
Rack Cache on Cedar Rails 3.1+ Applications

Though slightly more complex, using a CDN is the most performant option for serving static assets. See the [CDN asset host](cdn-asset-host-rails31) article for more details.

Ruby on Rails applications should use Rack::Cache to efficiently serve assets on the Cedar stack. Proper Rack::Cache usage improves response time, decreases load and is important when serving static assets through your application.

This article will summarize the concepts of caching assets using Rack::Cache and walk you through the appropriate configuration of a Rails 3.1 application and the asset pipeline.

Sample code for this article's [reference application](https://github.com/heroku/rack-cache-demo) is available on GitHub and can be seen running at [http://rack-cache-demo.herokuapp.com/](http://rack-cache-demo.herokuapp.com/)

Understanding Rack

Rack::Cache is Rack middleware that enables HTTP caching on your application and allows an application to serve assets from a storage backend without requiring work from the main Rails application. Many Ruby web frameworks, including Rails 3+ and Sinatra, are built on top of Rack.

At a high level, Rack works by taking a request and passing it through a series of steps, called middleware. Each Rack middleware performs some action, then passes the request to the next middleware in the rack. Eventually the request is properly formatted and it will reach your application.

Rack is written to be lightweight and flexible. If middleware can respond to a request directly, then it doesn't have to hit your Rails application. This means that the request is retuned faster, and the overall load on the Rails application is decreased.

Rack::Cache storage

Rack::Cache has two different storage areas: meta and entity stores. The metastore keeps high level information about each cache entry including HTTP request and response headers. This area stores small chunks of data that is accessed at a high frequency. The entitystore caches the response body content which can be a relatively large amount of data though it is accessed less frequently than the metastore.

Rack::Cache ships with three different storage engines: file, heap, and memcache. Storing data in the file engine is slower but memory efficient. Using heap means your process' memory will be used which is quicker but can have an impact on performance if it grows unbounded. Using memcache is the fastest option though it isn't well suited to store large objects.

For more information on the entity and meta stores read about [Rack Cache Storage](http://rtomayko.github.com/rack-cache/storage).

Using the metastore with the memcache storage engine, which allows very quick access to shared meta-data, while using the file engine for the entitystore and its larger objects results in an efficient and predictable application performance profile and is recommended on Heroku.

Install Memcache locally

Local installation instructions for other OSs can be found in the [Memcache add-on article](http://devcenter.heroku.com/articles/memcache).

To run your application locally and test the Rack::Cache setup you will need to have Memcache installed. You can install it on Mac OSX using a tool such as homebrew.

:::term
$ brew install memcached

At the end of installation homebrew will give you instructions on how to start Memcache manually and automatically on system start.

Configure Rails cache-store

Heroku recommends using Memcache with the Dalli gem as part of your Rack::Cache backend. In your Gemfile add:

:::ruby
gem 'dalli'

After running bundle install to establish Dalli as an application dependency tell Rails to use the Dalli client for its cache-store in config/application.rb.

:::ruby
config.cache_store = :dalli_store

Confirm dalli/memcache configuration by starting a local Rails console session and getting/setting a simple key-value.

:::term
$ rails c
> memcache = Dalli::Client.new
> memcache.set('foo', 'bar')
> memcache.get('foo')
'bar'

Once you've configured your application to use memcached it's now time to configure Rack::Cache.

Configure Rack::Cache

Modify your config/environments/production.rb environment file to specify the appropriate storage backends for Rails' built-in Rack::Cache integration.

:::ruby
config.action_dispatch.rack_cache = {
  :metastore    => Dalli::Client.new,
  :entitystore  => 'file:tmp/cache/rack/body',
  :allow_reload => false
}

If not specified, `Dalli::Client.new` automatically retrieves the Memcache server location from the `MEMCACHE_SERVERS` environment variable. Otherwise it will default to localhost and default port.

Serve static assets

See the `production.rb` config of the [reference application](https://github.com/heroku/rack-cache-demo/blob/master/config/environments/production.rb) on GitHub.

To allow your application to properly serve, invalidate and refresh static assets several config settings must be updated in config/environments/production.rb. Allow Rails to serve assets with the serve_static_assets setting.

:::ruby
config.serve_static_assets = true

Additionally, specify how long an item should stay cached by setting the Cache-Control headers. Without a Cache-Control header static files will not be stored by Rack::Cache

:::ruby
config.static_cache_control = "public, max-age=2592000"

These settings tell Rack::Cache to store static elements for a very long time. To properly invalidate modified files Rails updates a hash digest in the file name. Enable this approach with the config.assets.digest setting.

:::ruby
config.assets.digest = true

You also want to confirm that caching is turned on in production.

:::ruby
config.action_controller.perform_caching = true

Provision Memcache add-on

Since you will use memcache as your Rack::Cache metastore, you will need to add the Memcache add-on to your application on Heroku.

:::term
$ heroku addons:add memcache
----> Adding memcache to myapp... done, v25 (free)

Caching in production

Deploy the application to Heroku and use the heroku logs command to view cache output.

:::term
$ git push heroku master
$ heroku logs --ps web -t
Using a hard refresh clears your browser cache and is useful for forcing asset requests.

You should see cache entries in your production log-stream. Tailing miss, store tokens indicate that the item was not found in the cache but has been saved for the next request.

:::term
cache: [GET /assets/application-95bd4fe1de99c1cd91ec8e6f348a44bd.css] miss, store
cache: [GET /assets/application-95fca227f3857c8ac9e7ba4ffed80386.js] miss, store
cache: [GET /assets/rails-782b548cc1ba7f898cdad2d9eb8420d2.png] miss, store

fresh indicates item was found in cache and will be served from cache.

:::term
cache: [GET /assets/application-95bd4fe1de99c1cd91ec8e6f348a44bd.css] fresh
cache: [GET /assets/application-95fca227f3857c8ac9e7ba4ffed80386.js] fresh
cache: [GET /assets/rails-782b548cc1ba7f898cdad2d9eb8420d2.png] fresh

Your Rails 3.1+ application is now configured to cache static assets using Memcached, freeing up dynos to perform dynamic application requests.

Debugging

If a setting is not configured properly, you might see miss in your logs instead of store or fresh.

:::term
cache: [GET /assets/application-95bd4fe1de99c1cd91ec8e6f348a44bd.css] miss
cache: [GET /assets/application-95fca227f3857c8ac9e7ba4ffed80386.js] miss
cache: [GET /assets/rails-782b548cc1ba7f898cdad2d9eb8420d2.png] miss

When this happens ensure that the Cache-Control header exists by using curl to inspect asset response headers.

:::term
$ curl -I 'http://rack-cache-demo.herokuapp.com/assets/shipit-72351bb81da0eca408d9bd8342f1b972.jpg'
HTTP/1.1 200 OK
Age: 632
Cache-Control: public, max-age=2592000
Content-length: 70522
Etag: "72351bb81da0eca408d9bd8342f1b972"
Last-Modified: Sun, 25 Mar 2012 01:51:21 GMT
X-Rack-Cache: fresh

The response headers should contain Cache-Control with the value specific in the config.static_cache_control setting i.e.: public, max-age=2592000. Also confirm that you are seeing the X-Rack-Cache header indicating the status of your asset (fresh/store/miss).

Inconsistent file versions

If you modify a file and your server continues to serve the old file check that you committed it to your Git repository before deploying. You can check to see if it exists in your compiled code by using heroku run bash and listing the contents of the public/assets directory. This directory should contain the hashed asset file names.

:::term
$ heroku run bash
Running bash attached to terminal... up, run.1
$ ls public/assets
application-95bd4fe1de99c1cd91ec8e6f348a44bd.css      application.css           manifest.yml
application-95bd4fe1de99c1cd91ec8e6f348a44bd.css.gz   application.css.gz        rails-782b548cc1ba7f898cdad2d9eb8420d2.png
application-95fca227f3857c8ac9e7ba4ffed80386.js       application.js            rails.png
application-95fca227f3857c8ac9e7ba4ffed80386.js.gz    application.js.gz

Also confirm that the file is listed in Rails' manifest.yml.

:::term
$ cat public/assets/manifest.yml
rails.png: rails-782b548cc1ba7f898cdad2d9eb8420d2.png
application.js: application-95fca227f3857c8ac9e7ba4ffed80386.js
application.css: application-95bd4fe1de99c1cd91ec8e6f348a44bd.css

If the file you're looking for does not show up try running bundle exec rake assets:precompile RAILS_ENV=production locally and ensure that it is in your own public/assets directory.

@schneems
Copy link
Author

schneems commented Mar 27, 2012 via email

@rwdaigle
Copy link

rwdaigle commented Mar 27, 2012 via email

@rwdaigle
Copy link

How did I miss that incredible large Squirrel on the boat the first few times around?

@schneems
Copy link
Author

schneems commented Mar 27, 2012 via email

@rwdaigle
Copy link

Yep, too bad our syntax highlighter can get whacked. This will do for now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment