Implementing concurrent rails application

- 3 mins

Last week, I had a situation, a concurrent web service call was needed. Let me explain the issue.

Regarding to the following simplified system architecture, there is a Rails application needed a bunch of web-services to be called before respond to the user. These web services have to be called synchronously, response of Rails app will be the aggregation of all web services’ responses.

It is not possible to cache results. All transactions should go through web services.

implementing concurrent rails application

Each web service is completely different in response time, latency, and response format.

In this article, I will explain different ways to implement such functionality by providing benchmarks.

software versions:

ruby: 2.5.0
rails: 5.2.0 # used in API mode

crystal: 0.24.2 # crystal language

ab: 2.4.18 # apache benchmark tools

Following simple app written by crystal language is used as our web services.

require "http/server"

server = HTTP::Server.new(8080) do |context|
  sleep 1
  context.response.content_type = "text/plain"
  context.response.print "ok"
end

puts "Listening on http://127.0.0.1:8080"
server.listen

Api class to interact with web services:

# lib/api.rb

require 'net/http'

class Api
  include Concurrent::Async
  
  def fetch
    Net::HTTP.get(URI.parse('http://localhost:8080/'))
  end
end

Some available methods to implement our scenario:

1. Simple loop to fetch web services


class IndexController < ApplicationController
  def index
    api = Api.new
    (1..10).each do
      api.fetch
    end

    return render json: {status: 'ok'}
  end
end

2. Using concurrent-ruby gem that wraps Thread

class IndexController < ApplicationController
  def index
    operations = []
    api = Api.new
    (1..10).each do
      operations << api.async.fetch
    end

    operations.each do |o| o.wait end

    return render json: {status: 'ok'}
  end
end

3. Spawning child process by Parallel gem

class IndexController < ApplicationController
  def index
    api = Api.new

    Parallel.map((1..10)) do
      api.fetch
    end

    return render json: {status: 'ok'}
  end
end

Benchmark results

method N=1, C=1 N=10, C=10
1 - loop 10.023 20.102
2 - thread 10.03 20.102
3 - process 3.033 6.336

conclusion

First method just runs Apis in sequence, then response time = sum ( response time of each Api

Second method uses Ruby’s Thread, Because of GIL, it is not native OS thread. Regarding to blocking nature of our Apis, it is not useful in this use case. then response time is like previous one.

Third method spawns child processed for each Api call. It will use more memory, take some time to create processes and kill them after finishing the job. But it is the best choice in this scenario.

Mohsen Alizadeh

Mohsen Alizadeh

I am a Software Developer living in Frankf.
My interests span Backend Development with Ruby On Rails && Elixir, System Architecture Design, Sytem Reliability, and System Scalability.

rss facebook twitter github youtube mail spotify lastfm instagram linkedin google google-plus pinterest medium vimeo stackoverflow reddit quora quora