Friday, September 14, 2012

JRuby transparently running methods asynchronously. Combining Ruby metaprogramming techiniques and Java concurrent Future

JRuby is great, it offers an opportunity to combine my two favorite languages and here is another great way of combining Java power with Ruby beauty and convenience.

In this case I created a small gem in a couple of hours (still not really well tested, just some simple unit tests) that allows to use some of the nice metaprogramming techniques from Ruby to transparently execute methods asynchronously, wrapping their return value in a java.util.concurrent.Future, so that when we access any method of the returned object, the future's get method will be called to make sure we have access to the value only when we really need it.

What follows is the source code of the main file in the Gem. Where all the relevant logic is:

require 'java'
java_import 'java.util.concurrent.ExecutorService'
java_import 'java.util.concurrent.Executors'
java_import 'java.util.concurrent.Future'
java_import 'java.util.concurrent.TimeUnit'
java_import 'java.util.concurrent.Callable'

module Futurizeit
  module ClassMethods
    def futurize(*methods)
      Futurizeit.futurize(self, *methods)
    end
  end

  def self.included(klass)
    klass.extend(ClassMethods)
  end

  def self.executor
    @executor ||= Executors.newFixedThreadPool(10)
  end

  def self.futurize(klass, *methods)
    klass.class_eval do
      methods.each do |method|
        alias :"non_futurized_#{method}" :"#{method}"
        define_method :"#{method}" do |*args|
          @future = Futurizeit.executor.submit(CallableRuby.new { self.send(:"non_futurized_#{method}", *args) })
          Futuwrapper.new(@future)
        end
      end
    end
  end
end

module Futurizeit
  class Futuwrapper < BasicObject
    def initialize(future)
      @future = future
    end

    def method_missing(method, *params)
      instance = @future.get
      instance.send(method, *params)
    end
  end

  class CallableRuby
    include Callable

    def initialize(&block)
      @block = block
    end

    def call
      @block.call
    end
  end
end
The functionality can be used in two ways, including the module in a class and calling the macro method futurize on the class, or from the outside calling the Futurizeit.futurize method directly passing a class and the instance methods of that class that we want to run asynchronously.

The way it works is straightforward:

First it creates an alias to the original instance method called "non_futurized_xxx" where xxx is the name of the original method. Then it defines a new method with the original name. This method will create a CallableRuby object which implements (include the module) the Java Callable interface.

This CallableRuby instance is then submitted to a preconfigured ExecutorService. The ExecutorService will create a Future internally and return it inmediately. We the wrap this Future in a Futurewrapper instance.

The Futurewrapper is the object that will be returned by the method. When we try to access any method on this wrapper, it will internally call the future's get method which in turn will return the actual instance that the original method would have returned without the futurizing feature.

Following is the RSpec test that tests the current functionality:

require '../lib/futurizeit'

class Futurized
  def do_something_long
    sleep 3
    "Done!"
  end
end

class FuturizedWithModuleIncluded
  include Futurizeit
  def do_something_long
    sleep 3
    "Done!"
  end
  futurize :do_something_long
end


describe "Futurizer" do
  before(:all) do
    Futurizeit::futurize(Futurized, :do_something_long)
  end

  it "should wrap methods in futures and return correct values" do
    object = Futurized.new
    start_time = Time.now.sec
    value = object.do_something_long
    end_time = Time.now.sec
    (end_time - start_time).should < 2
    value.to_s.should == 'Done!'
  end

  it "should allow calling the value twice" do
     object = Futurized.new
     value = object.do_something_long
     value.to_s.should == 'Done!'
     value.to_s.should == 'Done!'
   end

  it "should increase performance a lot parallelizing work" do
    object1 = Futurized.new
    object2 = Futurized.new
    object3 = Futurized.new
    start_time = Time.now.sec
    value1 = object1.do_something_long
    value2 = object2.do_something_long
    value3 = object3.do_something_long
    value1.to_s.should == 'Done!'
    value2.to_s.should == 'Done!'
    value3.to_s.should == 'Done!'
    end_time = Time.now.sec
    (end_time - start_time).should < 4
  end

  it "should work with class including module" do
      object = FuturizedWithModuleIncluded.new
      start_time = Time.now.sec
      value = object.do_something_long
      end_time = Time.now.sec
      (end_time - start_time).should < 2
      value.to_s.should == 'Done!'
    end

  after(:all) do
    Futurizeit.executor.shutdown
  end
end


All the code is in https://github.com/calo81/futurizeit

No comments: