One of the many benefits of side projects is that you get to try out new things. In my job I can’t screw around too much—I’ve got a site to run. But with side projects, I can play with new APIs and try out ideas. Lately, Twistr has been my playground.
Twistr. Twitter + Flickr = LOLs?
In the case of Twistr, shared hosting is the mother of invention. To give Twistr “teh snappy” on crappy shared hosting, I wanted to use page caching. Page caching is the simplest and fastest Rails caching mechanism—it writes the result of a request to an HTML file which is served by the web browser for subsequent requests. Rails is bypassed entirely.
Page caching: First request
Page caching: subsequent requests
The limitation of page caching is that you’ve got to show the same thing to every user. No dynamic content. But you can bend the rules a bit. Here’s what I did to create a page caching solution that works pretty well and allows Twistr to perform solidly on shared hosting.
I forked the plugin and removed the dependence on the JSON gem and Prototype to make it more lightweight and easier to deploy on Dreamhost.
Caching paginated results
On Twistr, the WIN and FAIL pages show a list of mashed-up photos, ordered by votes. Twistr uses will_paginate, but by default will_paginate is not compatible with page caching because it creates links with query parameters (for example
Fortunately, this is easy to fix!
Just add a route with a
:page path like this:
map.win_page '/win/:page', :controller => 'photos', :action => 'win'
Now each results page will get a separate page cached file (for example
Expiring page caches by time
I decided to use a time-based approach to expire the page caches. This happily punts on the hard problem of figuring out which pages actually need to be purged from the cache.
Photo pages are never expired. Using Cacheable Flash means I do not need to have these pages be dynamic. When the site design changes, I simply delete the photo pages and allow them to regenerate.
Cache expiration for the other pages is triggered by a user casting a vote. I created a cache sweeper that observes the
after_create to do this.
The home page is a photo page, but it needs to be expired periodically so that it doesn’t always have the same image. When a vote is cast, the cache sweeper deletes the home page if it is more than 20 minutes old.
The win/fail pages are the only pages that actually change because of user input. Up to the second results are not necessary, but I want them to be fairly current. When a vote is cast, the win and fail pages are expired if they are more than 5 minutes old.
Note that this only happens after a vote is cast—if no vote is cast, the pages will stay page cached forever.
A better solution might use cron to purge the files every 5 minutes, then fire up a GET request to warm up the cache for the next person who requests the resource (attempting to avoid the dog pile effect).
Using page caching, I can get adequate performance from shared hosting. Almost every request on the site serves a cached page. For most users, only submitting a vote will hit Rails.