Streaming API with Rails and ActiveRecord
Sometimes there are cases where the response is too big; one way to reduce the memories and time used on the server(as well on client’s side!) is to support HTTP streaming. This article gives a better introduction and informative overview.
Here I will show some code snippets to making a streaming API in Rails.
In Rails 4.2, we use ActionController::Live, which is quite well documented.
describe ' GET /my_stream' do
let(:expected_records) do
SoftwareEngineers.where('job_satisfaction > 9.0').select(:id, :title, :name)
end
before { get '/my_stream' }
it 'returns one line for each processed lists' do
expect(response.code).to eq '200'
response_lines = response.body.split('\n')
expect(response_lines.count).to eq expected_records.count
expected_records.each do |e|
expect(response.body).to match("#{e.id} #{e.title} #{e.name}\n")
end
end
end
end
class Api::MyStreamController < ApplicationController
include ActionController::Live
STREAM_SIZE = 128 * 1024 # Using nginx proxy_buffer_size
# GET /my_stream
#
def my_stream
response.headers['Content-Type'] = 'text/plain'
select_query = "SELECT id, title, name FROM software_engineers WHERE job_satisfaction > 9.0"
# Unbuffered db query is needed so that we don't need to load all
# results before writing to stream. We call #query on mysql2.connection object
# directly as ActiveRecord::Base.connection doesn't support unbuffered query.
ActiveRecord::Base.connection_pool.with_connection do |conn|
mysql2_conn = conn.raw_connection
results = mysql2_conn.query(select_query,
stream: true,
cache_rows: false)
body = ""
results.each do |row|
body << "#{row[0]} #{row[1]} #{row[2]}\n"
if body.size > STREAM_SIZE
response.stream.write(body.slice!(0, body.size))
end
end
response.stream.write(body)
end
ensure
response.stream.close
end
end
If nginx is used as the reverse proxy, the following is needed:
location / {
proxy_http_version 1.1;
}
It was a pain debugging nginx config :(