My Five Favorite Things About Rails 3

Over the last few months Rails 3 has really begun to take shape. We've been hard at work building, refactoring, building, and then refactoring all over again, and I'm pretty pleased with how things are going. There's still a lot of work to be done (we're working on it, these things just take time :P ), but I wanted to pause for a bit and talk about some of my favorite features.

I know a lot of you are eager for information on Rails 3. While this isn't a whole lot of code... think of it more like a movie trailer, and keep an eye out here, on the rubyonrails.org blog and on my personal blog for more information as we get closer to the release.

1. New Bundler

A lot of you have had trouble with the various efforts to make it possible to declare gem dependencies in your application and ensure that they're available on your development and production environment. Merb had an early solution to this problem that got about 90% of the way there, but didn't handle a large number of gems very well. This was exposed by the Engine Yard Cloud team, who had some trouble with the Merb gem bundler with a large number of potentially interdependent gems.

The new bundler handles virtually all issues we could think of around bundling gems in Rails applications, and then some.

Rubygems Resolution

This problem arises when you have multiple gems that depend on overlapping versions of some other gem. For instance, let's say you depend on ActiveMerchant 1.4.2 and Rails 2.3.2. ActiveMerchant depends on activesupport >= 2.3.2. Rails 2.3.2 depends on activesupport = 2.3.2. Now let's say you have Rails 2.3.2 and Rails 2.3.3 on your system.

If you go ahead and require "activemerchant", it will see the >= 2.3.2. Since you have 2.3.3 on your system, it will pull it next. Next, you gem "action_controller", "= 2.3.2". Since ActionController depends on activesupport = 2.3.2, it'll see that 2.3.3 was already activated and fail. You'll get the dreaded Gem::LoadError: can't activate activesupport (= 2.3.2, runtime), already activated activesupport-2.3.3.

This problem arises because gems are currently resolved linearly instead of doing an algorithmic dependency resolution. It turns out that doing dependency resolution with overlapping arbitrary ranges over a large set of potential dependencies is a hard problem. It took us a number of iterations over about a year to get it right, but our current solution is simple, elegant, reasonably fast, and most importantly, correct.

Limit Network Dependencies for Deploy

This is something we felt very strongly about with the Merb bundler, and most people seem to be coming around. The basic idea is that you should store your app's dependencies inside your application, not require a connection to Rubyforge, Github, and who knows what else at deploy time.

In addition to the possible risk of a network resource being down, there is the risk that you declared a dependency on something like nokogiri, and a new version was released to Rubyforge since staging, which breaks your code. You could solve this by always hardcoding all of your versions, but it's nicer to be able to declare your conceptual dependencies ("I want Nokogiri -- any version"). Once you pull the latest versions of your declared dependencies into your application, they should stay put, rather than possibly changing at deploy time.

Consistent Environment

By bundling specific .gem files inside of your application, you ensure that every developer, staging, and production are all running with the same environment.

Native Gems

By storing just the .gem file in the application, we can have a command that compiles the native gems for the architecture in question.

2. ActionController Architecture

In Rails 3, ActionController::Base is built on top of ActionController::Metal, a stripped down version of ActionController with support for simple Rack semantics. We then include a series of modules to add support for things like callbacks, rendering, layouts, helpers, and on and on. This means that it's easy for you to start with ActionController::Metal and pull in just the features you want, paying just the performance cost you want.

We even implemented deprecated 2.3 features as a module that gets mixed in to ActionController::Base. If you aren't using any deprecated features, you can simply stop including the module and get some performance back. You also know that you're not using any features you shouldn't be.

3. Responder

Since we got RESTful controllers, it has been very common to see idiomatic controllers that have actions looking like this:

def create
  @user = User.new(params[:user])

  respond_to do |format|
    if @user.save
      flash[:notice] = 'User was successfully created.'
      format.html { redirect_to(@user) }
      format.xml { render :xml => @user, :status => :created, :location => @user }
    else
      format.html { render :action => "new" }
      format.xml { render :xml => @user.errors, :status => :unprocessable_entity }
    end
  end
end

In Rails 3, this will be shrunk down to:

respond_to :html, :xml # class level

def create
  @user = User.new(params[:user])
  flash[:notice] = 'User was successfully created.' if @user.save
  respond_with(@user)
end

What we do is move the idiomatic logic into a new object called a Responder. By default, we use the Responder that ships with Rails, but you can use your own subclass the same way you would set a controller-wide (or application-wide) layout. That way, you can package up your own RESTful idioms into objects and use those.

4. ActiveModel

You've probably heard that things like validations are now available to any Ruby object by simply including a module: it's true. Take a look:

class Person
  include ActiveModel::Validations

  validates_presence_of :name
  attr_accessor :name
  def initialize(name)
    @name = name
  end
end

Person.new.valid?         #=> false
Person.new.errors         #=> {:name =>  \["cannot be blank"\]} # localizable of course
Person.new("matz").valid? #=> true

This is pretty cool, and provides a lot of ActiveRecord functionality for other ORMs. What's really exciting to me, however, is that ActiveModel provides an API that can be used by ORM plugins to hook directly into ActionPack. If an object complies with the ActiveModel API, it will work seamlessly with the rest of Rails, just as well as ActiveRecord works in Rails 2.3.

This requires:

  • Object#to_model: A method that returns an ActiveModel compliant version of the object. In most cases, this will return self
  • Model#new_record?: true if the record has not yet been persisted
  • Model#valid?: true if the record is valid (or the object has no concept of validations)
  • Model#errors: a valid errors object. The errors object should respond to # and #full_messages
  • Model#class.model_name: a valid naming object that responds to singular, plural, element, collection, partial_path, and human. This is used in routing, URL generation, and a number of other areas, and is possibly the most important integration point. If your object implements these APIs, it will work with ActionPack with the same level of support as ActiveRecord. Pretty exciting!

5. Performance

Over the past several weeks, I've spent a lot of time working on reducing Rails' overhead. There's still a lot to be done, but I was able to reduce the overhead of Rails between two and five times, with especially good gains on Ruby 1.9. While this does not improve the time spent in your code, it reduces the overhead in calling partials, rendering collections, rendering templates, and rendering the entire response. This will be especially apparent for action-cached pages where most of the cost of the request is in Rails, not your request.

I've also started to look at general bottlenecks, like stylesheet_link_tag (surprising!) and URL generation in Paperclip, two things that bubbled up in a recent performance analysis of a real application.

Most of the improvement came from finding bottlenecks and then caching them, which can be tricky because of the possibility of dynamic changes. For instance, it is possible to modify the view_paths during any request, so it's hard to cache view lookup, since someone might prepend a path to the lookup order. In this case, my current approach has been to take a slower path, which means that doing dynamic things in Rails 3 is noticeably more expensive than the same operations in Rails 2.3 (where none of this was cached). I'll have some details on what kinds of things to avoid for optimal performance in a later post.

For now, until the next batch of awesomeness, there are my top upcoming features of Rails 3. Feel free to comment with questions on these or other Rails 3 features and topics. As always, I'm happy to help however I can.