Rails Caching: Sweepers, Controllers, and Models

Posted by Lance Ivy Tue, 04 Mar 2008 22:42:00 GMT

When you start implementing caching mechanisms into your Rails application, you’re going to have to figure out Rails’ Sweepers. These sweepers confused me for a bit and I’d like to offer clarification for anyone else that might have similarly misunderstood the documentation.

From the documentation:
Sweepers are the terminators of the caching world and responsible for expiring caches when model objects change. They do this by being half-observers, half-filters and implementing callbacks for both roles.

The second sentence is what confused me until just today when, while investigating an odd exception email, I dug into the source code. But once I understood what sweepers actually did, the first sentence made even less sense than the second. Further, the example in the documentation is even more misleading. Oh well.

Here’s the scoop: sweepers observe both your models and your controllers. They’re not half-this and half-that, they’re both. You can define model callbacks (after_save, after_update, etc.), and you can also define before/after callbacks for any controller action (e.g. after_list_create).

Here’s the rest of the scoop: if you only define model callbacks, you don’t need to call the cache_sweeper method on your individual controllers. In fact, you may not want to. You can instead add your sweeper directly to config.active_record.observers in your config/environment.rb. There are only two reasons you’d want to call cache_sweeper to register your sweeper: first, if you want to add callbacks to a controller’s actions; and second, if you need access to the current controller in your sweeper’s model-callback methods (justifiable when you need the controller to calculate fragment cache paths, for example). But in the second case, you want to call cache_sweeper from your ApplicationController. Why? Because otherwise you’ll end up observing models that might change from controllers you didn’t explicitely add the cache_sweeper line to, and will eventually rely on a controller variable that doesn’t exist.

Summary:

  • Sweepers may implement all ActiveRecord::Observer callbacks. To make these work, you could simply add your sweeper to config.active_record.observers.
  • Sweepers may create before/after callbacks for any controller action. To make these work, you need to use the cache_sweeper method on the controllers in question.
  • If your sweepers implement model callbacks but need to refer to the controller for any reason, you want to add the cache_sweeper method to the ApplicationController.

Posted in  | Tags , , , , , , ,  | 2 comments

Comments

  1. Jonathan Frank said 3 months later:

    Nice post. I never really understood why sweepers seem so tightly integrated to controllers in Rails. A particularly maddening example of this is the ‘method_missing’ definition in the Sweeper class:

    def method_missing(method, *arguments) return if @controller.nil? @controller.send!(method, *arguments) end

    To say nothing of how nauseating this implementation is (or I guess just that one thing), it’s just a fact that this method is what’s used to be able to call ‘expire_page’ and ‘expire_fragment’ from the sweeper. So no controller, no page expiration.

    That means that if you write a rake task that does a bunch of things like:

    person.name = “Bob” ; person.save!

    then the pages in cache tied to the Person model will not expire because there’s no controller object for the sweeper (and as shown in the method_missing call above, it will just return silently).

    Any insights on this implementation decision (the sweeper/controller coupling)?

  2. Lance said 3 months later:

    It could definitely use more cleanup. There’s an :expire_page method on the controller instance and class both, but they’re not identical. And :expire_fragment is only added to the controller instance, so you’re right: you need the @controller to utilize those methods.

    I’d guess that this was just the first good-enough implementation the Rails devs figured out, and everyone since then has figured it was easier to use the system than try and clean it up. I’d venture that the right way to clean it up would be to move all of the expiration-related methods into a separate module that could be mixed into the Sweeper class.

    I’ve had the most success creating my own BaseSweeper (e.g. in lib/base_sweeper.rb), and adding whatever methods I needed in order to be able to expire things properly even w/o a controller.

    Strike that: I’ve had the most success not even using the expire_fragment/expire_action/expire_page methods, but instead incrementing version numbers that get mixed into my cache keys. This only works well with memcache, though.

(leave url/email »)

   Comment Markup Help Preview comment