Easy SuckerPunch Asynchronous Mailers
For a Rails side project recently I finally had the need for a background task handler. Now traditionally I’d jump at Sidekiq, and for good reason. It’s sexy, relatively simple, and comes with a ton of niceties like automatic exponential back-off retries. For this project however, at least at this stage, Sidekiq is overkill. We just need to run some ActionMailers out of the render loop, so we’d like to avoid booting up another server if possible.
So, in comes SuckerPunch, a gem built to address exactly this issue. SuckerPunch uses Celluloid to operate a barebones background task handler inside the server process, avoiding the significant memory overhead of running two Rails instances. Now while its worth noting thanks to the Global Interpreter Lock present in MRI, CPU intensive jobs are still liable to slow down your requests, however the GIL does not apply to external I/O, and so the bulk of what makes a mailer slow (negotiating SMTP) doesn’t block the server.
This is great! So now we just need to write a load of jobs for each mailer method we want to run in the background, like so:
Refactor, refactor, refactor!
Clean as this DSL is, its clearly going to get unwieldy. It would be much nicer to be able to have a job that could handle any email for any mailer. Fortunately this isn’t too hard to achieve with just a pinch of meta-programming:
Now we can slightly modify our existing mailer calls slightly to achieve the same effect as before, without a specific job for each of them:
AsyncMailerJob.new.async.perform(UserMailer, :registration, user)
But wait, there’s more!
Hmmm, it seems we can do a little better than that though. The syntax is a little ugly, wouldn’t it be nice if all we had to do to run a mailer asynchronously was (like with SuckerPunch jobs) add an .async
call before calling the message method? If we knock the meta-programming dial up a notch this isn’t actually too difficult. First up we need to build a module that defines the .async
method on a mailer, lets not worry about whats going in there for now:
Nothing too crazy yet, just using the included
callback to allow a module to define methods on the class (as opposed to instances of the class). So what’s going in async
? Lets look at the AsyncMailerJobRunner
to find out:
Remember async
has to return an object that acts like a mailer while actually running the mailer commands asynchronously. To do that, it effectively mocks a mailer, delegating all messages not to the mailer, but instead to the AsyncMailerJob
we wrote earlier. Thanks to method_missing
we don’t have to worry about which mailer method is called, and thanks to splat *args
we don’t have to worry about that method’s footprint. Meta-programming at its most finest!
I hope all that was helpful to you. If you see any improvements or errata please throw me a comment! For the lazier here’s the source all together in its entirety:
Enjoy!