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.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 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
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