The Caching Gap(tm)
August 8th, 2007
So my talk at the July RoRo meetup was mostly about caching rails apps, and how we go about it. The main part of our site is the tournament sweeper that monitors everything that can belong to a tournament, articles, scores, courses etc. It also removes some of the busiest pages on the site, namely the front page and the News and Tournaments page.
This has worked all well and good, that was until we got huge amounts of traffic due to our coverage of the US PGA Bridgestone Invitational, the boss and the photographer where over there taking photos and video, most of the articles on that tourney have 15+ images on them.
What happened next was pretty inevitable, it was always going to happen just didn’t quite know when. I call it:
The Caching Gap
It’s the time between when you expire a cache and the next time its written out, The Caching Gap is usually closed up by the next poor bastard to come along and request the page, he’ll be sitting there twiddling his thumbs while the DB gets spanked and the cache rendered. So, what happens if that page takes, say 2-4 seconds to load and that page is getting >200 requests a second? I’ll tell you what happens, all your mongrels go “ok, no cache file present, better spank the DB and write one out”. Once this happens as many times as mongrels you have, things get messy. Enter my newest, and first, plugin, Cache Fixer.
The plugin itself only really does one minor thing, it allows you to hand in a force attribute to the cache method. At present, the cache method looks for the existence of the fragment you’re trying to cache and if it exists, just uses it, if not it’ll render and cache a new one. The changes are backwards compatible as the new force argument is false by default. Here’s an example:
1 2 3 4 5 6 7 8 9 10 |
## The Controller class FrontpageController < ApplicationController def index @expire = params[:special_param_to_hose_cache] == "1" ? true : false if !read_fragment("frontpage") || @expire ... heavy lifiting goes here end end end |
1 2 3 4 5 |
## The View <% cache("frontpage" , @expire) do %> ... render page here ... <% end %> |
The @expire variable get set in the controller and used in the view, thus allowing a single call to the method to re-render the cache. This solved half the problem. The other half is sweeping. I first tackled this in a very punk, quick-and-dirty way.
1 2 3 4 5 6 7 |
## The Quick and Dirty Sweeper class TournamentSweeper < ActionController::Caching::Sweeper observe ...models... def after_save(record) system("wget #{LIVE_URL}?special_param_to_hose_cache=1 -O /dev/null &") end |
This worked quite well, LIVE_URL is a constant that is set in each rails environment and this was even quite fast due the the & sending the process into the background however not very railsy, not very railsy at all.
After a bit of head banging, the good folk in #ror_au gave me a few heads up, specifically toolmantim and freelancing_god. This is what I came up with.
1 2 3 4 5 6 7 8 9 10 11 |
## The more Railsy Sweeper class TournamentSweeper < ActionController::Caching::Sweeper observe ...models... def after_save(record) require 'action_controller' require 'action_controller/integration' sess = ActionController::Integration::Session.new sess.host! LIVE_HOST sess.get '/?special_param_to_hose_cache=1 end |
This code was yoinked from the console app on rails, works a treat. Obviously LIVE_HOST is a constant like LIVE_URL is in my app.
So that is how I solved my caching problem, I’m throwing it out there to anyone who can think of a better or faster way to do this. What I may do next is flick the re-caching to a DRB server.
The plugin is available at http://rails-oceania.googlecode.com/svn/mattallen/plugins/cache_fix/ it’s not tested beyond me playing around. The plugin includes this patch too. Solves a nasty cache_sweeper bug when called with the :only param
Sorry, comments are closed for this article.