Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Waiting for async calls with EventMachine and Ruby fibers

I'm running this code snippet under Ruby 1.9.2:

require "eventmachine"
require "fiber"

EM.run do
  fiber = Fiber.new do
    current_fiber = Fiber.current
    EM.add_timer(2) do
      print "B"
      current_fiber.resume("D")
    end
    Fiber.yield
  end
  print "A"
  val = fiber.resume
  print "C"
  print val
  EM.stop
end

I'm expecting the output to be "ABCD", with the program pausing for two seconds after the "A". However, instead it just prints out "AC" right away, then waits around for two seconds before exiting. What am I doing wrong?

(For reference, I'm trying to reproduce the em-synchrony-style behaviour described in this article without using em-synchrony.)

Edit: Here are some more details about what I'm ultimately trying to accomplish. I'm developing a Grape API running on Thin, and each route handler has to make various calls in series to datastores, ZooKeeper, other HTTP services, etc. before returning a response.

em-synchrony is really cool, but I keep running into issues with yielding from the root fiber or with results showing the non-synchronous symptoms of the case above. rack-fiber_pool also seems potentially useful, but I'm reluctant to commit to using it because, out of the box, it breaks all my Rack::Test unit tests.

I reduced my problems into the simple example above because I seem to have a fundamental misunderstanding about how fibers and EventMachine should be used together that is preventing me from using the more complex frameworks effectively.

like image 650
breaker Avatar asked Oct 04 '12 03:10

breaker


1 Answers

You probably wanted something like this:

require "eventmachine"
require "fiber"

def value
  current_fiber = Fiber.current

  EM.add_timer(2) do
    puts "B"
    current_fiber.resume("D") # Wakes the fiber
  end

  Fiber.yield # Suspends the Fiber, and returns "D" after #resume is called
end

EM.run do
  Fiber.new {
    puts "A"
    val = value
    puts "C"
    puts val

    EM.stop
  }.resume

  puts "(Async stuff happening)"
end

This should yield the following result:

A
(Async stuff happening)
B
C
D

A more conceptual explanation:

Fibers help untangle asynchronous code because they chunks of code to be suspended and reanimated, much like manual threads. This allows for clever tricks regarding the order on which things happen. A small example:

fiberA = Fiber.new {
  puts "A"
  Fiber.yield
  puts "C"
}

fiberB = Fiber.new {
  puts "B"
  Fiber.yield
  puts "D"
}

fiberA.resume # prints "A"
fiberB.resume # prints "B"
fiberA.resume # prints "C"
fiberB.resume # prints "D"

So, when #resume is called on a fiber, it resumes its execution, be it from the start of the block (for new fibers), or from a previous Fiber.yield call, and then it executes until another Fiber.yield is found or the block ends.

It is important to note that placing a sequence of actions inside a fiber is a way to state a temporal dependency between them (puts "C" can't run before puts "A"), while actions on "parallel" fibers can't count on (and shouldn't care about) whether or not the actions on the other fibers have executed: We would print "BACD" only by swapping the first two resume calls.

So, here's how rack-fiber_pool does its magic: It places every request your application receives inside a fiber (which implies order-independence), and then expects you to Fiber.yield on IO actions, so that the server can accept other requests. Then, inside the EventMachine callbacks, you pass in a block that contains a current_fiber.resume, so that your fiber is reanimated when the answer to the query/request/whatever is ready.

This is already a lengthy answer, but I can provide an EventMachine example if it's still not clear (I get this is a hairy concept to grok, I struggled a lot).


Update: I've created an example that might help anyone that is still struggling with the concepts: https://gist.github.com/renato-zannon/4698724. I recommend to run and play with it.

like image 149
Renato Zannon Avatar answered Nov 11 '22 19:11

Renato Zannon