Tuesday, June 9, 2009

View Path Manipulation for Rails with AOP

If you have ever had to support multiple subdomains in Rails, you've probably had the same questions that we all have.

  • Where do we place the switching mechanism and how does it work?

  • Does it affect controllers, or just views?

  • How do helpers factor into it our strategy?


There are tons of examples out there on the net. I've chosen (like many others) to do basic view path manipulation based on some sort of runtime switching. With Rails 2.3, however, the view-caching that happens when you set view_paths with the typical view_path methods (prepend_view_path, view_path=, etc...) is crazy expensive and a bit mysterious (unless you frequently hack around in the framework, but the majority of us don't). The guys at Unspace seem to have arrived at a great solution for their switching needs. I did it a bit differently!

The Problem

ActionController::Base.view_paths looks like an array of file paths. It is NOT an array of file paths! It is a subclass of Array known as ActionView::PathSet containing instances of ActionView::Template::Path. The following examples, for this reason, are prohibitively nonperformant under heavy load with deep view hierarchies.
ActionController::Base.view_paths = ["app/views", "app/views/my_app", "app/views/your_app"]
or
ActionController::Base.prepend_view_path "app/views/my_app"

If you dynamically set the view paths with a bunch of strings, they will be typecast into ActionView::Template::Path objects which cache everything in the view path recursively... on EVERY REQUEST. Totally expensive!

The first obvious optimization (assuming you already have things wired up and are just trying to optimize the view path loading) would be to pre-process those String filepaths and store them so that view_path manipulation doesn't incur that insane expense during a request.

My issue with this approach is that the internals of PathSet caching and its typecasting become implemented in runtime request code. The class definitions of the controllers is the last place that I'd put framework hackery of that complexity. In most of my applications, I have a pretty good division between bootstrap/runtime extensions to the framework and actual run-on-every-request application initialization code. I'd just monkey patch those path classes directly, and set the view paths once at bootstrap time.

My needs for switching in an application are basically...

  1. app/views would be your fallback path, application view paths would be mutually exclusive

  2. application views would be of the form...
    views, views/app1, views/app2, ..., views/appN-1, views/appN

  3. Prioritization of a single app would lead to a view path set of the form...
    views/appX, views, views/app1, .... views/appX-1,views/appX+1,.... views/appN

  4. a per-request based switching mechanism (completely ignoring per-Controller view_paths)


A Solution
# Place these in a bootstrap-only file, like a config/initializers script.
ActionController::Base.class_eval
# Assumes a per-application/request view prioritization, not per-controller
cattr_accessor :application_view_path
self.view_paths = %w(app/views
app/views/application_one
app/views/application_two).map do |path| Rails.root.join(path).to_s end
end

ActionView::PathSet.class_eval do
def each_with_application_view_path(&block)
application_view_path = ActionController::Base.application_view_path

if application_view_path
# remove and prepend the view path in question to the array BEFORE proceeding with the 'each' operation
(select do |item|
item.to_s == application_view_path
end + reject do |item|
item.to_s == application_view_path
end).each(&block)
else
each_without_application_view_path(&block)
end
end

# as usual, lets play nice with anything else in the call chain.
alias_method_chain :each, :application_view_path
end

# Place this in application_controller
class ApplicationController < ActionController::Base
before_filter :set_application_view_path

def set_application_view_path
ActionController::Base.application_view_path = request.subdomains.first # IF you happen to use subdomains to switch (i.e. store.app.com, inventory.app.com)
end
end

To understand this completely, you MUST look at PathSet#find_template and understand how it works. After that, try imagining the cheapest way to influence the search order of the view paths. This method is one of those classic Rails-internals methods that does WAY too much in a single method. The only way to hack it is to override it completely or metaprogram a sneaky hack like this. I prefer the sneaky hack because influencing the iteration of the PathSet object introduces FAR less complexity than overriding or having a set of filters to constantly manage this stuff from the controllers.

Now, I have a personal rule to NEVER be immediately satisfied with with my first solution to a complex problem. Unlike music, where a first take for a guitar solo is usually imbued with an irreproducible spontaneity, my sweet spot for software is usually the second or third immediate refactoring!

I've been hearing about AOP from Ken Pelletier, my highly esteemed colleage (a dude that smiles fondly when he hears mention of Smalltalk) for years now. A sudden idea for a refactoring of this code gave me all impetus that I needed to dig into AOP and Rails. I wanted to see if using the AOP methodology would cleanup the pseudo-AOP-through-Ruby-metaprogramming junk that I typically code. Here's the quick refactoring with Aquarium (AOP for Ruby).

A Better Solution
# Put all of this in a bootstrap-only initializer
ActionController::Base.class_eval do
APP_ONE_VIEW_PATH = "app/views/application_one"
APP_TWO_VIEW_PATH = "app/views/application_two"

cattr_accessor :application_view_path
self.view_paths = ["app/views", APP_ONE_VIEW_PATH, APP_TWO_VIEW_PATH]

# This is where you determine the switching mechanism for your application. Here, it is a simple GET parameter.
# You can probably argue that this specific piece SHOULD be in your actual app_controller class definition, as it is the only piece
# of info pertinent to the rest of your application.
before_filter do |controller|
ActionController::Base.application_view_path = controller.params[:application_two] ? APP_TWO_VIEW_PATH : APP_ONE_VIEW_PATH
end
end

require 'aquarium'
ActionView::PathSet.class_eval do
include Aquarium::DSL
before :find_template do |join_point, object, *args|
object.each_with_index do |path,i|
object.unshift(object.delete_at(i)) if path.to_s == ActionController::Base.application_view_path
end
end
end
# I'll leave the exercise of testing this or implementing it for your particular app up to you.

As you can see, AOP allows the operation to be expressed very cleanly and succinctly. I love the feeling of swiping in some functionality horizontally through the framework code, in a way that doesn't rely on too much internal knowledge of its workings.  AOP seems really powerful but it will probably require quite a bit of discipline to use it properly on a very large Rails system in a performant way. Hopefully more resources on AOP best-practices emerge soon (I mean specifically for Ruby, we are sort of handicapped by the performance hit of using this wonderful framework).

I should apologize for moving through this stuff without as much explanation as it deserves. AOP is pretty badass. I'm going to be digesting all of the literature on this topic for a while! Next time, I'll blog about something Music-related. I think that you'll start seeing the parallels in Music Composition and Software Engineering pretty clearly. On that topic, Jonathan Dahl gave a pretty good presentation on the parallels between music and software at RailsConf this year. It was a great presentation, and everything he had to say was right on the money. For the first time, however, I got the feeling that it was a topic upon which I could expound prolifically. I'm going to try and formulate some interesting content along those lines!

9 comments:

  1. [...] part, now I needed to switch the view path based on the request. So the Googling began and I found this post. The solution there seemed pretty simple so I decided to go for [...]

    ReplyDelete
  2. Thanks for writing, I very much liked

    ReplyDelete
  3. who was shaking his head back and forth knowingly Grissom shifted his eyes over at Brass,

    ReplyDelete
  4. If you are wondering how you can help with this or future events, please contact us . Also, you can contact other

    ReplyDelete
  5. TUskun http://cgE8hcmk9Vvqlosr5wcBa6nk.com

    ReplyDelete
  6. I wonder exactly what Ginger will say about this!

    ReplyDelete