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.

@rwdaigle
Copy link

Great stuff. Few comments:

  • Instead of painting it as a negative that Varnish isn't available "but does not use Varnish or Nginx to help with caching and speed" reference it in relation to Cedar's 12-factor origins (from http://devcenter.heroku.com/articles/cedar): "Cedar does not include a reverse proxy cache such as Varnish, preferring to empower developers to choose the CDN solution that best serves their needs"
  • You can be very Cedar-centric in this article. We are greatly de-emphasizing the other stacks.
  • I would include the heroku addons:add memcache part of the process inline here instead of linking out to http://devcenter.heroku.com/articles/memcache#using_memcache_from_ruby. The pattern here is to include the link for more background on Memcached in a callout: http://devcenter.heroku.com/articles/writing#callouts
  • When I did something similar with a Sinatra app I chose different entity/meta stores (https://github.com/rwdaigle/ryandaigle.com/blob/master/config.ru#L19) Can we provide some clarity as to what the purpose of the two stores is the rationale/considerations to choosing the storage implementation?
  • We largely skip over how to run such an app locally (specifying MEMCACHE_SERVERS in a .env etc...). Minus installing memcached locally, I think we should cover it. Many of our tutorials have the pattern of describing code, running locally and deploying - we should try and mimic.
  • Does Dalli auto recognize the MEMCACHE_SERVERS env var? If not - how can you just say Dalli::Client.new in the rack-cache config?
  • We should have a reference app on GitHub that we can point to that contains instructions for deploying to Heroku in its README. I imagine you have one based on your work here - can we clean it up and make public? See here for the recommended way to reference a sample app: http://devcenter.heroku.com/articles/writing#supporting_applications

@jonmountjoy
Copy link

It feels like there is a lot of overlap between this new article, and this existing one?
http://devcenter.heroku.com/articles/building-a-rails-3-application-with-the-memcache-addon
Should they be merged?

@rwdaigle
Copy link

After reading through the Rails 3 w/ Memcached article I think they address two different, but related, topics and should remain separate.
The Rails 3 w/ Memcached article shows you how to use Memcached for Rails Caching on Heroku. This one shows you how to cache static assets w/ Rack-Cache in Rails.
I think with some tweaking of the titles and intro paragraphs we can distinguish this more clearly.

@schneems
Copy link
Author

@rwdaigle most of that is straightforward, however do have some comments:

 Not sure how much to add, I don't want to duplicate too much of the memcache article. I can throw this in though.
  • We largely skip over how to run such an app locally (specifying MEMCACHE_SERVERS in a .env etc...). Minus installing memcached locally, I think we should cover it. Many of our tutorials have the pattern of describing code, running locally and deploying - we should try and mimic.

This is already covered in the http://devcenter.heroku.com/articles/memcache#using_memcache_from_ruby
article which includes setting up the environment locally.

  Awesome point. Actually need to test out using tmp store versus memcache to
  see if tmp store makes more sense (looks like it does/should), just not
  sure if it will behave well on a distributed system.

  Is it okay to link out the the Rack::Cache docs as well, I know we
  prefer not to, but there is quite a bit of information here:
  http://rtomayko.github.com/rack-cache/storage. I can summarize, but the
  full text is also useful.
  • Does Dalli auto recognize the MEMCACHE_SERVERS env var? If not - how can you just say Dalli::Client.new in the rack-cache config?

  It does ("By default Dalli will look for the proper environment variables when deployed to Heroku, and otherwise will default to localhost and the default 
  port") , I can be explicit here.

@rwdaigle
Copy link

Sounds good, Richard.

Re:

Is it okay to link out the the Rack::Cache docs as well, I know we prefer not to, but there is quite a bit of information here: http://rtomayko.github.com/rack-cache/storage. I can summarize, but the full text is also useful.

It's definitely fine to link out to it for readers that want to dive deeper. Just as long as we provide enough content to get an idea of what's going within our article for the average reader.

@rwdaigle
Copy link

Hey Richard, Saw you made a few edits this weekend. Is the ball back in my court?

@schneems
Copy link
Author

@rwdaigle, yes please take a look, i'm also working on an example app: https://github.com/heroku/rack-cache-demo

@jonmountjoy
Copy link

  • Is there a way to start this article differently? Not with a negative. Do people expect their cloud PaaS has CDN built in? Force.com does, but that rocks :-P I mean, there's nothing stopping me from using a CDN anyway. Likewise, do people expect us to run Varnish (on Cedar). The fact that we used to on Bamboo is not relevant here (IMO - this is not a Bamboo article).
  • Should the title include "Memcache"?

This article makes me ask the question: how do I add a CDN too :-) I want my static assets on Dev Center distributed across the globe man! I see we're currently serving them from our app.

@rwdaigle
Copy link

@jonmountjoy: Disagree with the first point and agree with the second. Re: expectations of Varnish - yes - this is actually something I want to directly address with this article. Lot of people have that expectation because we did so good a job evangelizing Varnish in the paste. We can tweak the language a bit further, though.

Reviewing now...

@schneems
Copy link
Author

Funny enough adding a CDN actually negates most of the benefit of enabling Rack Cache since your server should only get hit once for the assets anyway. I would love to write an article on getting a CDN setup with Rails and Cloudfront. How is this?

On the Cedar stack you can use Rack::Cache with Memcache in your Rails application to improve response time and decrease application load. Cedar empowers developers to choose the CDN or caching solution 
that best serves their needs and does not include a reverse proxy cache such as Varnish. Heroku instead recommends using Memcache with Rack::Cache which acts as a HTTP cache that can serve files; this is 
especially important if you're using the   asset pipeline. 

*New Title

Rack Cache with Memcache on Cedar Rails 3.1+ Applications

I wouldn't say people expect their PaaS to have a CDN, but for every feature we do have, thats just one less thing a developer has to do, and thats one more reason a developer has to choose us.

@rwdaigle
Copy link

Gents, I present to you Richard's masterpiece: https://devcenter.heroku.com/articles/rack-cache-memcached-static-assets-rails31?preview=1
Richard - I did some small section title changes and removed some of the local debugging steps while attempting to keep the essence in place. Let me know what you think and we can press publish on this thing and link it up with existing articles!

@schneems
Copy link
Author

ShipIt

@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