I am busy building a SaaS product that has to reliably send large numbers of emails. At ZappiStore we use the Ruby on Rails stack extensively. I think Rails is an excellent choice for building web applications. Rails makes you super productive and the community support is excellent. Naturally I want to use Rails for the web application layer of the system. There are a couple of different options built into Rails for sending emails.
I don’t want a user to have to wait one or two seconds for an SMTP request to finish before they get a response, so sending will have to be done asynchronously. The ActionMailer API in Rails is built for sending emails but ideally I want to pass off the load of sending mails to something that is good at making large amounts of requests and handling them asynchronously. There is support for asynchronous requests in Ruby but it is not as geared towards it as JavaScript is. Also, since Ruby is single-threaded with a global interpreter lock, I want it to do as little processing as possible and rather play to its strengths.
Node.js is built from the ground up with asynchronous operations in mind. Functions are first-class citizens in JavaScript and this makes programming with callbacks easy. Therefore it is a good choice for the creation of a microservice that sends emails. I am using the Mailgun API in order to send emails using POST requests.
Since the web application layer is written in Ruby and I have a microservice written in Node.js, these need to pass data between one-another in some way. The communication solution should have the following properties:
Robust. If a service goes down, the message should be persisted in the queue until it is handled.
Decoupled. The individual microservices should ideally not have any direct knowledge of each other. Each service should communicate via the message broker.
Enter RabbitMQ. Rabbit has been around for a long time and has proven to be a high-performance and reliable message queue. The idea is to use Ruby to push a message onto the queue and then pop the message off of the queue in the Node.js microservice.
Getting Started with RabbitMQ
For those of us that use OS X, installing RabbitMQ is easiest done with Homebrew:
brew install rabbitmq
Once RabbitMQ is installed, start it up as a service with the following command:
brew services start rabbitmq
The Ruby Layer
In order to connect to RabbitMQ from Ruby we are going to use the bunny gem. Add it to your Gemfile:
gem 'bunny', '~> 2.3.1'
Then run a bundle install.
The bunny gem gives us a lot of great functionality out of the box like robust connection handling. If the connection to RabbitMQ drops, bunny will attempt to reconnect until the connection is restored. The bunny gem also gives us an easy API for leveraging RabbitMQ from within our application.
The connection to RabbitMQ should be established at application startup and remain open for the duration of the application’s lifetime. To this end we create a MessagingService singleton class:
By using the Singleton pattern for the MessageService class we can ensure that the connection is not closed resulting in us having to reconnect every time we want to send an email. The EmailQueueService class uses the connection provided by the MessageService class to create a queue for our email pipeline:
The AccountActivationService class is specific to sending account activation emails when new users sign up:
The Node Layer
The package.json for the project is as follows:
The amqplib library is used to communicate with RabbitMQ, lodash as a helper library, mime to get the mime types of the email attachments, request for sending the HTTP requests, winston for logging and yamljs to parse the config.yml file containing settings.
The bulk of the work is done in the worker.js file:
The worker is the started using the main.js file:
Putting it all together
In order to send email accross the queue, the following code is executed from the Rails UsersController:
Summary
Node.js provides first-class support for asynchronous execution of HTTP requests. This makes it a great technology to use for sending emails via the Mailgun API. Ruby on Rails is great for building web applications. We want to leverage the best features of these two technologies together and we implemented RabbitMQ to enable robust communication between the Ruby and the JavaScript layers.