Scaling :ruby 
with Evented I/O 
Omer Gazit 
[github.com/omerisimo]
Scaling Strategies[] 
<< "Machines" 
<< "Processes" 
<< "Threads" 
<< "Reactor pattern"
Blocking I/O 
start 
wait for I/O [e.g. file,network] 
complete
Non-Blocking I/O 
start 
call I/O operation 
Do something else 
complete 
Kernel 
I/O 
callback()
:sync 
def sync_io(file_name) 
file = File.open(file_name) 
puts "File #{file_name} opened" 
end
:async 
def async_io(file_name) 
file = File.async_open(file_name) 
file.callback do |result| 
puts "File #{file_name} opened" 
end 
end
Reactor pattern 
“The reactor design pattern is an event 
handling pattern for handling service 
requests delivered concurrently… 
The service handler then demultiplexes the 
incoming requests and dispatches them 
synchronously to the associated request 
handlers.” 
-wikipedia
Reactor pattern 
“The reactor design pattern is an event 
handling pattern for handling service 
requests delivered concurrently… 
The service handler then demultiplexes the 
incoming requests and dispatches them 
synchronously to the associated request 
handlers.” 
-wikipedia
Reactor pattern 
IO Stream 
Demultiplexer 
Event Handler A 
Event Handler B 
Event Handler C 
Event 
Dispatcher
Reactor pattern 
A reactor is a single thread 
running an endless loop that 
reacts to incoming events
Reactor pattern 
A reactor is a single thread 
running an endless loop that 
reacts to incoming events
event_loop do 
while reactor_running? 
expired_timers.each { |timer| timer.process } 
new_network_io.each { |io| io.process } 
end
Reactor.when? 
● Proxies 
● Real time data delivery 
(Websockets) 
● Streaming 
● Background processing (MQ listener) 
● High throughput and mostly I/O 
bound
{ 
javascript: "Node.js", 
ruby: "EventMachine", 
perl: "AnyEvent", 
python: "Twisted", 
c: ["libev", "libevent"] 
} 
implementations=
EventMachine 
A toolkit for writing evented applications 
in Ruby
But node.js is so 
much faster... 
Is it really faster?
Simple HTTP Server 
Node.js 
var http = require('http'); 
http.createServer(function (request, response) { 
response.writeHead(200, 
{'Content-Type': 'text/plain'} 
); 
response.send('Hello Worldn'); 
}).listen(8080, '0.0.0.0'); 
console.log('Server running on port 8080');
Simple HTTP Server 
EventMachine 
EM.run do 
EM.start_server "0.0.0.0", 8080 do |server| 
def server.receive_data(data) 
response = EM::DelegatedHttpResponse .new(self) 
response.status = 200; 
response.content_type 'text/plain' 
response.content = "Hello World n" 
response.send_response 
end 
end 
end
Showdown 
results = { 
node: { req_per_sec: 2898, 
req_time_ms: 35 }, 
em: { req_per_sec: 6751, 
req_time_ms: 15 } 
} 
ab -n 1000 -c 100 "http://localhost:8080/" 
* executed on my MacBook Air
RabbitMQ Processor 
Node.js 
var amqp = require('amqp'); 
var connection = amqp.createConnection(); 
connection.on( 'ready', function() { 
connection.queue( 'my_queue' , function(queue) { 
queue.subscribe( function(payload) { 
console.log("Received message: " + payload.body); 
} 
}); 
var exchange = connection.exchange(); 
exchange.publish( 'my_queue' , { body: 'Hello World!' }); 
});
RabbitMQ Processor 
EventMachine 
require 'amqp' 
EM.run do 
connection = AMQP.connect(host: '0.0.0.0') 
channel = AMQP::Channel .new(connection) 
queue = channel.queue( "my_queue" ) 
queue.subscribe do |metadata, payload| 
puts "Received message: #{payload}." 
end 
exchange = channel.default_exchange 
exchange.publish "Hello world!" ,routing_key : "my_queue" 
end
Showdown 
average_time_ms = { 
node: 4285, 
em: 3488 
} 
send and receive 10k messages 
* executed on my MacBook Air
EM can be really fast 
If used correctly!
Never Block the 
Event Loop
Never Block 
Blocking Non-Blocking 
EM.run do 
puts "Started EM" 
sleep 2.0 
puts "Shutting down EM" 
EM.stop 
end 
EM.run do 
puts "Started EM" 
EM.add_periodic_timer( 1.0) do 
puts "Tick" 
end 
EM.add_timer( 2.0) do 
puts "Shutting down EM" 
EM.stop 
end 
end
Never Block 
Blocking Non-Blocking 
require 'net/http' 
EM.run do 
response = Net::HTTP.get(URL) 
puts "Completed HTTP request" 
EM.stop 
end 
require 'em-http' 
EM.run do 
http = EM::HttpRequest .new(URL).get 
http.callback do |response| 
puts "Completed HTTP request" 
EM.stop 
end 
end
module NonBlock 
<< "igrigorik/em-http-request" # Asynchronous HTTP Client 
<< "eventmachine/evma_httpserver" # HTTP Server 
<< "igrigorik/em-websocket" # WebSockets server 
<< "igrigorik/em-proxy" # High-performance transparent proxies 
<< "brianmario/mysql2" # Make sure to use with :async => true 
<< "royaltm/ruby-em-pg-client" # PostgreSQL EM client 
<< "bcg/em-mongo" # EM MongoDB driver (based off of RMongo) 
<< "simulacre/em-ssh" # EM compatible Net::SSH 
<< "pressly/uber-s3" # S3 client with asynchronous I/O adapters 
<< "tmm1/amqp" # AMQP client for EM 
* See full list of protocols at: github.com/eventmachine/eventmachine/wiki/Protocol-Implementations
module NonBlock 
<< "igrigorik/em-http-request" 
<< "eventmachine/evma_httpserver" 
<< "igrigorik/em-websocket" 
<< "igrigorik/em-proxy" 
<< "brianmario/mysql2" 
<< "royaltm/ruby-em-pg-client" 
<< "bcg/em-mongo" 
<< "simulacre/em-ssh" 
<< "pressly/uber-s3" 
<< "tmm1/amqp" 
* See full list of protocols at: github.com/eventmachine/eventmachine/wiki/Protocol-Implementations
Never Block the 
Event Loop
EM.defer 
Defer blocking code to a thread 
EM.run do 
long_operation = proc { 
sleep(1.0) 
"result" 
} 
callback = proc {|result| 
puts "Received #{result}" 
EM.stop 
} 
EM.defer(long_operation, callback) 
end
EM.next_tick 
Postpone execution to the next iteration 
Blocking Non-Blocking 
EM.run do 
(1..10000).each do |index| 
puts "Processing #{index}" 
end 
EM.stop 
end 
EM.run do 
index = 0 
process_index = proc { 
if index < 10000 
puts "Processing #{index}" 
index += 1 
EM.next_tick &process_index 
else 
EM.stop 
end 
} 
EM.next_tick &process_index 
end
EM::Primitives
EM::Deferrable 
EM::Deferrable != EM.defer 
class DeferrableTimer 
include EM::Deferrable 
def wait 
EM.add_timer( 1.0) do 
succeed "result" 
end 
self 
end 
end 
EM.run do 
timer = DeferrableTimer .new.wait 
timer.callback do |result| 
puts "1 second has passed!" 
EM.stop 
end 
end
EM::Connection 
class EchoServer < EM::Connection 
def post_init 
puts "Client connecting" 
end 
def receive_data (data) 
puts "Client sending data #{data}" 
send_data ">> #{data}" 
end 
def unbind 
puts "Client disconnecting" 
end 
end 
EM.run do 
EM.start_server( "0.0.0.0", 9000, EchoServer ) # Listen on TCP socket 
end
EM::Queue 
A cross thread, reactor scheduled, linear queue 
EM.run do 
queue = EM::Queue.new 
queue_handler = proc { |message| 
puts "Handling message #{message}" 
EM.next_tick{ queue.pop( &queue_handler) } 
} 
EM.next_tick{ queue.pop( &queue_handler) } 
EM.add_periodic_timer( 1.0) do 
message = Time.now.to_s 
puts "Pushing message ' #{message}' to queue" 
queue.push(message) 
end 
end
EM::Channel 
Provides a simple thread-safe way to transfer data 
between (typically)long running tasks 
EM.run do 
channel = EM::Channel.new 
handler_1 = proc { | message| puts "Handler 1 message #{message}" } 
handler_2 = proc { | message| puts "Handler 2 message #{message}" } 
channel.subscribe &handler_1 
channel.subscribe &handler_2 
EM.add_periodic_timer( 1.0) do 
message = Time.now.to_s 
puts "Sending message ' #{message}' to channel" 
channel << message 
end 
end
EM::Primitives 
<< EM::Queue # A cross thread, reactor scheduled, 
linear queue 
<< EM::Channel # Simple thread-safe way to transfer 
data between (typically)long running tasks 
<< EM::Iterator # A simple iterator for concurrent 
asynchronous work 
<< EM.System() # Run external commands without 
blocking
EM.run do 
EM.add_timer( 1) do 
json = nil 
begin 
data = JSON.parse(json) 
puts "Parsed Json data: #{data}" 
rescue StandardException => e 
puts "Error: #{e.message}" 
end 
EM.stop 
end 
end 
Error Handling
Error Handling 
EM.run do 
EM.add_timer( 1) do 
http = EM::HttpRequest .new(BAD_URI).get 
http.callback do 
puts "Completed HTTP request" 
EM.stop 
end 
http.errback do |error| 
puts "Error: #{error.error }" 
EM.stop 
end 
end 
end
EM::Synchrony 
Fiber aware EventMachine clients and 
convenience classes 
github.com/igrigorik/em-synchrony
EM::Synchrony 
em-synchrony/em-http 
require 'em-synchrony' 
require 'em-synchrony/em-http' 
EM.synchrony do 
res = EM::HttpRequest .new(URL).get 
puts "Response: #{res.response }" 
EM.stop 
end 
em-http-request 
require 'eventmachine' 
require 'em-http' 
EM.run do 
http = EM::HttpRequest .new(URL).get 
http.callback do 
puts "Completed HTTP request" 
EM.stop 
end 
end
Testing
EM::Spec 
require 'em-spec/rspec' 
describe LazyCalculator do 
include EM::SpecHelper 
default_timeout( 2.0) 
it "divides x by y" do 
em do 
calc = LazyCalculator .new.divide(6,3) 
calc.callback do |result| 
expect(result).to eq 2 
done 
end 
end 
end 
end 
class LazyCalculator 
include EM::Deferrable 
def divide(x, y) 
EM.add_timer( 1.0) do 
if(y == 0) 
fail ZeroDivisionError .new 
else 
result = x/y 
succeed result 
end 
end 
self 
end 
end
EM::Spec 
require 'em-spec/rspec' 
describe LazyCalculator do 
include EM::SpecHelper 
default_timeout( 2.0) 
it "fails when dividing by zero" do 
em do 
calc = LazyCalculator .new.divide(6,0) 
calc.errback do |error| 
expect(error).to be_a ZeroDivisionError 
done 
end 
end 
end 
end 
class LazyCalculator 
include EM::Deferrable 
def divide(x, y) 
EM.add_timer( 1.0) do 
if(y == 0) 
fail ZeroDivisionError .new 
else 
result = x/y 
succeed result 
end 
end 
self 
end 
end
RSpec::EM::FakeClock 
require 'rspec/em' 
describe LazyCalculator do 
include RSpec::EM::FakeClock 
before { clock.stub } 
after { clock.reset } 
it "divides x by y" do 
calc = LazyCalculator .new.divide(6,3) 
expect(calc).to receive(: succeed).with 2 
clock.tick( 1) 
end 
it "fails when dividing by zero" do 
calc = LazyCalculator .new.divide(6,0) 
expect(calc).to receive(: fail).with(kind_of( ZeroDivisionError )) 
clock.tick( 1) 
end 
end
Project Demo 
A proxy for Google Geocode API
Limitations 
● Can only use async libraries or 
have to defer to threads. 
● Hard to debug (no stack trace) 
● Harder to test 
● Difficult to build full blown 
websites.
Caveats 
● Low community support 
● The last release is almost two 
years old
Summary 
● Evented I/O offers a cost effective 
way to scale applications 
● EventMachine is a fast, scalable 
and production ready toolbox 
● Write elegant event-driven code 
● It is not the right tool for every 
problem
EM.next? 
<< ['em_synchrony' + Fiber] 
<< ['async_sinatra' + Thin] # Sinatra on EM 
<< ['goliath'] # Non-blocking web framework 
<< [EM.epoll + EM.kqueue] # maximize 
demultiplexer polling limits 
<< [Celluloid + Celluloid.IO] # Actor 
pattern + Evented I/O
EM.stop 
Thank you 
Code examples: github.com/omerisimo/em_underground

Scaling Ruby with Evented I/O - Ruby underground

  • 1.
    Scaling :ruby withEvented I/O Omer Gazit [github.com/omerisimo]
  • 2.
    Scaling Strategies[] <<"Machines" << "Processes" << "Threads" << "Reactor pattern"
  • 3.
    Blocking I/O start wait for I/O [e.g. file,network] complete
  • 4.
    Non-Blocking I/O start call I/O operation Do something else complete Kernel I/O callback()
  • 5.
    :sync def sync_io(file_name) file = File.open(file_name) puts "File #{file_name} opened" end
  • 6.
    :async def async_io(file_name) file = File.async_open(file_name) file.callback do |result| puts "File #{file_name} opened" end end
  • 7.
    Reactor pattern “Thereactor design pattern is an event handling pattern for handling service requests delivered concurrently… The service handler then demultiplexes the incoming requests and dispatches them synchronously to the associated request handlers.” -wikipedia
  • 8.
    Reactor pattern “Thereactor design pattern is an event handling pattern for handling service requests delivered concurrently… The service handler then demultiplexes the incoming requests and dispatches them synchronously to the associated request handlers.” -wikipedia
  • 9.
    Reactor pattern IOStream Demultiplexer Event Handler A Event Handler B Event Handler C Event Dispatcher
  • 10.
    Reactor pattern Areactor is a single thread running an endless loop that reacts to incoming events
  • 11.
    Reactor pattern Areactor is a single thread running an endless loop that reacts to incoming events
  • 12.
    event_loop do whilereactor_running? expired_timers.each { |timer| timer.process } new_network_io.each { |io| io.process } end
  • 13.
    Reactor.when? ● Proxies ● Real time data delivery (Websockets) ● Streaming ● Background processing (MQ listener) ● High throughput and mostly I/O bound
  • 14.
    { javascript: "Node.js", ruby: "EventMachine", perl: "AnyEvent", python: "Twisted", c: ["libev", "libevent"] } implementations=
  • 15.
    EventMachine A toolkitfor writing evented applications in Ruby
  • 16.
    But node.js isso much faster... Is it really faster?
  • 17.
    Simple HTTP Server Node.js var http = require('http'); http.createServer(function (request, response) { response.writeHead(200, {'Content-Type': 'text/plain'} ); response.send('Hello Worldn'); }).listen(8080, '0.0.0.0'); console.log('Server running on port 8080');
  • 18.
    Simple HTTP Server EventMachine EM.run do EM.start_server "0.0.0.0", 8080 do |server| def server.receive_data(data) response = EM::DelegatedHttpResponse .new(self) response.status = 200; response.content_type 'text/plain' response.content = "Hello World n" response.send_response end end end
  • 19.
    Showdown results ={ node: { req_per_sec: 2898, req_time_ms: 35 }, em: { req_per_sec: 6751, req_time_ms: 15 } } ab -n 1000 -c 100 "http://localhost:8080/" * executed on my MacBook Air
  • 20.
    RabbitMQ Processor Node.js var amqp = require('amqp'); var connection = amqp.createConnection(); connection.on( 'ready', function() { connection.queue( 'my_queue' , function(queue) { queue.subscribe( function(payload) { console.log("Received message: " + payload.body); } }); var exchange = connection.exchange(); exchange.publish( 'my_queue' , { body: 'Hello World!' }); });
  • 21.
    RabbitMQ Processor EventMachine require 'amqp' EM.run do connection = AMQP.connect(host: '0.0.0.0') channel = AMQP::Channel .new(connection) queue = channel.queue( "my_queue" ) queue.subscribe do |metadata, payload| puts "Received message: #{payload}." end exchange = channel.default_exchange exchange.publish "Hello world!" ,routing_key : "my_queue" end
  • 22.
    Showdown average_time_ms ={ node: 4285, em: 3488 } send and receive 10k messages * executed on my MacBook Air
  • 23.
    EM can bereally fast If used correctly!
  • 24.
    Never Block the Event Loop
  • 25.
    Never Block BlockingNon-Blocking EM.run do puts "Started EM" sleep 2.0 puts "Shutting down EM" EM.stop end EM.run do puts "Started EM" EM.add_periodic_timer( 1.0) do puts "Tick" end EM.add_timer( 2.0) do puts "Shutting down EM" EM.stop end end
  • 26.
    Never Block BlockingNon-Blocking require 'net/http' EM.run do response = Net::HTTP.get(URL) puts "Completed HTTP request" EM.stop end require 'em-http' EM.run do http = EM::HttpRequest .new(URL).get http.callback do |response| puts "Completed HTTP request" EM.stop end end
  • 27.
    module NonBlock <<"igrigorik/em-http-request" # Asynchronous HTTP Client << "eventmachine/evma_httpserver" # HTTP Server << "igrigorik/em-websocket" # WebSockets server << "igrigorik/em-proxy" # High-performance transparent proxies << "brianmario/mysql2" # Make sure to use with :async => true << "royaltm/ruby-em-pg-client" # PostgreSQL EM client << "bcg/em-mongo" # EM MongoDB driver (based off of RMongo) << "simulacre/em-ssh" # EM compatible Net::SSH << "pressly/uber-s3" # S3 client with asynchronous I/O adapters << "tmm1/amqp" # AMQP client for EM * See full list of protocols at: github.com/eventmachine/eventmachine/wiki/Protocol-Implementations
  • 28.
    module NonBlock <<"igrigorik/em-http-request" << "eventmachine/evma_httpserver" << "igrigorik/em-websocket" << "igrigorik/em-proxy" << "brianmario/mysql2" << "royaltm/ruby-em-pg-client" << "bcg/em-mongo" << "simulacre/em-ssh" << "pressly/uber-s3" << "tmm1/amqp" * See full list of protocols at: github.com/eventmachine/eventmachine/wiki/Protocol-Implementations
  • 29.
    Never Block the Event Loop
  • 30.
    EM.defer Defer blockingcode to a thread EM.run do long_operation = proc { sleep(1.0) "result" } callback = proc {|result| puts "Received #{result}" EM.stop } EM.defer(long_operation, callback) end
  • 31.
    EM.next_tick Postpone executionto the next iteration Blocking Non-Blocking EM.run do (1..10000).each do |index| puts "Processing #{index}" end EM.stop end EM.run do index = 0 process_index = proc { if index < 10000 puts "Processing #{index}" index += 1 EM.next_tick &process_index else EM.stop end } EM.next_tick &process_index end
  • 32.
  • 33.
    EM::Deferrable EM::Deferrable !=EM.defer class DeferrableTimer include EM::Deferrable def wait EM.add_timer( 1.0) do succeed "result" end self end end EM.run do timer = DeferrableTimer .new.wait timer.callback do |result| puts "1 second has passed!" EM.stop end end
  • 34.
    EM::Connection class EchoServer< EM::Connection def post_init puts "Client connecting" end def receive_data (data) puts "Client sending data #{data}" send_data ">> #{data}" end def unbind puts "Client disconnecting" end end EM.run do EM.start_server( "0.0.0.0", 9000, EchoServer ) # Listen on TCP socket end
  • 35.
    EM::Queue A crossthread, reactor scheduled, linear queue EM.run do queue = EM::Queue.new queue_handler = proc { |message| puts "Handling message #{message}" EM.next_tick{ queue.pop( &queue_handler) } } EM.next_tick{ queue.pop( &queue_handler) } EM.add_periodic_timer( 1.0) do message = Time.now.to_s puts "Pushing message ' #{message}' to queue" queue.push(message) end end
  • 36.
    EM::Channel Provides asimple thread-safe way to transfer data between (typically)long running tasks EM.run do channel = EM::Channel.new handler_1 = proc { | message| puts "Handler 1 message #{message}" } handler_2 = proc { | message| puts "Handler 2 message #{message}" } channel.subscribe &handler_1 channel.subscribe &handler_2 EM.add_periodic_timer( 1.0) do message = Time.now.to_s puts "Sending message ' #{message}' to channel" channel << message end end
  • 37.
    EM::Primitives << EM::Queue# A cross thread, reactor scheduled, linear queue << EM::Channel # Simple thread-safe way to transfer data between (typically)long running tasks << EM::Iterator # A simple iterator for concurrent asynchronous work << EM.System() # Run external commands without blocking
  • 38.
    EM.run do EM.add_timer(1) do json = nil begin data = JSON.parse(json) puts "Parsed Json data: #{data}" rescue StandardException => e puts "Error: #{e.message}" end EM.stop end end Error Handling
  • 39.
    Error Handling EM.rundo EM.add_timer( 1) do http = EM::HttpRequest .new(BAD_URI).get http.callback do puts "Completed HTTP request" EM.stop end http.errback do |error| puts "Error: #{error.error }" EM.stop end end end
  • 40.
    EM::Synchrony Fiber awareEventMachine clients and convenience classes github.com/igrigorik/em-synchrony
  • 41.
    EM::Synchrony em-synchrony/em-http require'em-synchrony' require 'em-synchrony/em-http' EM.synchrony do res = EM::HttpRequest .new(URL).get puts "Response: #{res.response }" EM.stop end em-http-request require 'eventmachine' require 'em-http' EM.run do http = EM::HttpRequest .new(URL).get http.callback do puts "Completed HTTP request" EM.stop end end
  • 42.
  • 43.
    EM::Spec require 'em-spec/rspec' describe LazyCalculator do include EM::SpecHelper default_timeout( 2.0) it "divides x by y" do em do calc = LazyCalculator .new.divide(6,3) calc.callback do |result| expect(result).to eq 2 done end end end end class LazyCalculator include EM::Deferrable def divide(x, y) EM.add_timer( 1.0) do if(y == 0) fail ZeroDivisionError .new else result = x/y succeed result end end self end end
  • 44.
    EM::Spec require 'em-spec/rspec' describe LazyCalculator do include EM::SpecHelper default_timeout( 2.0) it "fails when dividing by zero" do em do calc = LazyCalculator .new.divide(6,0) calc.errback do |error| expect(error).to be_a ZeroDivisionError done end end end end class LazyCalculator include EM::Deferrable def divide(x, y) EM.add_timer( 1.0) do if(y == 0) fail ZeroDivisionError .new else result = x/y succeed result end end self end end
  • 45.
    RSpec::EM::FakeClock require 'rspec/em' describe LazyCalculator do include RSpec::EM::FakeClock before { clock.stub } after { clock.reset } it "divides x by y" do calc = LazyCalculator .new.divide(6,3) expect(calc).to receive(: succeed).with 2 clock.tick( 1) end it "fails when dividing by zero" do calc = LazyCalculator .new.divide(6,0) expect(calc).to receive(: fail).with(kind_of( ZeroDivisionError )) clock.tick( 1) end end
  • 46.
    Project Demo Aproxy for Google Geocode API
  • 47.
    Limitations ● Canonly use async libraries or have to defer to threads. ● Hard to debug (no stack trace) ● Harder to test ● Difficult to build full blown websites.
  • 48.
    Caveats ● Lowcommunity support ● The last release is almost two years old
  • 49.
    Summary ● EventedI/O offers a cost effective way to scale applications ● EventMachine is a fast, scalable and production ready toolbox ● Write elegant event-driven code ● It is not the right tool for every problem
  • 50.
    EM.next? << ['em_synchrony'+ Fiber] << ['async_sinatra' + Thin] # Sinatra on EM << ['goliath'] # Non-blocking web framework << [EM.epoll + EM.kqueue] # maximize demultiplexer polling limits << [Celluloid + Celluloid.IO] # Actor pattern + Evented I/O
  • 51.
    EM.stop Thank you Code examples: github.com/omerisimo/em_underground

Editor's Notes

  • #16 Been around for over 10 years Widely used in production by many companies Postrank (purchased by Google) Github Heroku Engine Yard For example Thin web server is implemented using EventMachine
  • #27 Demo
  • #32 Show demo