Tag

ruby on rails

2 articles
14605397426_aec6fc0b23_o

Setting up multiple databases in Rails: the definitive guide

There are different reasons why you might consider having multiple databases in your Ruby on Rails application. In my specific case scenario, I needed to store large quantities of data representing user behavior: clicks, pages visited, historical changes, and so on.

This kind of databases generally are not mission critical, and grow much faster (and larger) than most databases. Their requirements are often different: for instance, they need more storage space, are more tolerant in the face of hardware or software failures, and are write-intensive. For these reasons, sometimes it is interesting to separate them from your application's primary database. Often, non-RDBMS databases are chosen for these kind of tasks, something which is however beyond the scope of this article.

I googled and read many different solutions, however I couldn't find one that was able to fully cover how to:

  • Have different and isolated migrations and schemas for every database.
  • Use rails generators to create new migrations for every database, independently.
  • Offer database-specific rake tasks for the most common database operations (i.e. like the ones available for the primary database).
  • Integrate with RSpec's default spec task.
  • Work with Database Cleaner.
  • Work on Heroku.

This is my take on how to solve all of these - and have a fully working multiple database solution for your Rails application.

Create the custom database files

For the purpose of this tutorial, we're going to set up a second database called Stats. To do so, we're going to duplicate how Rails handles the primary database, and stick to conventions.

First of all, create the file config/database_stats.yml and populate it as you do with the primary database's config file. Your file will look something like this:

config/database_stats.yml
development:
  adapter: postgresql
  encoding: utf8
  host: localhost
  pool: 10
  database: myapp_stats_development
  username: postgres
  password:

test:
  adapter: postgresql
  encoding: utf8
  host: localhost
  pool: 10
  database: myapp_stats_test
  username: postgres
  password:

production:
  adapter: postgresql
  encoding: utf8
  url:  <%= ENV["DATABASE_STATS_URL"] %>
  pool: <%= ENV["DB_POOL"] || 5 %>

Note that I've given specific names to the databases, trying to follow as closely as possible Rails' naming conventions. Also, I've set the database production url to an environment variable DATABASE_STATS_URL. This will allow us to easily set this variable to a secondary database when deploying to Heroku.

We're now going to create a directory that will hold the schema and all the migrations of the Stats database, so that it will have its own files clearly isolated from the primary database. We are basically going to duplicate Rails' primary database db directory.

Create the directory db_stats in the Rails root and ensure to copy the structure and files of the primary database db directory within it. You will have something like:

-- db
   |-- migrate
   schema.rb
   seeds.rb
-- db_stats
   |-- migrate
   schema.rb
   seeds.rb

The created files schema.rb and seeds.rb, together with the migrate directory, should just be empty.

Add Rake tasks

To handle the Stats database, and allow for its creation, migrations, schema dumping and other functionalities we're going to need custom Rake tasks. These tasks will provide us with the same functionalities that Rails provides us for the primary database.

Create a new file lib/tasks/db_stats.rake, and paste the following:

lib/tasks/db_stats.rake
task spec: ["stats:db:test:prepare"]

namespace :stats do

  namespace :db do |ns|

    task :drop do
      Rake::Task["db:drop"].invoke
    end

    task :create do
      Rake::Task["db:create"].invoke
    end

    task :setup do
      Rake::Task["db:setup"].invoke
    end

    task :migrate do
      Rake::Task["db:migrate"].invoke
    end

    task :rollback do
      Rake::Task["db:rollback"].invoke
    end

    task :seed do
      Rake::Task["db:seed"].invoke
    end

    task :version do
      Rake::Task["db:version"].invoke
    end

    namespace :schema do
      task :load do
        Rake::Task["db:schema:load"].invoke
      end

      task :dump do
        Rake::Task["db:schema:dump"].invoke
      end
    end

    namespace :test do
      task :prepare do
        Rake::Task["db:test:prepare"].invoke
      end
    end

    # append and prepend proper tasks to all the tasks defined here above
    ns.tasks.each do |task|
      task.enhance ["stats:set_custom_config"] do
        Rake::Task["stats:revert_to_original_config"].invoke
      end
    end
  end

  task :set_custom_config do
    # save current vars
    @original_config = {
      env_schema: ENV['SCHEMA'],
      config: Rails.application.config.dup
    }

    # set config variables for custom database
    ENV['SCHEMA'] = "db_stats/schema.rb"
    Rails.application.config.paths['db'] = ["db_stats"]
    Rails.application.config.paths['db/migrate'] = ["db_stats/migrate"]
    Rails.application.config.paths['db/seeds'] = ["db_stats/seeds.rb"]
    Rails.application.config.paths['config/database'] = ["config/database_stats.yml"]
  end

  task :revert_to_original_config do
    # reset config variables to original values
    ENV['SCHEMA'] = @original_config[:env_schema]
    Rails.application.config = @original_config[:config]
  end
end

This needs a little explanation: let's break up this file in its main sections. First of all, we simply provide "proxies" to standard Rails database tasks, in a newly created Rake namespace stats:db:

RUBY
task :drop do
  Rake::Task["db:drop"].invoke
end

task :create do
  Rake::Task["db:create"].invoke
end

task :setup do
  Rake::Task["db:setup"].invoke
end

task :migrate do
  Rake::Task["db:migrate"].invoke
end

[...]

Then, we loop all of these tasks, and ensure the task stats:set_custom_config  is run before and the task stats:revert_to_original_config  after every one of the "proxy" tasks:

RUBY
# append and prepend proper tasks to all tasks defined in stats:db namespace
ns.tasks.each do |task|
  task.enhance ["stats:set_custom_config"] do
    Rake::Task["stats:revert_to_original_config"].invoke
  end
end

We have to do this since, unfortunately, Rails support for multiple databases isn't that great, hence we need to provide minor hacks to make everything work. For this reason we have to set specific environment and configuration variables to custom values which match our Stats database before we run the "proxy" tasks, and then ensure that the original values are set back once those tasks have been run. The following two tasks do just that:

RUBY
task :set_custom_config do
  # save current vars
  @original_config = {
    env_schema: ENV['SCHEMA'],
    config: Rails.application.config.dup
  }

  # set config variables for custom database
  ENV['SCHEMA'] = "db_stats/schema.rb"
  Rails.application.config.paths['db'] = ["db_stats"]
  Rails.application.config.paths['db/migrate'] = ["db_stats/migrate"]
  Rails.application.config.paths['db/seeds'] = ["db_stats/seeds.rb"]
  Rails.application.config.paths['config/database'] = ["config/database_stats.yml"]
end

task :revert_to_original_config do
  # reset config variables to original values
  ENV['SCHEMA'] = @original_config[:env_schema]
  Rails.application.config = @original_config[:config]
end

Notice how the lines 9-13 set values to the files and directories we have created in the previous steps.

Finally, if you're using RSpec you can add one dependency to the spec task, to ensure that the Stats database is automatically prepared when tests are run:

RUBY
task spec: ["stats:db:test:prepare"]

Once all of this is set up, we can create the Stats database and run its first migration:

BASH
$ rake stats:db:create
$ rake stats:db:migrate

This will generate the Stats database schema file in db_stats/schema.rb.

Add a custom generator

Unfortunately, we cannot simply use Rails' generator ActiveRecord::Generators::MigrationGenerator because it hardcodes the parent directory of the migration (notice the path hardcoded to the directory db/migrate in line 4 here below):

active_record/migration/migration_generator.rb
def create_migration_file
  set_local_assigns!
  validate_file_name!
  migration_template @migration_template, "db/migrate/#{file_name}.rb"
end

Therefore, we need to have a custom generator to create migrations for the Stats database. However, we can still inherit from it and monkey patch this specific function. Create the following generator in lib/generators/stats_migration_generator.rb:

lib/generators/stats_migration_generator.rb
require 'rails/generators/active_record/migration/migration_generator'

class StatsMigrationGenerator < ActiveRecord::Generators::MigrationGenerator
  source_root File.join(File.dirname(ActiveRecord::Generators::MigrationGenerator.instance_method(:create_migration_file).source_location.first), "templates")

  def create_migration_file
    set_local_assigns!
    validate_file_name!
    migration_template @migration_template, "db_stats/migrate/#{file_name}.rb"
  end
end

In line 9 we set the directory base to the Stats database directory. Also, in line 4 we initialize the templates directory and point it at the original one used by the generator we're inheriting from.

With all of this in place, we can now generate migrations for the Stats database:

BASH
$ rails g stats_migration create_clicks
      create  db_stats/migrate/20151201191642_create_clicks.rb

You'll notice that the migration file gets created in the Stats database migrate directory db_stats/migrate. You can edit this file and then run your migrations with the Rake task that we've set up in the previous steps, just as you normally would do with your primary database:

BASH
$ rake stats:db:migrate

Finalize connection and models

We're almost done. Add a new initializer file config/initializers/db_stats.rb  and paste the following:

config/initializers/db_stats.rb
# save stats database settings in global var
DB_STATS = YAML::load(ERB.new(File.read(Rails.root.join("config","database_stats.yml"))).result)[Rails.env]

Notice that we reference the Stats database configuration file that we created in the first step here above. By doing this, we initialise a global variable DB_STATS that holds the current environment's configuration of the Stats database.

Finally, we can set our models' connection to this configuration. For example, let's say that we have a Click  model that corresponds to the migration here above. All you have to do is add one extra line that specifies which connection to use:

click.rb
class Click < ActiveRecord::Base
  establish_connection DB_STATS

end

It's that easy. Your model will now use the database Stats.

If you have multiple models that need to connect to the Stats database, however, you will need to add an extra step. If you were to have another model establishing its own connection to the Stats database, it would have its own connection pool and you might risk getting out of available connections to your Stats database. Therefore, if you have multiple models it is recommended to inherit from a single model, so that all the models connecting to the Stats database will share the same connection pool.

To do so, create the base model that connects to the Stats database:

click.rb
class StatsBase < ActiveRecord::Base
  establish_connection DB_STATS
  self.abstract_class = true
end

You can now inherit in all your models:

click.rb
class Click < StatsBase
end

class View < StatsBase
end

Heroku

As already anticipated, the last step that you need to make this work on Heroku is to set the environment variable DATABASE_STATS_URL  to the database you want to use as Stats. For example, if you created a second database called HEROKU_POSTGRESQL_TEAL_URL all you have to do is to set this database's value using the Heroku toolbelt:

$ heroku config:set DATABASE_STATS_URL=postgres://gsdfjrthjsnaew:gry6OJF6drDjththjkSDngldsf@ec2-116-22-114-221.compute-1.amazonaws.com:5432/hmsrthj24dfgks

And you're ready to go.

Bonus: DatabaseCleaner

If you're using the DatabaseCleaner gem, you can set it to clean the models that use the Stats database too. For example, your spec/rails_helper.rb may look something like this:

spec/rails_helper.rb
ENV["RAILS_ENV"] ||= 'test'
require 'spec_helper'
require File.expand_path("../../config/environment", __FILE__)
require 'rspec/rails'

Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f }

ActiveRecord::Migration.maintain_test_schema!

RSpec.configure do |config|
  config.use_transactional_fixtures = false
  config.infer_spec_type_from_file_location!

  config.before(:suite) do
    DatabaseCleaner.clean_with(:truncation)
    DatabaseCleaner[:active_record, { model: Click }].clean_with(:truncation)
  end

  config.before(:each) do |example|
    unit_test = ![:feature, :request].include?(example.metadata[:type])
    strategy = unit_test ? :transaction : :truncation

    DatabaseCleaner.strategy = strategy
    DatabaseCleaner[:active_record, { model: Click }].strategy = strategy

    DatabaseCleaner.start
    DatabaseCleaner[:active_record, { model: Click }].start
  end

  config.after(:each) do
    DatabaseCleaner.clean
    DatabaseCleaner[:active_record, { model: Click }].clean
  end
end

According to DatabaseCleaner README, it should be possible to set a connection option instead of the model one. Unfortunately, my attempts at this have been unsuccessful. If anyone knows how to do this and avoid specifying a DatabaseCleaner strategy for every model, please let me know.

I hope you've enjoyed reading this, and that my ramblings can be helpful to someone going down this same path. As usual, any suggestions on how to improve any of this are warmly welcome.

Happy multiple db'ing! :)

Continue Reading…
Trains

How to build a Rails API server: Optimizing the framework

I have been developing Rails JSON API applications for quite some time now, and I'd like to share a few of my setups and discuss why I do things this way. I'm starting today a series of articles that will cover up pretty much the steps I take every time I bootstrap a new Rails JSON API application.

One of the first things I do is to ensure I'm optimizing Rails for speed. I basically optimize the framework itself, prior coding any specific application logic.

You may have heard before that "Premature optimization is the root of all evil". However, "Premature optimization is a phrase used to describe a situation where a programmer lets performance considerations affect the design of a piece of code", which "can result in a design that is not as clean as it could have been or code that is incorrect, because the code is complicated by the optimization and the programmer is distracted by optimizing" (source: WikiPedia). This is not what we're doing here: we're just going to apply a few changes to Rails, and then basically forget about those and start coding in a framework that is optimized to serve our API.

Many of Rails functionalities are simply not needed when building an API server, and by stripping down Rails to a bare minimum we can actually achieve pretty significant performance increases.

Greenfield Ruby On Rails

Let's first see what an empty project can achieve. I'm currently using Ruby 2.2.2 and Rails 4.2.1. Let's create a new Rails application:

rails new api_greenfield -T

Let's add a production server. For the scope of this post, it's not really important what we use, as long as it's a server that we can use in production. We are going to benchmark the results we get after applying our changes to Rails, so the absolute values resulting from our benchmarks are not as important as the relative improvements that we see in speed.

We're going to use Puma, as it is now the recommended Ruby webserver by Heroku (and as I host most of my applications there, using it has become my default choice). Add it to the project Gemfile:

Gemfile
source 'https://rubygems.org'
ruby '2.2.2'

gem 'rails', '4.2.1'
gem 'sqlite3'

gem 'puma'

Then bundle install. Create a Puma configuration file config/puma.rb  and set the following basic params:

RUBY
workers 4
threads_count = 1
threads threads_count, threads_count

preload_app!

rackup DefaultRackup
port ENV['PORT'] || 3000
environment ENV['RAILS_ENV'] || 'development'

on_worker_boot do
  # Worker specific setup for Rails 4.1+
  # See: https://devcenter.heroku.com/articles/deploying-rails-applications-with-the-puma-web-server#on-worker-boot
  ActiveRecord::Base.establish_connection
end

We now need to set up a simple response page that we will hit with our benchmarks. We're going to create a controller and an action that responds with a JSON body to the entry point /benchmarks/simple. To do so, let's create benchmarks_controller.rb:

RUBY
class BenchmarksController < ApplicationController

  def simple
    # example from http://json.org/example
    json = {
      glossary: {
        title: "example glossary",
        gloss_div: {
          title: "S",
          gloss_list: {
            gloss_entry: {
              id: "SGML",
              sort_as: "SGML",
              gloss_term: "Standard Generalized Markup Language",
              acronym: "SGML",
              abbrev: "ISO 8879:1986",
              gloss_def: {
                para: "A meta-markup language, used to create markup languages such as DocBook.",
                gloss_see_also: ["GML", "XML"]
              },
              gloss_see: "markup"
            }
          }
        }
      }
    }

    render json: json
  end
end

Set the routes for this controller:

RUBY
Rails.application.routes.draw do
  resources :benchmarks, only: :none do
    collection do
      get :simple
    end
  end
end

Start Puma in production:

BASH
RAILS_ENV=production bundle exec puma -C config/puma.rb

Verify that Rails responds with our JSON body at the chosen entry point:

$ curl -H "Content-type: application/json" http://127.0.0.1:3000/benchmarks/simple
{"glossary":{"title":"example glossary","gloss_div":{"title":"S","gloss_list":{"gloss_entry":{"id":"SGML","sort_as":"SGML","gloss_term":"Standard Generalized Markup Language","acronym":"SGML","abbrev":"ISO 8879:1986","gloss_def":{"para":"A meta-markup language, used to create markup languages such as DocBook.","gloss_see_also":["GML","XML"]},"gloss_see":"markup"}}}}}

The server is up and ready. We can now benchmark our greenfield Rails application running with Puma. We will use the basic Apache Benchmark tool to do so.

$ ab -c 5 -n 10000 -H "Content-type: application/json" http://127.0.0.1:3000/benchmarks/simple
This is ApacheBench, Version 2.3 <$Revision: 1604373
gt; Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ Licensed to The Apache Software Foundation, http://www.apache.org/ Benchmarking 127.0.0.1 (be patient) Completed 1000 requests Completed 2000 requests Completed 3000 requests Completed 4000 requests Completed 5000 requests Completed 6000 requests Completed 7000 requests Completed 8000 requests Completed 9000 requests Completed 10000 requests Finished 10000 requests Server Software: Server Hostname: 127.0.0.1 Server Port: 3000 Document Path: /benchmarks/simple Document Length: 369 bytes Concurrency Level: 5 Time taken for tests: 4.676 seconds Complete requests: 10000 Failed requests: 0 Total transferred: 6990000 bytes HTML transferred: 3690000 bytes Requests per second: 2138.53 [#/sec] (mean) Time per request: 2.338 [ms] (mean) Time per request: 0.468 [ms] (mean, across all concurrent requests) Transfer rate: 1459.79 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 0 0 0.0 0 0 Processing: 1 2 1.0 2 27 Waiting: 1 2 1.0 2 27 Total: 1 2 1.0 2 27 Percentage of the requests served within a certain time (ms) 50% 2 66% 2 75% 3 80% 3 90% 3 95% 4 98% 4 99% 5 100% 27 (longest request)

