EM::RemoteCall
EM::RemoteCall provides an Eventmachine server/client couple which allows the client to call methods within the server process. Local callbacks and errbacks on the client are supported, yielding the value of the server callback.
Overview
Method declaration
-
.remote_method
– a local (client) instance method -
.remote_class_method
– a local class method
Arguments
-
:server_class_method
[boolean] – is the method a class method on the server? -
:debug
[boolean] – putses some debug statements -
:calls
[method name] – which method to call on the server
Method types:
method declaration | client | server |
remote_method :foo |
instance method | instance method |
remote_method :faz, :server_class_method => true |
instance method | class method |
remote_class_method :baz |
class method | class method |
Server side return values
- method takes a block: the values yielded to the block are returned the client (async)
- method returns a EM::Deferrable: the outcome of the deferrable is returned to the client (async)
- neither nor: the return value is returned to the client (sync)
Note: Whether or not a method takes a block can only be determined on ruby 1.9 and only if the block is explicitly in the signature. Most of the time this is the case in typical EM asynchronous methods as these blocks are passed around. So if you want your server method act asynchronously please be explicit in the signature of the method or – better yet – use EM:Deferrables.
Client side return values
Unless usual RPC libraries em_remote_call methods don’t just return values. To fit into the Eventmachine landscape they are either called with a block or a callback is defined on the deferrable they return. The return value of the remote execution is passed as argument into the block:
ClientClass.foo do |result|
puts "server returned #{result}"
end
or
call = ClientClass.foo
call.callback{ |result|
puts "server returned #{result}"
}
The later has the advantage that you can define an errback, a block wich is use in case of an error:
call = ClientClass.foo
call.callback{ |result| puts "server returned #{result}" }
call.errback { |error| puts "server errored: #{error}" }
This brings us to…
Server side errors
Errors on the server side are only handled in two cases:
- The server method returns a EM:Deferrable. Then the usual errback is used.
- The happens synchronously when parsing the call and calling the method.
In case an error occurs asynchronously we can’t get hold of the error and return it to the client.
Errors are returned as error classes (as string) and the error message.
By example
Class methods (explaining example/class_methods.rb)
We have a class on the server:
class ServerClass
def self.foo(&blk)
EM.add_timer 1 do # do some work...
blk.call 42 # then return
end
end
end
and a corresponding class on the client:
class ClientClass
has_em_remote_class 'ServerClass', :socket => '/tmp/foo'
remote_class_method :foo
end
Let’s start the server:
EM.run do
EM::RemoteCall::Server.start_at '/tmp/foo'
end
and the client:
EM.run do
ClientClass.foo do |result|
puts "server returned #{result}"
end
end
Instance methods (explaining example/instance_methods.rb)
For instance methods to work we have to identify the instances on the server side. This always works via the corresponding instance on the client side. Server side instances are never identified explicitly. (This is why the combination client side class method – server side instance method is missing in the method types table above.
To accomplish the identification of the server side instances, the :instance_finder parameter is used. By default it’s the #id
method, but it can be anything else:
has_em_remote_class 'ServerClass', :socket => '/tmp/foo', :instance_finder => :name
The simple example/instance_methods.rb doesn’t go that fancy, it just uses the default #id
defined in the common parent class:
class CommmonClass
attr_reader :id
def initialize(id)
@id = id
end
end
class ServerClass < CommmonClass
is_a_collection
def foo(&blk)
EM.add_timer 1 do # do some work...
blk.call "#{id} says 42" # then return
end
end
end
class ClientClass < CommmonClass
has_em_remote_class 'ServerClass', :socket => TEST_SOCKET
remote_method :foo
end
Let’s start the server:
EM.run do
ServerClass.new 23
EM::RemoteCall::Server.start_at TEST_SOCKET
end
… and the client:
EM.run do
c = ClientClass.new 23
c.foo do |result|
puts "server returned '#{result}'"
EM.stop
end
end
Note that the corresponding instances are created before doing the actual method call. Obviously it’s pretty pointless to manually generate all the instances on client and server side beforehand.
Let’s rectify this…
Instance methods & class methods (explaining example/instance_methods2.rb)
Here, we’re adding another instance #new_on_server
method to the client, calling the class method #new
on the server:
remote_method :new_on_server, :calls => :new, :server_class_method => true
The complete client class:
class ClientClass < CommmonClass
def initialize(id)
new_on_server id
super
end
has_em_remote_class 'ServerClass', :socket => TEST_SOCKET
remote_method :new_on_server, :calls => :new, :server_class_method => true
remote_method :foo#, :debug => true
end
Our new #new_on_server
method is called in the clients initialize method to generate the corresponding server instance every time a client instance is made.
Mashing it all together (explaining example/advanced.rb)
Common Track class
Say we have an ordinary Track class:
class Track
attr_reader :title, :artist
def initialize(opts)
@title, @artist = opts[:title], opts[:artist]
puts "new track: #{title} - #{artist} (#{self.class})"
super
end
def id
"#{title} - #{artist}"
end
end
This class contains the similar functionality on the server and the client. Note the definition of the #id
method. It serves as an identifier for tracks on the client and the server.
ServerTrack class
The server adds a #play
method to the class:
class ServerTrack < Track
is_a_collection
def play(timer=3, &callb)
puts "playing: #{id} (for #{timer} seconds...)"
EM.add_timer timer do
callb.call "finished #{id}"
end
end
end
Note the use of the is_a_collection gem. It provides a #find method and by default it does the lookup via the #id
method (that’s why we defined this above, remember?). Also note that there is no remote call specific code here. In this regards the server is completely agnostic of it being used by the client.
ClientTrack class
This is the enhancement of the Track class on the client side:
class ClientTrack < Track
def initialize(opts)
super
init_track_on_server :title => title, :artist => artist
end
extend EM::RemoteCall
remote_method :init_track_on_server, :class_name => 'ServerTrack', :calls => :new
remote_method :play, :class_name => 'ServerTrack', :find_by => :id
end
Here, we’re specifying the remote methods. It illustrates two cases:
-
#init_track_on_server
doesn’t specify a :find_by parameter.#init_track_on_server
is a instance method on the client, but call the#new
class method on the server. -
#play
specifies a :find_by parameter, therefor it’s an instance method on the server, too.
Note that on initialization of a ClientTrack we initialize a ServerTrack on the server, too.
The reactor code for the server simply looks like this:
Running the server
EM.run do
EM::RemoteCall::Server.start_at /tmp/foo
end
Nothing to see here. We just start the remote call server.
The client
On the client we then drive the remote server:
EM.run do
ClientTrack.remote_connection = EM::RemoteCall::Client.connect_to socket
track_one = ClientTrack.new :title => 'Smells like Teen Spirit', :artist => 'Nirvana'
track_two = ClientTrack.new :title => 'Concrete Schoolyards', :artist => 'J5'
EM.add_timer 1 do
track_one.play(2){|v| puts "finished: #{v}"}
end
EM.add_timer 2 do
track_two.play{ puts "finished J5!" }
end
end
First we open the connection to the server, then we initialize two tracks. As stated above: on initialization on the client side the tracks start tu exist on the server side, too. After one second we tell the server to play the first track for 2 seconds and after another second the second (defaults to 3 seconds).
FAQ
- How do you handle more complex data types as method arguments? – I don’t. Method arguments and return values have to be serializable to and form JSON. Possible data types are Strings, Numbers, Array, Hashes, Booleans.
- How do you transport the callback blocks and their bindings over the wire? – I don’t. Client callbacks stay on the client. They get stored there for later execution.
TODO
- more specs (at the moment there are no unit spec, but just integration specs)