Event Machine in Normal Ruby (or Rails) Apps

- - posted in eventmachine, ruby

I learned a new trick from my friend and co-worker @DaDDYe, author of the awesome Padrino framework, on how to use EventMachine in normal Ruby or Rails applications.

In most EventMachine apps, you have to place everything inside of a EventMachine.run block, i.e.:

1
2
3
4
5
require 'eventmachine'

EM.run do
  # ... your app here
end

This usually means that concurrent Ruby apps have to be designed so from the start. However, you can get around this limitation.

You can start EventMachine in a seperate thread, and then you can use EventMachine calls in your synchronous program. There’s some setup and teardown to make sure everything works:

eventmachine_threads.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
require 'eventmachine'

# Setup EventMachine
Thread.new { EM.run  }

# Catch signals to ensure a clean shutdown of EventMachine
trap(:INT) { EM.stop }
trap(:TERM){ EM.stop }

# Wait for the reactor to start
while not EM.reactor_running?; end

# Setup done! Now you can use any EventMachine code here!

# Examples:
channel = EM::Channel.new
sub_id  = channel.subscribe { |msg| puts "Got: #{msg}" }

channel.push('hello world')
# => "Got: hello world"

# Clean up channel
channel.unsubscribe(sub_id)

# Will output the current time every second since it started
EM.add_timer(1) { puts "Executing timer: #{Time.now}" }

# Deferred blocks execute concurrently to the main thread
EM.defer { sleep 3; puts "It's been at least 3 seconds!" }

# Can even use EventMachine modules and code for async I/O
EM.system('ls') { |output,status| puts output }

# Tear-down, make sure that all EM defers finish
while not EM.defers_finished?; end

There’s some obvious advantages to doing this, you can easily bring asynchronous I/O and concurrent processing to any Ruby program, even in normal Rails code. There is a cost, however, and that comes in performance:

bench_results.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
# Rehearsal -------------------------------------------------------
# normal EventMachine   0.180000   0.000000   0.180000 (  0.185271)
# ---------------------------------------------- total: 0.180000sec

#                           user     system      total        real
# normal EventMachine   0.170000   0.000000   0.170000 (  0.173966)

# Rehearsal ---------------------------------------------------------
# threaded EventMachine   0.450000   0.730000   1.180000 (  0.806186)
# ------------------------------------------------ total: 1.180000sec

#                             user     system      total        real
# threaded EventMachine   0.740000   1.390000   2.130000 (  1.384526)

Using the threaded approach is a large performance penalty, but the trade off is the additional flexibility. Use it wisely.

Benchmark code for normal EventMachine:

eventmachine_bench.rb
1
2
3
4
5
6
7
8
9
10
11
12
require 'eventmachine'
require 'benchmark'

EM.run do
  channel = EM::Channel.new
  counter = 0
  sub_id  = channel.subscribe { |msg| counter += msg }

  Benchmark.bmbm do |x|
    x.report("normal EventMachine") { 50000.times {|i| channel.push(i) } }
  end
end

Benchmark code for the threaded EventMachine:

eventmachine_thread_bench.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
require 'eventmachine'
require 'benchmark'

Thread.new { EM.run  }

# Catch signals to ensure a clean shutdown of EventMachine
trap(:INT) { EM.stop }
trap(:TERM){ EM.stop }

# Wait for the reactor to start
while not EM.reactor_running?; end

channel = EventMachine::Channel.new
counter = 0
sub_id  = channel.subscribe { |msg| counter += msg }

Benchmark.bmbm do |x|
  x.report("threaded EventMachine") { 50000.times {|i| channel.push(i) } }
end