Content

Rails3 Upgrade #1 – Delayed Mailers

26 January 2011 // Filed under action mailer + delayed job

Sending emails inside your request/response cycle is one of the slower things you can do to users. A while ago, I contrived a way to delay every single email in my application in one fell swoop, without having to change my models and controllers that were actually sending the email. That is, I wanted to keep the existing deliver_* calls in tact, so that my application code continued to look like rails code.

In Rails 2.3.x, using Delayed Job, here is what I did:

For the record, I only have a single ActionMailer subclass, so this simplifies things quite a bit. In the event you have more than one you’ll need to modify the following to also pass the class name to delayed job. (not hard)

class Notifier < ActionMailer::Base

  #...a bunch of mailer methods

  def self.method_missing(method, *args)
   if method.to_s.match(/^deliver_(.*)/)
     Delayed::Job.enqueue(DelayedNotifier.new("create_#{$1}", args))
    else
      super(method, *args)
    end
  end

In a nutshell, we're hijacking ActionMailer's built-in deliver_* methods, changing them to create_* methods, and sending it to delayed job. Here's what Delayed Notifier is:

 class DelayedNotifier < Struct.new(:method, :args)
   def perform
    mail = Notifier.send(method, *args)
    Notifier.deliver(mail)
   end
 end

The class that you send to delayed job must respond to #perform in order for Delayed Job to work correctly. So, delayed job grabs the method name, the arguments that were passed in, builds the TMail object and sends it. Again, since I only have a single Mailer class, I don't need to worry about passing around the name of that class.

Note that the call from your controller/model to deliver_* doesn't actually build a Tmail object.*** All it does is build the DelayedNotifier object and save it to the DB. So our servers are *not* creating emails and sending them once. That would be lame.

Well, this broke with Rails 3.

Rails 3's ActionMailer API is a bit different than the deliver_* methods that we've been used to. It looks like this:

Notifier.password_reset_instructions(user).deliver

Well, crap, now it looks like I *have* to build the TMail Mail object inside of the request/response cycle. I really don't want to do that, sooooo, let's bring deliver_* back. Yea, seriously.

class Notifier < ActionMailer::Base
  def self.method_missing(method, *args)
    if method.to_s.match(/^deliver_(.*)/)
      Delayed::Job.enqueue(DelayedNotifier.new($1, args))
    else
      super(method, *args)
    end
  end
end

And the new, fancy Delayed Notifier struct:

 class DelayedNotifier < Struct.new(:method, :args)
   def perform
     Notifier.send(method, *args).deliver
   end
 end

You use this the same way you used mailers in rails 2.3.x. That is, with deliver_*. Blasphemy, I know, but look on the bright side: Now you don't have to grep your code and update all calls to deliver_*. Sweet!

P.S.

Delayed Job allows you to handle exceptions as you see fit, and hoptoad handles exception notification like no other. It's a match made in heaven.

require 'hoptoad_notifier'

class DelayedNotifier < Struct.new(:method, :args)
  def perform
    Notifier.send(method, *args).deliver
  end

  def failure(job, exception)
    HoptoadNotifier.notify(exception)
  end
end

***
TMail objects are very large (html, plain text, headers, attachments, etc...). If you designed this such that the controller built the TMail object, and then saved that to the DB, you would need to make your delayed_jobs' handler column's type mediumtext or bigger, since you run the risk of there not being enough room in the column for the entire TMail object. Also, you're building the TMail object inside the request/response cycle which is slow and completely unnecessary. Keep it simple, save the method name and the arguments required to build the TMail object and let Delayed Job do the rest.

2 comments  ::  Share or discuss  ::  2011-01-26  ::  admin

Rails3 Upgrade Intro

26 January 2011 // Filed under Uncategorized

I launched into a rails 3 upgrade for my main side project last night. I’m going to use it as an excuse to blog about a bunch of things that I’ve been meaning to write about, but never got around to.

Here’s a quick look at the scope of this app:

