Ruby is considered by many to be slow and unscalable. In this talk we’ll try to disprove this premise by introducing EventMachine. We will cover the basic concepts of evented I/O programming and the Reactor pattern. Talk about best practices and useful libraries for EventMachine and see how to test your event driven code.
Code examples from the presentation can be found at: https://github.com/omerisimo/em_underground
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
“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
8. 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
9. Reactor pattern
IO Stream
Demultiplexer
Event Handler A
Event Handler B
Event Handler C
Event
Dispatcher
10. Reactor pattern
A reactor is a single thread
running an endless loop that
reacts to incoming events
11. Reactor pattern
A reactor is a single thread
running an endless loop that
reacts to incoming events
12. event_loop do
while reactor_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
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
25. 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
26. 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
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
30. 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
31. 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
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 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
36. 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
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.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
40. EM::Synchrony
Fiber aware EventMachine 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
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
47. 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.
48. Caveats
● Low community support
● The last release is almost two
years old
49. 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
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
Hinweis der Redaktion
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