Skip to content

Instantly share code, notes, and snippets.

@schneems
Created March 18, 2012 23:13
Show Gist options
  • Select an option

  • Save schneems/2084471 to your computer and use it in GitHub Desktop.

Select an option

Save schneems/2084471 to your computer and use it in GitHub Desktop.
Rack Cache on Cedar Rails 3.1+ Applications

Rack Cache on Cedar Rails 3.1+ Applications

Cedar does not include a reverse proxy cache such as Varnish, preferring to empower developers to choose the CDN or caching solution that best serves their needs. You can use Rack::Cache your Rails application to improve response time and decrease load. This is important if you're using the asset pipeline and serving static assets through your application.

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/)

Using Rack::Cache

Rack::Cache is rack middleware that enables HTTP caching on your application and can allow us to serve assets from a storage backend without requiring work from your 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 store, which can be configured for use with three different storage backends ,file, heap, and memcache. The metastore in Rack::Cache keeps high level information about each cache entry including headers. The entitystore is the response body content, the actual object being returned through the server. Storing data in a file is slow but memory efficient; using heap means your process' memory will be used which is quicker, but can have serious impacts; and using memcache is the fastest option, but isn't well suited to store large objects.

Since the metastore is accessed frequently and is much smaller than the entitystore, you can use memcache for the backend, but to keep the amount of data stored in Memcache to a minimum, you can write the entitystore to a temporary file. Heap storage is not recommended. For more information on the entity and meta stores read more about Rack Cache Storage.

Configure Memcache

Configuring your application to use [Memcache](http://devcenter.heroku.com/articles/memcache)

Since you will use memcache in your Rack::Cache metastore, you will first need to add it to your project. Heroku recommends using Memcache with the Dalli gem as part of your Rack::Cache backend.

You'll need to configure your Heroku application to use Memcache.

:::term
$ heroku addons:add memcache

To get this running on your local machine you will need to have Memcache installed. You can install it using a tool such as homebrew.

:::term
$ brew install memcache

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

You can then tell your application to use Dalli, Heroku's preferred Memcache client. In your Gemfile add

:::term
gem 'dalli'

Then in your config/application.rb add

:::term
config.cache_store = :dalli_store

If you start a Rails console session you should now have access to Memcache

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

Once you have configured your application to use memcached, you can configure Rack::Cache for use in your application.

Configure Rack::Cache on Rails 3.1+

For Rails 3.1+ you will need to modify your config/production.rb to tell rack_cache to use Memcache. By default Dalli::Client.new will look for the proper environment variables (ENV["MEMCACHE_SERVERS"]) when deployed to Heroku, and otherwise will default to localhost and the default port. If you want, rather than specifying an object, you can pass the connection string needed to talk to an external memcache server.

:::term
# This will tell `Rack::Cache` to use the default settings of Dalli, Heroku's recommended Memcache Gem
config.action_dispatch.rack_cache = {
                        :metastore    => Dalli::Client.new,
                        :entitystore  => 'file:tmp/cache/rack/body',
                        :allow_reload => false }

You can set the entitystore to write to a temporary file in the rails directory.

To allow your application to serve static files from /public you need to set config.servestatic_assets to true.

:::term
# This will allow Action::Dispatch to serve files from /public when set to true
config.serve_static_assets = true

Finally you need to tell the cache how long an item should stay in cache by setting the Cache-Control headers. Without a Cache-Control header, static files will not be stored by Rack::Cache

:::term
# This will set Cache-Control headers used by browsers
config.static_cache_control = "public, max-age=2592000"

Since these settings will tell Rack::Cache to store static elements for a very long time, it is important that you let your cache store know when you change a file. Typically cache invalidation can be very tricky, so to avoid that problem you can ensure that Rails generates a new file name every time you modify a file. This is done by using a hash digest such as MD5 on your files, tell Rails to do this automatically by setting config.assets.digest to true.

:::term
# Generate digests for assets URLs
config.assets.digest = true

You also want to ensure that caching is turned on.

# Enables caching including `Rack::Cache`
config.action_controller.perform_caching = true

Once all of this is set up correctly, you should see cache lines in your production log. The 'miss, store' indicates 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

The '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

Debugging

If a setting is not configured properly, you might see miss in your logs instead of store or fresh as seen below. Make sure that you're using a hard refresh to clear your browser cache while you're investigating the problem.

:::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. It can be easier to debug locally so you need to set up your project to be run in production locally.

:::term
$ bundle exec rake db:create RAILS_ENV=production

Compile assets with

:::term
$ bundle exec rake assets:precompile RAILS_ENV=production`

You can then start your application locally in production

:::term
$ bundle exec rails s -e production

Next copy one of your asset urls and curl it in the terminal using -I which will return the headers. It for an asset like rails.png it would look similar to this.

:::term
$ curl 'http://localhost:3000/assets/rails-782b548cc1ba7f898cdad2d9eb8420d2.png' -I

The result should look something like below, where Cache-Control returns public, max-age=2592000

:::term
$ curl 'http://localhost:3000/assets/rails-782b548cc1ba7f898cdad2d9eb8420d2.png' -I

HTTP/1.1 200 OK
Last-Modified: Sun, 18 Mar 2012 00:19:19 GMT
Content-Type: image/png
Cache-Control: public, max-age=2592000
Content-Length: 6646
Date: Sun, 18 Mar 2012 21:27:07 GMT
X-Content-Digest: 501d6b0108b930264e19f37cb8ee6c8222d4f30d
Age: 689
X-Rack-Cache: fresh
Server: WEBrick/1.3.1 (Ruby/1.9.2/2011-07-09)
Connection: Keep-Alive

You can also ensure that you are seeing and X-Rack-Cache header indicating the status of your asset (fresh/store/miss).

If you choose to use file storage for your entity store as demonstrated above, you can try deleting contents of the cache on disk, if your store is set to write to file:tmp/cache/rack/body you would go to your Rails root and remove all entries under tmp/cache/rack/body. When debugging using this step, do not forget to do a hard refresh in the browser.

If you modify a file and your server continues to serve the old file, check that you committed the file to your Git repository before deploying, and you can check to see if it exists in your compiled code by using heroku run bash

:::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

Don't forget to check if the file exists in your 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