zerosum dirt(nap)

evolution through a series of accidents

zerosum dirt(nap)

Resque Mailer

January 09, 2010 @ 01:47 AM by nap · 6 comments

We've been making heavy use of Chris Wanstrath's Resque library in my latest project. Resque is a Redis-based background job system that Chris built for GitHub. It's easy to use, especially if you're already leveraging Redis in other parts of your infrastructure, and also has a nice Sinatra front-end for monitoring job status.

Anyway, Resque jobs are just Ruby classes that respond to the special perform method. They're placed on a queue — you can place different jobs on different queues — and later, a worker polls the queue, pops the jobs off and performs the task.

We have a number of different asynchronous jobs happening in Mogo, many of which are domain-specific. But one thing we're doing that's very common is using the background system to process mail delivery asynchronously. Because synchronous mail delivery is for jerks.

Outgoing mail in a Rails app is generally handled by ActionMailer, which expects you to implement message delivery types as class methods on an ActionMailer "model" (a butchering of the term). So a typical mailer might look like this:

class Notifications < ActionMailer::Base

  def signup(user_id, sent_at = Time.now)
    @user = User.find(user_id)

    subject    'Welcome to Mogo'
    recipients @user.email
    from       'Mogoterra <noreply@mogoterra.com>'
    sent_on    sent_at
  end

end

Then, to send a signup message, you use the following method call from somewhere else in your codebase:

Notifications.deliver_signup(@user.id)

So this all works good but it's synchronous. You'd like to be able to background these tasks and use Resque. But you don't really want to mess around with the mailer implementation. Right? Me too.

So ResqueMailer does just that.

Following in the footsteps of DelayedJobMailer, ResqueMailer allows you to shift processing of your existing mailers to an async Resque worker without doing pretty much anything. Just install the gem in your Rails project (via Gemcutter) and then mix the Resque::Mailer module into your mailer.

class Notifications < ActionMailer::Base
  include Resque::Mailer

  # ...
end

You'll need to restart your Resque workers and make sure at least one of them is working on the special mailer queue (or * for all queues). Now when you call MyMailer.deliver_signup, the task will be placed on the mailer queue and processed by the first qualifying worker.

It's always nice when a tiny amount of code makes a task transparent. Check out the project page on GitHub for more information.

6 comments so far ↓

  • Marcin Kulik // January 10, 2010 @ 04:09 PM

    Great stuff.

    I've been looking for something like this for Merb but couldn't found... so I've created one: http://github.com/sickill/merbresquemailer

    It's heavily inspired by your work in resque_mailer.

    Best regards!

  • nap // January 10, 2010 @ 07:37 PM

    @Marcin Your Merb version looks great. Thanks for posting a link!

  • Anthony Eden // January 22, 2010 @ 12:15 PM

    Out of curiosity, why is this better than just using a local MTA? "Because synchronous mail delivery is for jerks." doesn't really seem to be a viable reason when your sends start at a local server...so what is a good reason?

  • nap // January 25, 2010 @ 01:10 PM

    This is definitely most useful if you're not using a local MTA or don't have control over that piece of your deployment environment.

    That said, if your use of ActionMailer also involves processing attachments, it might be nice to have that backgrounded in this manner.

    FWIW, we're currently using both this and a local MTA in production, configured to relay mail to an external service for delivery.

  • Kevin // January 26, 2010 @ 09:40 AM

    Very nice, I'm implementing it right now. It may be worth mentioning though, that since the arguments to the mailer "model" are being stored in Redis, they have to be serializable to json. I got bit passing an entire User AR object to my mailer method. No biggie, just refactored it to accept a hash of the user attributes I needed.

  • nap // January 26, 2010 @ 11:20 AM

    I did mention that in the README, but maybe it should be bolded or something for emphasis (probably easy to miss):

    "Don’t forget that your async mailer jobs will be processed by a separate worker. This means that you should resist the temptation to pass database-backed objects as parameters in your mailer and instead pass record identifiers. Then, in your delivery method, you can look up the record from the id and use it as needed."

Leave a Comment