This is actually not bad at all! A greenfield Rails project is able to sustain 2,138 req/sec. Obviously, this is without any application logic, nor database calls, but it is still a good starting point.

The Rails API gem

The Rails API gem is "a subset of a normal Rails application, created for applications that don't require all functionality that a complete Rails application provides. It is a bit more lightweight, and consequently a bit faster than a normal Rails application. The main example for its usage is in API applications only, where you usually don't need the entire Rails middleware stack nor template generation".  Note that Rails API will be part of Rails 5, but for now we still have to include the gem:

RUBY
source 'https://rubygems.org'
ruby '2.2.2'

gem 'rails', '4.2.1'
gem 'rails-api'
gem 'sqlite3'

gem 'puma'

Don't forget to bundle install. Then, change our benchmarks_controller.rb to inherit from the Rails::API Action Controller:

RUBY
class BenchmarksController < ActionController::API

Also, comment out in application_controller.rb :

RUBY
# protect_from_forgery with: :exception

Let's try a new benchmark (portions omitted):

$ ab -c 5 -n 10000 -H "Content-type: application/json" http://127.0.0.1:3000/benchmarks/simple

[...]

Concurrency Level:      5
Time taken for tests:   4.220 seconds
Complete requests:      10000
Failed requests:        0
Total transferred:      6990000 bytes
HTML transferred:       3690000 bytes
Requests per second:    2369.39 [#/sec] (mean)
Time per request:       2.110 [ms] (mean)
Time per request:       0.422 [ms] (mean, across all concurrent requests)
Transfer rate:          1617.39 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.0      0       0
Processing:     1    2   0.9      2      27
Waiting:        1    2   0.9      2      27
Total:          1    2   0.9      2      27

Percentage of the requests served within a certain time (ms)
  50%      2
  66%      2
  75%      2
  80%      3
  90%      3
  95%      3
  98%      4
  99%      4
 100%     27 (longest request)

We now see a response rate of 2,369 req/sec, which is an increase in performance of ~11% over greenfield Rails. This is a modest improvement, but an improvement nonetheless.

OJ

Rails' default JSON serializer isn't the fastest out there, so let's swap it for Oj:

RUBY
source 'https://rubygems.org'
ruby '2.2.2'

gem 'rails', '4.2.1'
gem 'rails-api'
gem 'sqlite3'

gem 'puma'

gem 'oj'
gem 'oj_mimic_json'

Let's run the benchmark with Oj (portions omitted):

$ ab -c 5 -n 10000 -H "Content-type: application/json" http://127.0.0.1:3000/benchmarks/simple

[...]

Concurrency Level:      5
Time taken for tests:   4.040 seconds
Complete requests:      10000
Failed requests:        0
Total transferred:      6990000 bytes
HTML transferred:       3690000 bytes
Requests per second:    2475.34 [#/sec] (mean)
Time per request:       2.020 [ms] (mean)
Time per request:       0.404 [ms] (mean, across all concurrent requests)
Transfer rate:          1689.71 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.3      0      27
Processing:     1    2   0.8      2      29
Waiting:        1    2   0.8      1      29
Total:          1    2   0.9      2      29
WARNING: The median and mean for the waiting time are not within a normal deviation
        These results are probably not that reliable.

Percentage of the requests served within a certain time (ms)
  50%      2
  66%      2
  75%      2
  80%      3
  90%      3
  95%      3
  98%      3
  99%      4
 100%     29 (longest request)

We can see a small improvement here, which is practically irrelevant (~4%) as we hit 2,475 req/sec. The switch to Oj is going to be more relevant the bigger the JSON objects to serialize are, but at this stage it doesn't hurt to keep Oj in here.

ActionController::Metal

It is now time to give the final boost, by:

  • Removing unnecessary railties.
  • Using Rails' ActionController::Metal instead of the base controllers that our BenchmarkController has inherited from until now.

First, remove unnecessary imports from application.rb (your mileage may vary - this is my standard setup and I've rarely needed anything else):

RUBY
# require "active_model/railtie"
# require "active_job/railtie"
require "active_record/railtie"
# require "action_controller/railtie"
require "action_mailer/railtie"
# require "action_view/railtie"
# require "sprockets/railtie"

Second (and this is what is really going to make a difference), we're going to create a new controller that all of our API controllers are going to inherit from. Let's create our base api_controller.rb :

RUBY
class ApiController < ActionController::Metal
  abstract!

  include AbstractController::Callbacks
  include ActionController::RackDelegation
  include ActionController::StrongParameters

  private

  def render(options={})
    self.status = options[:status] || 200
    self.content_type = 'application/json'
    body = Oj.dump(options[:json], mode: :compat)
    self.headers['Content-Length'] = body.bytesize.to_s
    self.response_body = body
  end

  ActiveSupport.run_load_hooks(:action_controller, self)
end

As you can see, in this controller we define our custom render method. By default, I've already included the three modules that I basically use everywhere:

  • AbstractController::Callbacks which allows you to set callbacks such as before_action  in your controllers.
  • ActionController::RackDelegation which is needed to set the response_body  (called in the render  method).
  • ActionController::StrongParameters which allows you to use Strong Params in your controllers.

Other modules that you might want to include here are, for instance:

  • ActionController::HttpAuthentication::Token::ControllerMethods to use the authenticate_with_http_token  helper method if you are going to use token authentication in your API.
  • ActionController::HttpAuthentication::Basic::ControllerMethods to use the authenticate_with_http_basic  helper method if you are going to use basic authentication in your API.

Now for our benchmarks, let's ensure that benchmarks_controller.rb  inherits from our newly created controller:

RUBY
class BenchmarksController < ApiController

Here are the results of the benchmark that includes all of above changes (portions omitted):

$ ab -c 5 -n 10000 -H "Content-type: application/json" http://127.0.0.1:3000/benchmarks/simple

[...]

Concurrency Level:      5
Time taken for tests:   2.377 seconds
Complete requests:      10000
Failed requests:        0
Total transferred:      7200000 bytes
HTML transferred:       3690000 bytes
Requests per second:    4206.19 [#/sec] (mean)
Time per request:       1.189 [ms] (mean)
Time per request:       0.238 [ms] (mean, across all concurrent requests)
Transfer rate:          2957.48 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.0      0       0
Processing:     1    1   0.4      1       5
Waiting:        0    1   0.4      1       5
Total:          1    1   0.4      1       5

Percentage of the requests served within a certain time (ms)
  50%      1
  66%      1
  75%      1
  80%      1
  90%      2
  95%      2
  98%      2
  99%      2
 100%      5 (longest request)

This time the impact is notable, as we hit 4,206 req/sec.

Final Touch

With our latest ApiController, we are not using the controller that the Rails API gem exposes to us. Therefore, let's remove the gem:

RUBY
source 'https://rubygems.org'
ruby '2.2.2'

gem 'rails', '4.2.1'
# gem 'rails-api'
gem 'sqlite3'

gem 'puma'

gem 'oj'
gem 'oj_mimic_json'

However, the Rails API gem did other interesting things under the hood, such as disabling some unnecessary Rails middleware. Since we removed it, we now need to do so ourselves. Add to application.rb:

RUBY
module ApiGreenfield
  class Application < Rails::Application

    [...]

    # remove unnecessary middleware
    config.middleware.delete Rack::Sendfile
    config.middleware.delete Rack::MethodOverride
    config.middleware.delete ActionDispatch::Cookies
    config.middleware.delete ActionDispatch::Session::CookieStore
    config.middleware.delete ActionDispatch::Flash
  end
end

Running the benchmark returns the previous results, so we can safely say we don't need the Rails API gem anymore.

Conclusions

We have started with a greenfield Rails project, and have gradually applied changes to improve the speed performance of a simple benchmarked application:

[table]
Version,Req/sec,Increase
Greenfield Rails,"2,138",-
+ Rails API Gem,"2,369",+11%
+ Rails API Gem + Oj,"2,475",+15%
+ Oj + ActionController::Metal + Custom middleware,"4,206",+97%
[/table]

Overall, we experienced an increase from 2,138 to 4,206 req/sec, which is doubling the initial performance of a greenfield Rails application.

For additional boosts, you may consider caching techniques (such as partial JSON caching), which are application dependent and are therefore out of scope here.

Happy API'ing!

Continue Reading…