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:
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: