Warning: what you are about to see will offend your sensibilities as an engineer. It should never be used in production or for user-facing or critical client purposes. It is terrible, no-good very-bad hackiness, and also kinda works. I just hope there’s not some simple way to solve this that I totally missed. Now that you’ve been warned, read on!

There’s a problem with Rails page caching – the page has to fully load once in somebody’s browser before the cache kicks in. That means somebody’s request is really slow, or worse, times out. Not so good. Wouldn’t it be nice if something in the background could do all of a page’s data operations, render the view, and then update the cache for that page, without users ever noticing? Well, check this out.

You can render a page in irb via this method:

app.get 'https://www.mywebsite.com/slow/endpoint'; true
response = app.response; true

I use ; true to prevent irb from dumping huge amounts of request / response data onto the console.

Unfortunately, I couldn’t find any way of doing this outside irb that wasn’t very complicated. Our slow endpoint was on a back-end administrative page only; faking the session data in curl would have been annoying. Also, it was exceeding the timeout limits of our production server. So I had to add an ugly line in: Admin::SlowController.set_timeout 60000, which has to be run in the rails environment. So, irb it is! Or script/console in this case.

Here’s the rundown. I set up a shell script cache_generator.sh to run irb with a ruby file, like so:

#!/bin/bash
script/console production <worker/cache_page.rb

Then set up a ruby script cache_page.rb to do some crazy duck-typing to prepare our environment, render the page, and stuff it in Redis (our cache of choice):

Admin::SlowController.skip_before_filter :admin_required, :only => [:long]
Admin::SlowController.set_timeout 60000
app.get 'https://www.heyzap.com/admin/slow/long?ignore_cache=true'; true
response = app.response; true
REDIS.hset('page_caches', 'long', response.body)

This ignores the required filters, runs as long as it needs to, renders the page and stores it. Now all that’s left is to display it. In our controller method, I just added the following at the top:

def long
  # don't use cache
  if page = REDIS.hget('installs_cache', 'main') & !params[:ignore_cache]
    render :inline => page and return
  end
  # ...
  render :action => :long
end

That render :action => :long at the end is important - that’s necessary for the script/console code to work correctly. You might also want to skip the cache if there’s a fancy query string or something - maybe if params.count > 2, so if there are any crazy options the option-less cache won’t be used.

Last step, set up a cron job to run your script every now and then. I set ours for every five minutes, so we won’t be too far off real data.

*/5 * * * * cd /home/deploy/myapp/current && worker/cache_generator.sh

And boom! Your page now re-generates regularly, and is cached whenever users actually hit it. Wacky, hacky, and fun!