my.urgas.eu

RSS
May 6

Getting rid of the AR callbacks

Problem

When writing a small rails application, it is really easy to use ActiveRecord callbacks. You want to send a welcome message after user signs up? Add after_create callback and voila.

Your model might look something like this:

class User < ActiveRecord::Base
  validates :name, :presence => true

  after_create :send_welcome_email

  private

  def send_welcome_email
    UserNotifier.welcome(self).deliver
  end
end

It seems nice so far. Lets also add some tests to ensure it works:

require "spec_helper"

describe User do
  it "should send a welcome email after the user is created" do
    user = User.new(:name => "John")

    notifier = stub
    notifier.should_receive(:deliver)
    UserNotifier.stub(:welcome).with(user) { notifier }

    user.save
  end
end

This works, but there are some issues:

  1. It’s slow. Saving records to the database is something we don’t want to do.
  2. We have to stub out welcome message part in every test that creates an user.
  3. We also know that the site administrator later wants to add users manually using admin panel where welcome messages shouldn’t be sent.

There are some solutions for these issues:

  1. Instead of saving the user we can use user.run_callbacks(:create) { false }.
  2. We can build a helper method that stubs out sending welcome message.
  3. We can add an if condition to the callback that checks some attr_accessor variable.

There’s something common with each of these points: they suck! The reason why they suck is because they shouldn’t be in the model at all.

Solution

Instead of adding after_create callback just create a separate class that handles this logic. Lets create a new directory called services and add a new class:

class UserRegistrationService
  def initialize(name)
    @user = User.new(:name => name)                                            
  end
                                                                               
  def register                                                                 
    if @user.save                                                              
      send_welcome_email                                                       
    end
  
    @user                                                                      
  end

  private

  def send_welcome_email
    UserNotifier.welcome(@user).deliver
  end
end

Testing this class is much easier. We don’t even have to load rails.

require_relative "../../app/services/user_registration_service"

class User; end
class UserNotifier; end
  
describe UserRegistrationService do                                            
  let(:user) { stub(:save => true) }                                           
  let(:notifier) { stub(:deliver => true) }                                    
      
  before do
    User.stub(:new).with(:name => "John") { user }
    UserNotifier.stub(:welcome).with(user) { notifier }                        
  end

  it "returns the user" do
    UserRegistrationService.new("John").register.should == user
  end 
    
  it "sends a welcome message after the user is created" do
    notifier.should_receive(:deliver)

    UserRegistrationService.new("John").register
  end
  
  it "does not send a welcome message when saving the user fails" do               
    user.stub(:save) { false }
    notifier.should_not_receive(:deliver)
    
    UserRegistrationService.new("John").register
  end
end

There are many testing benefits for this approach:

  • We don’t have to load rails. (no spec_helper loaded). This means the testing time is really short.
  • There is no need to save anything to the database. Again, fast tests.
  • We don’t have to worry about stubbing out the welcome message part in the other tests. We only have to worry about it in one place.
  • We can use the same class in the integration testing if we need to populate the database.

Other benefits:

  • The User model is clean. It is easier to maintain shorter classes.
  • If we don’t want to send emails, we can just use some other class or the AR model directly.

When is it okay to use callbacks?

In my opinion - never.

I really like José Valim tweet: If you want to skip an Active Record callback, it probably shouldn’t be a callback.. He later added: Scratch that. Most of your Active Record callbacks probably shouldn’t be a callback..

Where to now?

I suggest reading a free book called Object on Rails.

If you already haven’t, then take a look at Destroy All Software screencasts.

There are also a few older blog posts that I found interesting regarding this topic: ActiveRecord’s Callbacks Ruined My Life and Crazy, Heretical, and Awesome: The Way I Write Rails Apps

An idea about asset pipeline

Rails 3 way

Usually we structure our Rails 3+ assets like this:

app
  assets
    stylesheets
      global.css
      comments.css
      posts.css
    javascripts
      global.js
      comments.js
      posts.js
  views
    comments
      ...
    posts
      ...

comments.js and comments.css are specific for comments controller and its views.

Keeping controller specific assets in the views directory

app
  assets
    stylesheets
      global.css
    javascripts
      global.js
  views
    comments
      comments.css
      comments.js
      ...
    posts
      posts.css
      posts.js
      ...

What’s the point?

  • Keeping assets closer to views that actually use it.
  • Suggesting users to use more structured assets. I sometimes find a project that has only one huge global.js and one huge global.css

What do you think about this?

Asset pipeline

Please feel free to comment and suggest your ideas.

Speeding up RSpec test suite: Tip #3

Assuming you are using ruby 1.9.3-p0 at the moment, then I suggest applying funny-falcon patch (https://gist.github.com/1688857). This made my rails start up faster and also the overall runtime.

This is also included in the RVM. To install it, just run:

rvm get head
rvm install 1.9.3-falcon
rvm use 1.9.3-falcon

I also suggest adding following to your shell rc file (bonus section from the gist). This basically calms down the garbage collector. It has almost same effect as disabling GC completely in the specs (tip #2).

export RUBY_HEAP_MIN_SLOTS=1000000
export RUBY_HEAP_SLOTS_INCREMENT=1000000
export RUBY_HEAP_SLOTS_GROWTH_FACTOR=1
export RUBY_GC_MALLOC_LIMIT=1000000000
export RUBY_HEAP_FREE_MIN=500000

Speeding up RSpec test suite: Tip #2

Got lots of RAM? Disable GC.

I have this snippet in my spec_helper.rb

  if ENV["RUBY_DISABLE_GC_FOR_SPECS"]
    config.before(:suite) do
      puts "GC disabled"
      GC.disable
    end
  end

And in my shell rc file I have:

export RUBY_DISABLE_GC_FOR_SPECS="true"

Warning: If you have many selenium tests, then do not disable it completely. You’ll just run out of RAM.

Speeding up RSpec test suite: Tip #1

If your test suite is database heavy and you are using postgresql, then turn off fsync and synchronous_commit.

fsync - PostgreSQL server will try to make sure that updates are physically written to disk (link). In the development machine we usually don’t care about power failures and data loss, especially with the test data.

synchronous_commit - Specifies whether transaction commit will wait for WAL records to be written to disk before the command returns a “success” indication to the client (link).

You can change these settings in the postgresql.conf. Just set:

fsync = off
synchronous_commit = off

My test suite time with default options (using ssd): 105 seconds.

My test suite time with fsync and synchronous_commit turned off: 30 seconds.