codahale.com٭blog

This is my old blog. My current writing is here: codahale.com

Content-only caching for Rails

So you’ve got a Rails app which is mostly static content, but it’s got some dynamic, user-specific stuff mixed in with the layout. You’d love to cache the static data, since it doesn’t change often, but that would leave you updating the dynamic content via AJAX or something, and as cool as AJAX is, it’s for crap when the Javascript is turned off.

You try page caching, but you notice that the dynamic content doesn’t update. You try action caching, but it’s the same story. You try fragment caching, but then your app still performs all the big database queries in your actions. There’s a level of granularity missing in Rails’ caching system. Cached pages are stupid-quick for very static content, cached actions allow you to filter via ActionController, and cached fragments clean up the messy bits of your views. But you can’t cache just a rendered view. Until now… (dun dun duuun)

Content caching is a different level of granularity for Rails. Like action caching, requests are routed through the ActionController framework. Unlike action caching, none of the layout is cached, allowing you to provide some dynamic, user-specific content while reducing DB loads and rendering times. It’s a bit like fragement caching, but if a copy exists in the cache, the controller action isn’t called, meaning the database is never queried.

Installation

Subversionality:


./script/plugin install -x http://svn.codahale.com/content_cache

Living dangerously?


./script/plugin install http://svn.codahale.com/content_cache

Usage


class NotesController < ActionController::Base
  caches_action_content :index

  def index
    @notes = Note.find(:all, :include => [:monkeys, dirigibles, robots])
  end
end

The first time /notes/index is requested, #index is executed and whatever it renders is stored wherever you have the cache store configured. None of the layout is stored, just the rendered view. The next time /notes/index is requested, the cached action content is read from the cache, placed within the layout, and sent to the client.

Sometimes an action’s instance variables are used in the layout itself–to set the title, for example. Instance variables your layout depends on can be specified as such:


ActionContentFilter.preserved_instance_variables += ['@title', '@content_type']

The content of these instance variables are cached alongside the action’s content, and sent to the layout during a request. (These types are marshalled, which means that simple data types are preferred, and anything which refers to records in a database will likely break after a certain period of time. Best to limit this to strings, integers, arrays, and other simple types which play well with marshalling.)

Speed

I did some rough benchmarking with this, and created an SQLite3 database with a single table and a thousand records in it. I generated a scaffold for this model and removed the pagination from the #list action. I made an alias of the #index action, called #index_with_cache, because I’m creative like this.

I requested each location twice. The first round was, as you’d expect, identical. The second round shows the benefit of the content caching mojo:


Completed in 3.93749 (0  reqs/sec) | Rendering: 3.38100 (85%) | DB: 0.48483 (12%) | 200 OK [http://localhost/monkeys/index]
Completed in 0.01605 (62 reqs/sec) | Rendering: 0.01167 (72%) | DB: 0.00000 ( 0%) | 200 OK [http://localhost/monkeys/index_with_cache]

That’s 24,532% faster!

Then I limited the number of results to 100, to get a more realistic picture:


                     user     system      total        real
without cache   15.360000   0.870000  16.230000 ( 20.844324)
with cache       3.250000   0.280000   3.530000 (  5.775237)

And this is just in development mode, too. Granted, this is a pretty edge case, but if you’ve got a lot of database traffic, caching will speed your app up something fierce. Plenty of people have been putting off adding caching because until now it’s been an all-or-nothing affair.

Have fun, kids!

31 Responses to “Content-only caching for Rails”

  1. kelyar Says:

    man, you saved my life! )
    before that rails caching was completely useless for me
    thanks

  2. Will Says:

    Great plugin!

    I couldn’t figure an easy way to expire the caches though, so I added an expire_action_fragment method to the ContentCache module (it’s not totally DRY, but it works):

    # Expires the cache for the given action options
    def expire_action_fragment(options = {})
    options = {:controller => self.controller_name }.merge(options)
    expire_fragment(url_for(options).split(”://”).last+’_content_only’)
    end

  3. Coda Says:

    Yeah, Will, sorry. I haven’t done any work on expiring cached actions, since that’s a rare event in the app I made this for, and I put it off until launch. It’s near the top of my to-do list now, so I’ll update it with a more humane way of expiring actions. Thanks for putting a fire under my ass, though.

  4. Ezra Says:

    Very cool Coda!

  5. Ben Says:

    Wow! This is just what I was looking for. I googled “cache views but not layouts” and this came right up. I can’t wait to try this out. Has the expire_action_fragment method from Will been added into the SVN code yet?

  6. Coda Says:

    Ben–No, it hasn’t, sorry. It’s definitely on my to-do list, though.

  7. Saimon Moore Says:

    Hi Coda,

    If an action has been cached, then the content cache plugin halts the filter chain. There are certain ‘after filters’ that need to be exeuted (e.g. sweep flash). How have you solved this?

    P.S. My own ‘before filters’ are not a problem as I just prepend them to the chain before the content cache plugin.

    Regards,

    Saimon

  8. Saimon Moore Says:

    Hi Coda,

    Well I solved the problem by adding the following line to the before(controller) method in your plugin just before it renders the cached action.


    controller.after_action

    Now all my after filters are executed and most importantly the flash is being sweeped (The reason this became an issue)

  9. Coda Says:

    Hi Saimon,

    I had experienced the same problem, but had fixed it by explicitly sweeping the flash. I’ll investigate your fix, though, because it probably plays much more nicely with the internals. Thanks for letting me know!

  10. Lee Iverson Says:

    I’ve got some UTF-8 content in my site and need to be able to munge the @headers['Content-Type']. I’ve tried to preserve the @headers variable (which I munge for ‘Content-Type’) and to make the after_action patch above, but neither seems to do the whole job. Any thoughts?

  11. Coda Says:

    Lee: Your best best is to probably use a before_filter in the specific controller. I know that’ll play nicely, as content_cache plays well with xhtml_content_type, another plugin of mine which changes the content type of the output. Failing that, one could always use an http-equiv meta element, but let’s hope it doesn’t come to that.

  12. Coda Says:

    Okay, I just committed Saimon’s patch, so you may want to update your source. It now plays much more nicely with the ActionController filter internals. Thanks a million, Saimon!

  13. Lee Iverson Says:

    With Saimon’s patch and an after_filter for the Content-Type header (it needs to be sensitive the actual Content-Type rendered), that did it!

  14. jack Says:

    How does one set the expiration interval for the cache? Is it possible to have the cache expire every 20 minutes?

  15. Coda Says:

    Jack–The cache expires when you expire it, basically. You’re probably better off writing cache sweepers to clear the old bits of the cache as the records change. A caching system can either be fast or self-aware, and the Rails caching system definitely comes down on the fast side.

  16. joost Says:

    If this works, it’ll be the best optimization of my app to date. That, and getting rid of components. They truly are evil.

  17. Sean O'Hara Says:

    I’ve used this with success however it conflicts with another hack that I am using (that also uses the content_for_layout variable. It is a method that allows one to use nested layouts (extremely useful): http://gridpt1.fe.up.pt/mlopes/blog/index.php/2006/08/12/nested-layouts-in-ruby-on-rails/

    Unfortunately when I use both content-only cacheing and these nest layouts the ‘inner’ or nested layout appears twice, thus breaking the layout. I guess this is because the nested layouts scheme sticks the inner layout into @content_for_layout and when it’s cached by content-caching and then rerendered it now has two copies of the inner layout… the one called by layout and the one already in the content cache…. I wonder if there’s any way to fix that. I kinda wish a lot of these really useful plugins would be rolled into rails since that would probably prevent them from breaking each other. Plus they are super useful (a lot of them).

  18. Daniel Says:

    Hello Coda,

    Your plugin is most welcomed, thank you!
    I have played with it and the results are amazing, as expected.

    Today I have encountered a problem trying to use it with rails engines ( http://rails-engines.org/ ). If I use caches_action_content in a controller that is placed in a engine, I have an error: ‘You can’t have more than one render per action’ (approximate quote).

    Do you have any idea why there’s this conflict?
    Thanks again.

  19. Fez Says:

    This is really slick. Thank you!!!

    I can’t believe I haven’t found this until now.

  20. Ed Says:

    Great work, Coda! Thanks for the effort.

    One question: have you done any speed comparisons between content-only and action caching?

    I have an app that displays the logged in user name in the layout. The user name is stored in session. As I see it, I have 3 choices: #1) remove the user name from layout and cache the whole action; #2) leave the user name in layout and cache the content; or #3) pass the user name in URL and action cache a separate page for each user.

    #3 doesn’t seem very scalable, so I’m leaning towards #1 or #2. Having the user name displayed in the layout is a nice usability touch but not essential for my app, so I can ditch it if I get enough extra performance bang from action caching versus content-only caching. Any insight on the relative performance of your plugin compared to action caching?

  21. dizave Says:

    Not having this is why I’ve thus far ignored rails caching! Great plugin!

    Hey, in ActionContentFilter::after, line 125, isn’t the “if template=controller.get_instance_variable(”@template”)” kinda redundant, and the else for it maybe better off as the else for the same if that started on line 117 (”if template”)? Or am I missing something?

    My template uses variables @css, @rss, and @javascripts to add stuff to the section for certain pages, and for some reason I can’t get content_cache to save these (arrays of strings) correctly, but I’m sure I will, and when it is working this will save me mad database lookups, so thanks again!

    -d

  22. Nikolas Says:

    Sweet :)

  23. Wordpress 2.0 & Typo themes - Desiloper » Content-only caching for Rails Says:

    [...] http://blog.codahale.com/2006/04/10/content-only-caching-for-rails/ [...]

  24. joost baaij Says:

    any word on if this will be updated for rails 1.2?

  25. Scott Patten Says:

    First, thanks for creating and sharing this. It was exactly what I needed.
    I’m running on Rails 1.2.1, and I had to comment out Saimon’s addition of
    controller.after_action
    If I don’t, I get the following error:
    undefined method `after_action' for #

    Is the after_action method from another plugin, or has it been removed from Rails in the last year? A quick search on api.rubyonrails.org didn’t turn up anything.

    I’m not 100% clear on what the after_action method does. Am I just preventing after_foo filters from running if I comment it out?

    Thanks,

    Scott Patten

  26. chubbz327 Says:

    Great tool. I was having problems with pagination but the following modification did the trick

    def action_url_to_id(controller, tag="content_only") #:nodoc:
    param_list = controller.params.to_s
    controller.url_for.split("://").last+'_'+param_list+tag
    end

  27. jujudellago Says:

    Wow !!

    thanks a lot for this great plugin that solved my problem with my “user menu/ please log in” in my layout !

    it also solved my problem with utf-8 encoding using the regular page caching.

    however, I really needed “Scott Patten”’s tip to remove the after_action, and “chubbz327″ for the fix on “action_url_to_id” to get it to work proprely.

    so thanks to them too !

    will there be an updated version of this lovely plugin in the future ?

  28. Dare to Dream? » Blog Archive » Giving ApnaBill a 76.35% kick :) Says:

    [...] content_cache plugin from Coda Hale. Check out other plugins he [...]

  29. www.ApnaBill.com Says:

    Thanks!
    Great plugin!

    http://www.makuchaku.info/blog/giving-apnabill-a-7635-kick


    Maku
    http://www.apnabill.com
    http://www.makuchaku.info/blog

    PS: I also had to use Scott’s hack - http://blog.codahale.com/2006/04/10/content-only-caching-for-rails/#comment-6144

  30. Anthony Says:

    Hey,

    is this basically ‘partial caching’ as well then?

    A

  31. José Valim Says:

    I’ve created a plugin that has the same behavior as content_cache, that plays nice with Rails 2.0 and it’s just an extension of Rails caches_action method.

    You would just do:

    caches_action :index, :without_layout => true

    It works with :if and :cache_path parameters also.

    Check it here at: http://code.google.com/p/action-cache-layout/