$ rake stats
+----------------------+-------+-------+---------+---------+-----+-------+
| Name                 | Lines |   LOC | Classes | Methods | M/C | LOC/M |
+----------------------+-------+-------+---------+---------+-----+-------+
| Controllers          |  1867 |  1616 |      23 |     154 |   6 |     8 |
| Helpers              |   403 |   351 |       0 |      55 |   0 |     4 |
| Models               |  3310 |  2701 |      58 |     331 |   5 |     6 |
| Libraries            |   441 |   401 |       2 |      38 |  19 |     8 |
| Integration tests    |   600 |   340 |       4 |       6 |   1 |    54 |
| Functional tests     |  8763 |  8421 |      22 |      21 |   0 |   399 |
| Unit tests           |  5230 |  4794 |      85 |      10 |   0 |   477 |
+----------------------+-------+-------+---------+---------+-----+-------+
| Total                | 20614 | 18624 |     194 |     615 |   3 |    28 |
+----------------------+-------+-------+---------+---------+-----+-------+
  Code LOC: 5069     Test LOC: 13555     Code to Test Ratio: 1:2.7

 ::  Share or discuss  ::  2011-01-26  ::  admin

jquery.dateTimePicker

10 February 2010 // Filed under javascript

I did a ground up re-write of my date-time picker. It now only relies on jQuery (1.4.1) and jQuery UI (1.8rc1).

Use case is pretty simple right now:

$(function(){
  $('#datetimepicker').dateTimePicker();
  $('#datepicker').dateTimePicker({showTime: false});
  $('#timepicker').dateTimePicker({showDate: false});
});

See the demo for more details. Enjoy!

 ::  Share or discuss  ::  2010-02-10  ::  admin

nested relations in ActiveRecord

13 November 2009 // Filed under active record

I personally don’t think this is all that exciting — but I see this question asked a lot and just want something to point at from now on.

Say you have several ‘nested’ has_many relations:

class State < ActiveRecord::Base
  has_many :cities
end
  
class City < ActiveRecord::Base
  has_many :streets
  belongs_to :state
end

class Street < ActiveRecord::Base
  has_many :houses
  belongs_to :city
end

class House < ActiveRecord::Base
  belongs_to :street
end

How do you get all of the Houses in virginia?

Well, you could do this:

virginia.cities.collect{|c| c.streets}.flatten.uniq.collect{|s| s.houses}.flatten.uniq

... but that's epically lame. It looks like shit and produces an metric ton of queries and as a result is highly inefficient.

How about this:

House.find(
  :all, 
  :include => {:street => {:city => :state}}, 
  :conditions => {'states.id' => virginia.id}
)

This is a single query with joins where appropriate, and it's a finder on House which is what you're getting anyways. Makes sense, no?

4 comments  ::  Share or discuss  ::  2009-11-13  ::  admin

the single best active record learning tool that exists. period. the end.

27 October 2009 // Filed under active record

Every once in a while I see this little code snippet show up somewhere:

script_console_running = ENV.include?('RAILS_ENV') && 
                         IRB.conf[:LOAD_MODULES] && 
                         IRB.conf[:LOAD_MODULES].include?('console_with_helpers')
rails_running = ENV.include?('RAILS_ENV') && 
                !(IRB.conf[:LOAD_MODULES] && 
                IRB.conf[:LOAD_MODULES].include?('console_with_helpers'))
irb_standalone_running = !script_console_running && !rails_running
if script_console_running
  require 'logger'
  Object.const_set(:RAILS_DEFAULT_LOGGER, Logger.new(STDOUT))
end

You see, if you drop this little gem in ~/.irbrc you’ll start getting the SQL output from your various ruby commands. This is an excellent debugging tool, but it’s an even better learning tool.

To the novice the difference between the following two statements may not be readily apparent…

User.find(:first).posts.each{|p| p.comments.do_something}
User.find(:first, :include => {:posts => :comments}).posts.each{|p| p.comments.do_something}

…but as soon as they sit and watch what scrolls by in the terminal they will *very* quickly realize that one of them is *very* wrong.

