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.

2011-01-26  ::  admin

  • Browse by category: action mailer - delayed job -

Talkback x 2

  1. Ian Lotinsky
    27 January 2011 @ 11:30 am

    I like this a lot. If you could get this to inject itself inside TMail itself, I think this would be a nifty gem for people to easily turn their mailers into DelayedJob Mailers.

  2. marko
    25 October 2011 @ 4:13 am

    I like it a lot! Brilliant!

Share your thoughts







Tags you can use (optional):
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>