Rails 3: Rack-Throttle your Rack API
Sometimes, you will want to decouple your Rails app and place your API in its own special folder in lib. You could be using Sinatra, Grape, or even your own Rack app. The advantage is you can separate out your API code, making it just that much easier to port over to another app. This guide is not intended to show you how to extract an API from your code, but how to throttle an API that has already been extracted. For some tips on how you can mount your API in your Rails app, take a look at the Inductor blog for a quick intro.
Now that you’ve skimmed the basics of mounting your Rack app (e.g. API) in your Rails app without touching your rackup file, you’re all set for the next step: throttling! testing!
Let’s go over a quick example with Cucumber. Don’t worry, I’ll keep it simple.
Go ahead and install cucumber-rails
and database_cleaner
into your Gemfile, if you aren’t using them already.
Note: You can use anything for your Rack app here, Sinatra, Grape, etc. as long as it returns a 200 status code for its base path. If you want to follow along, you can use this dumb rack app. There’s a link to a Github repo at the end of the post.
Here it is, a simple Rack app (a proc!) mounted in your Rails app at “/api”. It always responds “OK”, with status code 200.
# routes.rb
RailsApp::Application.routes.draw do
mount proc { |env|
[200, {}, ["OK"]]
} => "/api"
# your other Rails routes
end
At this point, you can start up the Rails server and hit your “API” at http://localhost:3000/api. You should see the word “OK
” — that means it’s running. You just created a Rack app in your Rails app. Wasn’t that easy?
Now for the cuking. A simple feature to start off:
# features/my_dumb_api.feature
Feature: My Dumb API
In order to retrieve an API response
As a web API developer
I want an API to respond to my requests
Scenario: API is available
When I send a GET request for "http://example.com/api/"
Then the response code should be "200"
And some step definitions:
When /^I send a GET request for "([^"]*)"$/ do |path|
get path
end
Then /^the response code should be "([^"]*)"$/ do |code|
last_response.status.should == code.to_i
end
Now give cucumber a run.
$ cucumber features/my_dumb_api.feature
Using the default profile...
# features/my_dumb_api.feature
Feature: My Dumb API
In order to retrieve an API response
As a web API developer
I want an API to respond to my requests
Scenario: API is available # features/my_dumb_api.feature:7
When I send a GET request for "http://example.com/api/" # features/step_definitions/api_steps.rb:1
Then the response code should be "200" # features/step_definitions/api_steps.rb:5
1 scenario (1 passed)
2 steps (2 passed)
0m0.154s
Excellent! It picked it up right away. Now we can start thinking about throttling. Jump over to your Gemfile and add rack-throttle:
gem 'rack-throttle', :require => 'rack/throttle'
Update your bundle. Now, let’s start with the Cucumber feature this time.
Feature: My Dumb API
In order to retrieve an API response
As a web API developer
I want an API to respond to my requests
Scenario: API is available
When I send a GET request for "http://example.com/api/"
Then the response code should be "200"
Scenario: Exceeding API Query Rate
When I send more than one GET request in a second to "http://example.com/api"
Then the response code should be "403"
And it’s corresponding steps file:
When /^I send a GET request for "([^"]*)"$/ do |path|
get path
end
When /^I send more than one GET request in a second to "([^"]*)"$/ do |path|
# We'll assume this happens in < 1 second
get path
get path
end
Then /^the response code should be "([^"]*)"$/ do |code|
last_response.status.should == code.to_i
end
If we run this through Cucumber, it fails, because we haven’t done throttling yet. Jump to your routes file, and switch it to:
RailsApp::Application.routes.draw do
mount Rack::Builder.new {
use Rack::Throttle::Interval, :min => 1.0
run proc { |env|
[200, {}, ["OK"]]
}
} => "/api"
end
There, we just built a Rack app with middleware (the Rack::Throttle
line), which defers to our Rack app (the proc) when the middleware passes the request onwards. Now when you run Cucumber, everything passes! You may think you’re ready to Cuke out the rest of your API, but you’re about to hit a roadblock — throttling hits all your Cucumber features. I have considered two ways to deal with this:
- Stub out
Rack::Throttle
and tell it which features you specifically want to throttle using Cucumber tags. - Use a separate Rack app for Cucumber testing, and turn throttling on for certain features with Cucumber tags.
I chose the second option. First, I added the @no-throttle tag to the scenarios where throttling was not relevant:
Feature: My Dumb API
In order to retrieve an API response
As a web API developer
I want an API to respond to my requests
@no-throttle
Scenario: API is available
When I send a GET request for "http://example.com/api/"
Then the response code should be "200"
Scenario: Exceeding API Query Rate
When I send more than one GET request in a second to "http://example.com/api"
Then the response code should be "403"
And then a before filter in the steps file:
Before "@no-throttle" do
@app = Rack::Builder.new {
map "/api" do
run proc { |env|
[200, {}, ["OK"]]
}
end
}
end
When /^I send a GET request for "([^"]*)"$/ do |path|
get path
end
When /^I send more than one GET request in a second to "([^"]*)"$/ do |path|
# We'll assume this happens in < 1 second
get path
get path
end
Then /^the response code should be "([^"]*)"$/ do |code|
last_response.status.should == code.to_i
end
Now when you run your Cucumber features, the unthrottled scenarios will use a separate rack app, mounted without throttling. The downside to this method is that your have to keep the Rack app in api_steps.rb
up to date with the one in routes.rb
. A small price to pay, but less work than stubbing out Rack::Throttle
.
You can browse the source code for this example Rails app on Github.