Delay API calls to Twilio with Rails, Active Job and Sidekiq

来源:互联网 时间:1970-01-01

Delay API calls to Twilio with Rails, Active Job and Sidekiq

Performance is key in web applications. Snappy websites make for better user experiences, higher conversion rates and better user retention. A swift application response causes less stress on servers trying to respond to many users too. There are many ways to improve the performance of a web application in Rails and I want to look at one of those today.

Performing long running, blocking tasks during the course of a request is a top way to slow down responses for any web application. I’m talking things like sending emails, generating PDF or CSV files or making HTTP requests to 3rd party APIs. All of these things take a relatively long time compared to other actions normally performed during the course of a request. If you’re using Twilio within an application the last point might stick out at you.

The best thing to do with long running tasks like this is move them away from the request itself and perform them in the background. This allows your application server to respond to requests swiftly and get the job done without affecting performance for your site’s users or tying up web server processes. Since version 4.2 Rails has included Active Job , alibrary which makes it easy to delay long running tasks and perform them in a background queue. In this post we will explore how we can Active Job to speed up our application.

Let’s queue!

In order to show how we can speed up an application’s response times by queueing tasks to happen in the background, we’re going to need an application to test against. Rather than build one up right now, we’re going to use one of the example applications available in theTwilio tutorials.Let’s take a look atClick to Call, a simple form on your website that takes a user’s phone number and calls them back.You can build the application up usingthe tutorialor grab the application from GitHub .

What you’ll need

To complete this you’ll need a few things:

A Twilio account (sign up for afree Twilio accountif you don’t have one already) A Twilio number that can make phone calls A way to tunnel from a public URL to your localhost (I really likengrok) Ruby and bundler installed Redis (check out the quick start guide for redis to get it up and running)

To run the application, grab your Twilio credentials from your account dashboard and the phone number, then:

Clone the application: $ git clone https : // cd to the application: $ cd clicktocall - rails Install the gems: $ bundle install Set your environment variables:


export TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxexport TWILIO_AUTH_TOKEN=yyyyyyyyyyyyyyyyyexport TWILIO_NUMBER=+123456789 Migrate the database (there’s no migrations for now, but this keeps the application happy): $ bundle exec rake db : migrate Run the tests: $ bundle exec rake test Run the server: $ bundle exec rails server In a separate console tab, set up your tunnel $ ngrok http 3000

Now we’re ready to test this application. Open up the application in your browser using the public URL from your tunnel. You’ll see a form asking for your phone number. Enter your number and submit the form, you’ll see a success message and then receive a phone call.

This all looks great, right? “What’s Phil’s problem?”, you might be asking. Let’s take a look at the logs:

That’s right, loading up the home page took just 38ms and starting the call took just over a second. Whilst this is reasonable when you are starting up a project or not dealing with a lot of traffic, over time this is going to hurt.

Kill the server with Ctrl-C and let’s sort this out.

Speeding it up

Like I said in my introduction, the solution to requests that contain long running, blocking tasks like this is to move the task into the background and perform it outside of the web request. We’re going to do that now using Active Job. First, let’s take a look at the whole action we’re dealing with:


def call contact = = params[:phone] # Validate contact if contact.valid? @client = @@twilio_sid, @@twilio_token # Connect an outbound call to the number submitted @call = @client.calls.create( :from => @@twilio_number, :to =>, :url => connect_url # Fetch instructions from this URL when the call connects ) # Let's respond to the ajax call with some positive reinforcement @msg = { :message => 'Phone call incoming!', :status => 'ok' } else # Oops there was an error, lets return the validation errors @msg = { :message => contact.errors.full_messages, :status => 'ok' } end respond_to do |format| format.json { render :json => @msg } end end

The action receives a phone number in the params and creates a ContactTwilio :: REST :: Client object, which we then use to create a call from our Twilio number to the entered phone number, pointing it at the connect_url (see the connect action for what happens here).

I’ve highlighted the API call in the code above. This is our long running task that we need to move out to a background job. So, let’s create a job to handle that. On the command line, enter:


$ rails generate job make_call

This gives us two new files, app / jobs / make_call_job . rb and test / jobs / make_call_job_test . rb . To ensure our job does the right thing, we can copy parts of the tests from the controller test to the job test.


class MakeCallJobTest < Active Job::TestCase test "should initiate a call with the given phone number and url" do twilio_number = '15008675309' to_number = '12066505813' url = '<a href=""></a>' # Set up mocks for the API wrapper objects client = calls = # We expect that a call is created with this hash as the argument calls.expect(:create, true, [{:from => twilio_number, :to => to_number, :url => url}]) # The client should just return the calls resource client.expect(:calls, calls) MakeCallJob.class_variable_set(:@@twilio_number, twilio_number) Twilio::REST::Client.stub :new, client do assert, url) end client.verify calls.verify endend

If we run the tests now, we’ll see a failure.

So let’s fix it.Thankfully all the code we need is already in the controller as I showed earlier. So we need to update the MakeCallJob . Active Job classes need only define two things, the queue name, which we can leave as : default , and a perform method. We can move the API request from our controller over to the perform method in our job.


class MakeCallJob < ActiveJob::Base queue_as :default # Define our Twilio credentials as instance variables for later use @@twilio_sid = ENV['TWILIO_ACCOUNT_SID'] @@twilio_token = ENV['TWILIO_AUTH_TOKEN'] @@twilio_number = ENV['TWILIO_NUMBER'] def perform(to, url) client = @@twilio_sid, @@twilio_token # Connect an outbound call to the number submitted call = client.calls.create( :from => @@twilio_number, :to => to, :url => url # Fetch instructions from this URL when the call connects ) endend

Run the tests again and they will pass.Good stuff, we’re halfway through. Now the controller needs changing too. We need to ensure that instead of placing the call using the API, the action will add a job to the queue with the right arguments. We can do this with the assert_enqueued_with assertion. Let’s open up test / controllers / twilio_controller_test . rb and change the following test:


class TwilioControllerTest < ActionController::TestCase ... test "should initiate a call with a real phone number" do to_number = '12066505813' assert_enqueued_with job: MakeCallJob, args: [to_number, connect_url] do post :call, :phone => to_number, :format => 'json' assert_response :ok json = JSON.parse(response.body) assert_equal 'ok', json['status'] assert_equal 'Phone call incoming!', json['message'] assert_enqueued_jobs 1 end end ...end

In order to use this fancy assert_enqueued_with test assertion, we need to include the Active Job test helpers. At the top of the test, insert one extra line:


class TwilioControllerTest < ActionController::TestCase include ActiveJob::TestHelper ...end

Run the tests again and we’ll see another failure.

We need to update the controller to queue the job instead of placing the call. Active Job classes define an instance method called perform but to place jobs on the queue we the class method perform_later . By default perform_later will place the job on the queue to be processed as soon as there is a worker available. You can also schedule jobs to run at a set time in the future using the set method followed by perform_later , like MakeCallJob . set ( wait_until : 3.days.from_now ) . perform_later .

As we want this job to run as soon as possible, let’s update the controller to call MakeCallJob . perform_later with the two arguments we defined for the job; the number to call and the URL for Twilio to request when the call is answered. Open up app / controllers / twilio_controller . rb and replace the lines where the Twilio :: REST :: Client is created and used with that call to the job class.


def call contact = = params[:phone] # Validate contact if contact.valid? MakeCallJob.perform_later(, connect_url) # Lets respond to the ajax call with some positive reinforcement @msg = { :message => 'Phone call incoming!', :status => 'ok' } else # Oops there was an error, lets return the validation errors @msg = { :message => contact.errors.full_messages, :status => 'ok' } end respond_to do |format| format.json { render :json => @msg } end end

Run the tests again and you’ll see success. Let’s test this out for real now. Start up the server again, enter your phone number and wait for the call.

The call comes through correctly. But the log is still showing more than a second to process this. What is happening?

Choosing a queue

Active Job is great because it gives us a very simple interface to queue up jobs. However, out of the box Active Job only supplies a default implementation that immediately executes jobs inline. In order to move the work into the background we need to supply a different back end. Active Job supports a number of popular Ruby job queues including Sidekiq , Resque and Delayed Job . There’s a list of all the adapters available in the Active Job documentation as well as their features.

I like Sidekiq, it uses Redis as a store for the jobs (Delayed Job integrates well with Active Record, but this can be slow) and thread based workers (Resque uses process based workers, which can take up a lot more memory). We’ll complete this example using Sidekiq, but I encourage you to read up on the options and choose the one that is right for your application.

Open up the Gemfile and add Sidekiq as a dependency. Add the line beneath gem 'twilio-ruby' :


# other gems# Validate phone numbers easilygem 'phony_rails'# Twilio REST API. gem 'twilio-ruby'# Sidekiq to use with Active Jobgem 'sidekiq'

Run $ bundle install to install Sidekiq and then open up config / application . rb to set the Active Job backend:


module DevedRails class Application < Rails::Application config.active_job.queue_adapter = :sidekiq endend

Now all we need to do is restart our application, start up the Sidekiq process, which will load up some workers to process our jobs, and watch the application speed along. Make sure you have Redis running at this point, Sidekiq will need it. If you have installed Redis, you can start it in a separate console tab with the command:


$ redis-server

To start Sidekiq, open up a new console tab, cd to the application directory, set the same environment variables as the application tab, using your credentials and Twilio number, and start Sidekiq:


$ export TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxx$ export TWILIO_AUTH_TOKEN=yyyyyyyyyyyyyyyyy$ export TWILIO_NUMBER= 123456789$ bundle exec sidekiq

Sweet ASCII art, right?Load up your app again, enter your phone number and wait for the call. You should see little difference in the usage of the application, but check out the application logs.

That’s right, our call action now only takes ~50ms to run. Using Active Job and Sidekiq we have taken all the load off our web application processes and left it for the background process to deal with.

Further considerations

There are a few things that need to be considered when you move work like this from the web process to the background. Like we discussed earlier, the queue backend that you choose is important. For example, if you’re not already using Redis, then adding Resque or Sidekiq will mean adding another dependency for your project. If you need to process a lot of jobs, then using Active Record as part of Delayed Job might not be appropriate either.

There’s the user interface to think of too. In this example if the call creation fails the user won’t see any error, they just won’t get a call. It might be a good idea to track the job progress and update users later with any errors.

Join the queue

In this post we took a controller action that contained a long running task and sped it up its response by moving the work to the background. We saw how Active Job makes it easy to do this within Rails apps and we chose Sidekiq as the backend queue system. Check out the final code here on GitHub .

Now you’ve tried out Sidekiq, it could be time to consider what the other queue backends can do for you. Resque and Delayed Job are the other popular ones, but check out Sneakers , which uses RabbitMQ to store jobs, or Que , which relies on PostgreSQL.

If you want to do the same for sending SMS messages check out the textris gem which wraps up SMS into an Action Mailer like interface, including easy Active Job integration.

Active Job is one of those Rails features that took a long time to arrive, but makes life easier now it’s here. If you’ve got any questions about using background jobs or interesting ways you use queues, drop them here in the comments or hit me up on Twitter at @philnash .

Build More With Ruby 5 Minute Heroes: Twilio Apps with Sinatra & Heroku Announcing New Official Twilio Helper Libraries for PHP, Ruby, Python, .NET and Java Add Twilio SMS Messaging to your Rails App