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

The Cedar stack gives us more flexibility but does not use Varnish or Nginx to help with caching and speed. To improve your application's response time and decrease load, you can use Rack::Cache. This is especially important if you're serving static assets through your application, such as images, stylsheets, and javascript through the Rails asset pipeline.

Using Rack::Cache

Rails 3+ and other Ruby web frameworks, such as 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 level in the rack. Eventually the request is properly formatted and it will reach your Rails application, where your application can handle the request. Rack is written to be lightweight and flexible. If a Rack middleware can respond to a request directly from a Rack middleware, 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 is 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.

Configure Memcache

Rack::Cache can be used with different storage backends. By default Rack::Cache will try to use the file system, which is read only on most of the Heroku platform, and on Cedar is ephemeral. Using memcache as a backend will allow you to persist your cache between deploys, across dynos, and will prevent a slow down on initial requests while warming the cache. Therefore Heroku recommends using Memcache with the Dalli gem as your Rack::Cache backend. Once you have configured your application to use memcached, you can configure Rack::Cache for use in your application.

Configure Rack::Cache to use Memcache 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 will look for the proper environment variables 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 prefered Memcache Gem
config.action_dispatch.rack_cache = {
                        :metastore    => Dalli::Client.new,
                        :entitystore  => Dalli::Client.new,
                        :allow_reload => false }

Then 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.

:::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 lines like this in your log.

:::term
# The item was not found in cache (miss) but has been saved for next request (store)

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

:::term
# The item was found in cache (fresh) and will be served from cache

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 like this. 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, copy one of your asset urls and curl it in the terminal using -I which will return the headers. If you can run your app in production mode locally it would look like this:

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

The result should look something like this, 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 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 rake assets:precompile 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