Ruby on Rails servers are a little infamous for having slow response times. It’s not uncommon for a Rails application to take upwards of 3 seconds to respond to an HTTP request. Some of this slowdown is unavoidable and caused by the additional overhead of running a web server in an interpreted language such as Ruby. But there can be other more controllable factors that may be contributing to slowdown.
As a Rails app matures, controllers can be become slow as more and more has to happen per HTTP request. At Hired, we solved this problem using a pub/sub implementation we call Reactor.
Consider a model named User:
# app/models/user.rb class User < ApplicationRecord # ... end
When a User record gets created, we’ll want to send the user a welcome email. Under the hood, Rails will make at least one, possible HTTP requests to send the email.
# app/mailers/user_mailer.rb class UserMailer < ApplicationMailer default from: 'email@example.com' def welcome_email(user) @user = user @url = 'http://example.com/login' mail(to: @user.email, subject: 'Welcome to My Awesome Site') end end
We also want to sync information about the User to Salesforce, the CRM of choice for our sales team. The actual Ruby implementation of this syncing process isn’t pertinent, but it’s worth noting that the syncing process requires making an external web service call to Saleforce’s API, an operation that can take multiple seconds. It also blocks our server from responding to the User being created until the sync with Salesforce has completed.
# app/services/salesforce_service.rb class SalesforceService def initialize(user) @user = user end def create_or_update_user! # code that communicates with API # ... end end
# app/controllers/users_controller.rb class UsersController < ApplicationController # POST /users def create @user = User.create!(params[:user]) UserMailer.welcome_email(@user).deliver SalesforceService.new(@user).create_or_update_user! render json: @user, status: :created end end
To actually create new Users, we have created the UsersController. When someone wants to sign up for an account on our service, they send a POST request to /users to create an account. Before our server can send a response to the server, it must complete UserMailer.welcome_email(@user).deliver and SalesforceService.new(@user).create_or_update_user!. These synchronous methods take a while to run.
This implementation will work, but it’s slow. Internet users hate slow websites. Fortunately, with the help of a tool we built called Reactor and some small refactoring, we can speed up this process and keep our users happy.
Reactor is Hired’s in-house implementation of a pub/sub framework. It is not the only Ruby pub/sub framework in existence. There are plenty of others. When we began working on Reactor in 2013, there were no other open-source pub/sub implementations for Ruby.
Pub/sub is short for publisher/subscriber. It’s a design pattern for applications where data producers (publishers) push data to a messaging queue. Workers monitor this queue, looking for events that they are subscribed to, and when a match is found, they perform an action in response.
Reactor provides us with a really nice API for firing events in a variety of contexts, writing these events into a Redis queue, and subscribing to these events with background workers.
Events in Reactor can have fields with any name, but there are a few standard fields that all Reactor events share.
To add Reactor to our project we just add:
to our Gemfile and in our shell execute:
Now that Reactor is installed all we need to do is call publish :some_event on an instance of ActiveRecord::Base to fire an event with that record as the actor.
We can now remove the long-running method-calls from UsersController#create. Instead, we use Reactor to handle our communication with Salesforce and send the user a welcome email asynchronously.
# app/controllers/users_controller.rb - UserMailer.welcome_email(@user).deliver - SalesforceService.new(@user).create_or_update_user! + @user.publish :user_created
Instead of performing the slow actions, we just publish a Reactor event and rely on subscribers running elsewhere to notice our event and take an action.
The welcome email is handled by the brand-new UserMailSubscriber:
# app/subscribers/user_mail_subscriber.rb class UserMailSubscriber include Reactor::Subscribable on_event :user_created do |event| user = event.actor UserMailer.welcome_email(user).deliver end end
By calling on_event :user_created, we are telling the UserMailSubscriber to listen for any :user_created events fired in the application and to pass the provided block the data of the events. In the example above, the provided block sends the welcome email. We use the same pattern for Saleforce synchronization:
# app/subscribers/salesforce_subscriber.rb class SalesforceSubscriber include Reactor::Subscribable on_event :user_created do |event| user = event.actor SalesforceService.new(user).create_or_update_user! end end
One of the nice things about Reactor is that both of these subscribers listen for the same :user_created event type, but won’t preempt each other from executing. Both subscribers receive the event.
Now that we’ve refactored our application code to use the Reactor framework, POST requests to /users should be much quicker. Calls to this endpoint will schedule work to be done offline with Reactor rather than completing it within the request.
Using pub/sub lets us build an ecosystem of events and responses. In our example, User is now a publisher. It lets subscribers know that it has been created by firing a :user_created event within UsersController#create. The event is immediately added to a Redis queue. UserMailSubscriber and SalesforceSubscriber, running on our Sidekiq instances, receive the :user_created event and perform their specified work.
There are a number of benefits that using a pub/sub pattern provides us beyond performance improvements. Subscribers don’t need to communicate with publishers directly or synchronously because the all events are passed through a messaging queue.
Pub/sub also loosely couples the subscriber and publisher logic. As long as the publisher can write to the messaging queue and subscribers can read from it. The publisher and subscriber don’t need to be implemented in the same programming language. Another advantage is when we want to make changes to the code of a subscriber, we don’t need to change the publisher.
Pub/sub is also scalable. The number of instances of publishers or subscribers can increase or decrease as long as the messaging queue remains intact. Different publishers and subscribers can be implemented but still use the same events, if it’s appropriate. For example, both UserMailSubscriber and SalesforceSubscriber subscribe to the :user_created event.
Reactor has enabled us to scale our application and serve more people while relying on Sidekiq’s rock-solid core infrastructure. Come work with us!