Skip to content

niko/em_remote_call

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

22 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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 methodserver 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)

About

Provides an Eventmachine server/client couple which allows the client to call methods within the server process, including local client-callbacks.

Resources

Stars

Watchers

Forks

Packages

No packages published

Languages