Raking and Testing with EventMachine

Posted on by in Process, Web

I have been getting more and more interested in high-performance Ruby apps, and in EventMachine in particular. First of all, super props to Aman Gupta for EM, and to some other Ruby devs out there who have been writing libraries and drivers on top if it, such as Ilya Grigorik, and Carbon Five’s own Mike Perham.

However one area that has not gotten a lot of attention within the EventMachine world is that of testing and tools support. It would be ideal for evented codebases if all tests and all rakes were automatically run inside an EventMachine reactor. I realize that many of the EM-enabled libraries out there, like mysql2, work whether they are in a reactor or not, so this may seem unnecessary. But this means that your tests are exercising a different code path than your production app, which is a bad idea.

So how can we get our tools running within EventMachine?

Monkey-patching to the rescue

I am using rake, rspec and cucumber in an EM-enabled project, and I monkey-patched each of these gems within my project to run inside a reactor. The strategy for each gem is very similar: I look for the first method called within the gem in question that is executed after the project files have been loaded, and I override it, wrapping it in a reactor.

Let’s take a look at how this works for Rake:

Rake

lib/tasks/em.rake

module Rake
  class Application

    alias_method :top_level_alias, :top_level

    def top_level
      EM.synchrony do
        top_level_alias
        EM.stop
      end
    end

  end
end

In this case, I override Rake::Application.top_level. I first alias it to top_level_alias, and then I wrap a call to that aliased method with a call to EM.synchrony.[1] And of course I stop the reactor in the end of my block.

For RSpec, the code is a little more complex, but the idea is the same.

RSpec

spec/spec_helper.rb

module RSpec
  module Core
    class ExampleGroup

      class << self
        alias_method :run_alias, :run

        def run(reporter)
          if EM.reactor_running?
            run_alias reporter
          else
            out = nil
            EM.synchrony do
              out = run_alias reporter
              EM.stop
            end
            out
          end
        end
      end

    end
  end
end

I would like to call a method that wraps the entire test suite, but the best I can do is a method wrapping a single example group. This means that I start and stop a reactor for each spec file in my project. This is not ideal, but it works just fine.

Also notice that I check whether or not the reactor is running. Because of the recursive way RSpec works, we are often already in a reactor loop when we wind up calling this method.

Cucumber is straightforward, just like Rake.

Cucumber

features/support/em.rb

module Cucumber
  module Ast
    class TreeWalker
      alias_method :visit_features_alias, :visit_features

      def visit_features(features)
        EM.synchrony do
          visit_features_alias features
          EM.stop
        end
      end
    end
  end
end

Conclusion

My hope is that each of these gems (and the other similar ones out there) will add the ability to run themselves inside an EM reactor. I see this as a configuration option, much the way ‘-drb’ is used by many gems to enable Spork. It is my plan to fork these gems and implement it myself, so the gem owners out there should expect a pull request some time soon.

Footnotes

[1] If you don’t already know, EM.synchrony is part of Ilya’s excellent em-synchrony gem. It elegantly starts a reactor within a Ruby Fiber (which also means you need to be using Ruby 1.9).


Feedback

  Comments: 16


  1. Given that you want to EM.stop after calling the aliased method, shouldn’t you call that in an ensure block so that stop is called even if there is an exception?

    Or will EM.synchrony handle that?

    • Michael Wynholds


      I don’t think there is any need to handle that specially. EM.stop does nothing more than break us out of the reactor loop. The block is running within the reactor loop, so an exception in the block will automatically break us out of the reactor loop.


  2. This is great, thanks. I feel that we need a real gem for doing EM testing. I had to abandon my use of Ruby 1.9’s Minitest to do this.. bah :-(

    • Michael Wynholds


      Thanks for the comment Kyle. (Sorry for such a late reply).

      I am using Minitest on a current project. Maybe I’ll try to get some EM testing in there somehow… If I do, I’ll add another comment here, or maybe put together a quick new post.


      • Were you able to find an elegant solution for using EM.synchrony and minitest, I’m running in to a lot of problems with doing so, I’ve been able to patch the “callback” methods for minitest so they run in the reactor, using:

        module ActiveSupport
          module Testing
            module SetupAndTeardown
              module ForMiniTest
                def run(runner)
                  EventMachine.synchrony do
                    result = '.'
                    begin
                      run_callbacks :setup do
                        result = super
                      end
                    rescue Exception => e
                      result = runner.puke(self.class, method_name, e)
                    ensure
                      begin
                        run_callbacks :teardown
                      rescue Exception => e
                        result = runner.puke(self.class, method_name, e)
                      end
                    end
                    result
                    EventMachine.stop
                  end
                end
              end
            end
          end
        end
        

        but it doesn’t seem to be working for unit test, only for functional tests, as well other weird things like it works using autotest but not `$ rake test`

        I’d love to know if you’ve gotten this working… Ilya and I have been talking about adding some of the patches you have in this post into EM.synchrony here: https://github.com/igrigorik/em-synchrony/issues/58#issuecomment-2061963


        • Hi Chris-

          I created a very simple test scenario that seems to work for me. It’s not a Rails app, just Ruby. And I monkey-patched minitest itself (in Ruby 1.9), not ActiveRecord. The repo is here:
          https://github.com/mwynholds/em-test

          and the monkey-patching is in test/test-helper.rb.

          Can you try this out and see if it works for you?

          -mike

  3. Matthew Schulkind


    I haven’t tested this extensively yet, but it seems like something like this might be cleaner for rspec at least:


    # Include this module in a spec to run all examples within a synchrony block.
    module SynchronySpec
    def self.append_features(mod)
    mod.class_eval %[
    around(:all) do |example|
    EM.synchrony do
    example.run
    EM.stop
    end
    end
    ]
    end
    end

    • Matthew Schulkind


      Gonna try that again…

      
      
      # Include this module in a spec to run all examples within a synchrony block.
      module SynchronySpec
        def self.append_features(mod)
          mod.class_eval %[
            around(:all) do |example|
              EM.synchrony do
                example.run
                EM.stop
              end
            end
          ]
        end
      end
      
      • Matthew Schulkind


        This should actually be around(:each) since around(:all) doesn’t actually exist.

      • Michael Wynholds


        Nice! I haven’t tried it out, but assuming it works (which I do), it is definitely cleaner than my monkey patching. And it has the added benefit of being included on a per-spec basis.

        Thanks.


  4. Thanks for this Michael, got me thinking.

    Thanks to Cucumber’s new Around hook, you can pull something like this off:

    
    Around("@async") do |scenario, blk|
      EM.synchrony do
        blk.call
        EM.stop
      end
    end
    
    
    • Michael Wynholds


      That looks great Scott. Definitely a cleaner way to handle Cuke features. I would love for all of my monkey patching hackery to be replaced with solutions like this.

      ps. I edited your comment to add a pre around the code tag, so that your indentation would show up


  5. I wrote a tiny library for testing eventmachine code. It’s inspired by jasmine and mocha.

    https://github.com/cameron-martin/event_machine-test

Your feedback