Callbacks, Redux

First off, apologies for the long delay in posting. Last week, I was giving a training down at Integrum in Phoenix. It was a nice three-day training that starting with building a Merb application, and ended with a walk through the BootLoader and Request path. All in all, a very successful endeavor, but definitely one that took up quite a bit of time (and sadly left me with no time for blogging).

At the end of last week, I continued my work on callbacks, which was actually pretty interesting. The initial approach, if you recall, compiled before, around, and after callbacks, with conditions, into a method that could be called. This effectively unrolled the loop and a number of checks into a nice hardcoded method that was about 10x faster than the previous technique. Take a look at my first post on this topic for more.

When I started to look at integrating it with ActionPack, I realized that the API there is a bit different, in an interesting way. In addition to allowing the typical if/unless conditions, ActionController filters allow you to supply :only/:except conditions. At first glance, these compile as follows:

before_filter :authenticate, :except => :index
# to
dispatch_callback :before, :authenticate, :unless => proc {|c| c.action_name == :index }

However, this would force the check every time, even though these conditions are always the same, for each action. In other words, every trip through index will run exactly the same set of callbacks (as for as :only/:except conditions are concerned). I could have hardcoded this optimization into ActionPack's usage of the ActiveSupport callbacks, but I decided instead to make it generic.

After thinking about the problem for a couple of days, I realized that it reduces down to "compile a specialized method for these callbacks, per key." In this case, the "key" is the action name. Now, instead of having the AP code call run_dispatch_callbacks { ... stuff ... }, it calls run_dispatch_callbacks(action_name) { ... stuff ... }. Every time the callbacks are run with a new key, it compiles a version of the method for that key.

The last piece of the puzzle is in specifying these conditions. Now:

before_filter :authenticate, :except => :index
# compiles to
dispatch_callback :before, :authenticate, :per_key => {:unless => proc {|c| c.action_name == :index } }

Per-key options are calculated when the method is compiled, while regular options are compiled into the method. Of course, the vagaries of the compiler are a bit messy, but I plan to clean it up, document it, and integrate it in the next few days.

Tomorrow, I fly out to Chicago to hack with David, Josh, and Rick. Wish us luck!