I submit that the next time you are teaching anyone the basics of AR that this be the very first thing that you introduce them to.

 ::  Share or discuss  ::  2009-10-27  ::  admin

redirect back or default — you’re (probably) doing it wrong

22 October 2009 // Filed under authentication

A basic authentication scheme should go to some length to do a little bit of remembering in the event your user hits a restricted page before they are actually authenticated.

First, some context. I’ve got this in my ApplicationController:

def require_user
  unless current_user
    store_location
    redirect_to login_path
    return false
  end
end

def store_location
  session[:return_to] = request.request_uri
end

def redirect_back_or_default(default)
  redirect_to(session[:return_to] || default)
  session[:return_to] = nil
end

… which allows me to do this in any of my controllers:

before_filter :require_user, :except => [:index, :show]

… and this is cool because now my users get redirected back to where they were going after they log in. I like saving my users a click or two here and there if at all possible.

There’s a problem with this though. What happens if somehow your user POSTs to an action that is behind require user? Well request.request_uri is *not* what you want to redirect to. Why? Because redirect_to is going GET the same url that your user was trying to POST to. And if you’re being a good Rails developer and using resources then you’re just going to smack them in a face with a big fat 404, and that’s not very nice.

I actually managed to do all of this over at github earlier today by trying to comment on a commit without being logged in. Want to see it in action? (until github fixes their ways) First, go to github and logout. Then find any commit and try to make a comment, you can figure the rest out. Go ahead, I’ll wait.

What is that critter anyways? It’s like a half octopus with a lizard tail and whiskers. wut?

Anyways, here’s the simple fix:

def store_location
  session[:return_to] =
  if request.get?
    request.request_uri
  else
    request.referer
  end
end

If they’re GETing something let em keep on GETing it, otherwise take them back to where they were coming from.

This solution is only half-baked though. Any form data that the user has filled out will be gone and they’ll have to redo it. On the other hand you really shouldn’t be showing a form to an unauthenticated user that requires them to be logged in to hit it.

2 comments  ::  Share or discuss  ::  2009-10-22  ::  admin

jquery.dateTimePicker

21 July 2009 // Filed under javascript

A basic authentication scheme should go to some length to do a little bit of remembering in the event your user hits a restricted page before they are actually authenticated.

First, some context. I’ve got this in my ApplicationController:

def require_user
  unless current_user
    store_location
    redirect_to login_path
    return false
  end
end

def store_location
  session[:return_to] = request.request_uri
end

def redirect_back_or_default(default)
  redirect_to(session[:return_to] || default)
  session[:return_to] = nil
end

… which allows me to do this in any of my controllers:

before_filter :require_user, :except => [:index, :show]

… and this is cool because now my users get redirected back to where they were going after they log in. I like saving my users a click or two here and there if at all possible.

There’s a problem with this though. What happens if somehow your user POSTs to an action that is behind require user? Well request.request_uri is *not* what you want to redirect to. Why? Because redirect_to is going GET the same url that your user was trying to POST to. And if you’re being a good Rails developer and using resources then you’re just going to smack them in a face with a big fat 404, and that’s not very nice.

I actually managed to do all of this over at github earlier today by trying to comment on a commit without being logged in. Want to see it in action? (until github fixes their ways) First, go to github and logout. Then find any commit and try to make a comment, you can figure the rest out. Go ahead, I’ll wait.

What is that critter anyways? It’s like a half octopus with a lizard tail and whiskers. wtf.

Anyways, here’s the simple fix:

def store_location
  session[:return_to] = 
  if request.get?
    request.request_uri
  else
   request.referer
  end
end

If they’re GETing something let em keep on GETing it, otherwise take them back to where they were coming from.

This solution is only half-baked though. Any form data that the user has filled out will be gone and they’ll have to redo it. On the other hand you really shouldn’t be showing a form to an unauthenticated user that requires them to be logged in to hit it. If you really insist on doing that have fun saving all those parameters in the session or somethin in between requests. I’d imagine that’ll suck.

 ::  Share or discuss  ::  2009-07-21  ::  admin