igrigorik/em-synchrony

can't yield from root fiber

coffeebite opened this issue · 6 comments

I am running a synchrony app where I have to use a library with potentially blocking IO (Selenium + Chrome)

This works:

require 'em-synchrony'
require 'em-resolv-replace'
require 'selenium-webdriver'

EM.synchrony do
  driver = Selenium::WebDriver.for :chrome
  driver.manage.window.resize_to 1024, 768
  driver.navigate.to 'http://google.com'
  EM::Synchrony.sleep 2
  driver.save_screenshot 'screenshot.png'
  driver.quit
end

But this does not save me from blocking IO. Hence, I am wrapping the selenium block in a defer call as such:

EM.synchrony do
  EM::Synchrony.defer do
    driver = Selenium::WebDriver.for :chrome
    driver.manage.window.resize_to 1024, 768
    driver.navigate.to 'http://google.com'
    EM::Synchrony.sleep 2
    driver.save_screenshot 'screenshot.png'
    driver.quit
  end
end

But this is throwing an error

gems/ruby-1.9.3-p194/gems/em-resolv-replace-1.1.3/lib/em-resolv-replace.rb:61:in `yield': > can't yield from root fiber (FiberError)

The em-resolv-replace code that is throwing this error:

fiber = Fiber.current
df = EM::DnsResolver.send(resolv_method, value)
df.callback do |a|
  fiber.resume(a)
end
df.errback do |*a|
  fiber.resume(ResolvError.new(a.inspect))
end
result = Fiber.yield

It seems like this is happening because the code inside defer runs in a separate thread without fibers? If so, what is the correct way of deferring blocking calls in EM::Synchrony? If not, how can I fix this?

Hmm. Current implementation of Synchrony.defer will pause and wait for the block to finish, but it doesn't wrap the block itself within the fiber.. this may be an oversight. Current code:
https://github.com/igrigorik/em-synchrony/blob/master/lib/em-synchrony.rb#L119

This should fix your immediate problem:

EM.synchrony do
  EM::Synchrony.defer do
    Fiber.new do
      driver = Selenium::WebDriver.for :chrome
      driver.manage.window.resize_to 1024, 768
      driver.navigate.to 'http://google.com'
      EM::Synchrony.sleep 2
      driver.save_screenshot 'screenshot.png'
      driver.quit
    end.run
  end
end

Thanks @igrigorik.
I assume you mean

Fiber.new do
 ...
end.resume

as opposed to Fiber.new ... run

I tried this previously, and I get a different error:

em-resolv-replace.rb:56:in `resume': fiber called across threads (FiberError)

Here is the resolver code again:

fiber = Fiber.current
df = EM::DnsResolver.send(resolv_method, value)
df.callback do |a|
  #--FOLLOWING LINE RAISES ERROR--
  fiber.resume(a)
end
df.errback do |*a|
  fiber.resume(ResolvError.new(a.inspect))
end
result = Fiber.yield

Yes, sorry: end.resume

Hi Ilya
No worries. It is still not working though. There is a different error this time, which I mention in the post above. Any ideas what may be happening?

Hmm, this is where it can get really tricky.. You'd have to look in the selenium driver to see what its doing. If there are multiple async calls, then the fiber logic needs to be applied in there as well -- plumbing all the way down (ugh).

Without going to deep down this route, perhaps another strategy worth considering is a sub-process:
https://github.com/postrank-labs/goliath/blob/master/examples/rasterize/rasterize.rb#L22

You can spawn another ruby (or any other type) of process, and listen to its status via EM.system. For memory heavy processes, this may actually be a better option than EM.defer (which has its own host of problems).

Thanks Ilya.
I guess I'll take this route then. Thanks very much.