The dRuby Book

7.5 Finding a Service with Ring

Ring is a name server, and it uses TupleSpace to publish your service on a local area network (LAN). The concept of Ring is close to Java’s Jini. For those familiar with Jini, Ring is similar to Jini’s discovery and lookup functionalities, but there are a few differences in terms of resource management. In Jini, a special service called a lease deletes objects if they haven’t been used for a while, while Ring manages them by simply adding an expiration date on the tuplespace. A server needs to renew the tuplespace on its own. However, the code for the renewal will be minimal by using a callback from the tuplespace.

dRuby didn’t have a default name server in the beginning, and I’ll explain why later. So, why did I write Ring later? The answer is simple: it looked interesting to do. Ring provides you with a way to dynamically look up services. An application can connect to a Ring network, register itself, and search for other available services. Ring can also remove unavailable services periodically or notify when a new service is registered. These functionalities let you change the process architecture of your system dynamically (see Figure 40, How RingService, a service, and an application work together).

images/d2ring0.png

Figure 40. How RingService, a service, and an application work together

Here are the advantages of Ring:

Ring consists of two elements:

Ring doesn’t have a special class; it simply uses TupleSpace as a name server. Once you find a TupleSpace, you can use it for normal usage of TupleSpace (for example, for writing, reading, or deleting tuples).

When you ask Ring for a service, Ring searches in two steps, so it’s not ideal for short-lived processes, such as CGI. It’s more suited for long-running processes between web applications or between web applications and other back-end services.

Locating a Name Server

As mentioned earlier, I haven’t provided a naming service, because you can just use Hash instead. Here is an example. First, you prepare a name server with a URI such as ’druby://localhost:12345’.

  ​require 'drb/drb'​
  ​DRb.start_service('druby://localhost:12345', Hash.new)​
  ​DRb.thread.join​

Then, you can add an entry called MyApp as follows:

  ​require 'drb/drb'​
  ​require 'myapp'​
  ​DRb.start_service(nil, MyApp.new)​
  ​ns = DRbObject.new_with_uri('druby://localhost:12345')​
  ​ns['MyApp'] = DRbObject.new(DRb.front)​
  ​DRb.thread.join​

Once registered, you can use the object, like so:

  ​require 'drb/drb'​
  ​DRb.start_service​
  ​ns = DRbObject.new_with_uri('druby://localhost:12345')​
  ​my_app = ns['MyApp']​
  ​my_app.do_it()​

There’s one thing I don’t like about this approach: you have to know the location of the name server. Both the name server and the client have to know the URI of the name server before starting the server. In the preceding example, we hard-coded the URI inside the script. Even if you make it configurable, you still need to know the location of the URI.

This isn’t a big problem if your system is relatively small (and the initial scope of dRuby use was for small systems), but I wanted to come up with a way to create a system without needing to know the name server’s URI. This is how I started developing Ring.

Searching TupleSpace

In Ring, the name server functionality is built on top of TupleSpace. To use the TupleSpace, you need to search available services. In this section, we’ll cover publishing and searching.

Publishing TupleSpace with RingServer

Ring uses UDP broadcasting to publish and search TupleSpace. RingServer is a class to support publishing TupleSpace via the User Datagram Protocol (UDP). RingServer monitors a UDP port and then returns references of TupleSpace requests. Here’s a basic example of RingServer usage:

ring00.rb
  ​require 'rinda/ring'
  ​require 'rinda/tuplespace'
  ​​
  ​DRb.start_service​
  ​​
  ​ts = Rinda::TupleSpace.new​
  ​place = Rinda::RingServer.new(ts)​
  ​​
  ​DRb.thread.join​

To use RingServer, instantiate an object by passing the tuplespace you want to publish as an argument.

RingServer.new(ts, port=7647) generates a RingServer with a tuplespace to publish. RingServer uses the UDP port number specified in the port argument. By default, it is 7647.

The internals of RingServer are a bit tricky. RingServer keeps waiting until DRbObject arrives at the UDP port of the RingServer. The object that gets sent via UDP is actually an Array tuple, as follows:

  ​[:lookup_ring, DRbObject.new(block)]​

RingServer executes a call method with the published tuple once the tuple arrives.

  ​tuple[1].call(@ts)​

UDP is used only to wait for DRbObject, and it isn’t used to send replies. For replies, you use the remote method invocation of DRbObject. When a client receives the search result, it receives it with dRuby’s RMI rather than writing code to wait for UDP.

Searching TupleSpace with RingFinger

RingFinger is a utility class that’s published via RingServer to search a tuplespace. There are two ways to search; one way is a detailed search using the instance method, and another is a simple search using the class method.

RingFinger searches RingServer by broadcasting a reference (DRbObject) of proc via UDP first and then receiving a callback from RingServer (see Figure 41, Searching by RingFinger using RingFinger#lookup_ring_any).

images/ring_lookup.png

Figure 41. Searching by RingFinger using RingFinger#lookup_ring_any

Use RingFinger.primary to find only one RingServer, and use RingFinger.to_a to find all RingServers in a network. Make sure you start the dRuby service before using RingFinger. Here’s an example of searching one RingServer:

  ​require 'rinda/ring'​
  ​DRb.start_service​
  ​ts = RingFinger.primary​

Here’s a list of all the RingFinger methods:

RingFinger.finger

Returns an instance of RingFinger. When first called, it searches RingServer using lookup_ring.

RingFinger.primary

Returns a reference to the TupleSpace of RingServer found by RingFinger.finger.

RingFinger.to_a

Returns all references to the TupleSpace of RingServer found by RingFinger.finger.

RingFinger.new(broadcast_list=[’<broadcast>’, ’localhost’], port=7647)

Generates RingFinger. Can specify broadcast range and port number.

RingFinger#lookup_ring(timeout=5, &block)

Awaits callback for the timeout period after a search packet is broadcast via UDP. RingServer yields the block given at RingFinger#lookup_ring with a reference to TupleSpace. You should specify an action you want to take once the TupleSpace is found in the code block.

RingFinger#lookup_ring_any(timeout=5)

Simplified version of lookup_ring. Returns the TupleSpace that responded to the first callback. Raises RingNotFound if nothing is found.

Searching by instance of RingFinger gives you more flexibility, such as being able to specify the range of broadcasting. Searching by class method will cache the result of the first search, so you can’t look up per request. It depends on the use case as to which one to use, but using class methods is enough for simple applications. To be precise, searching by the class method of RingFinger looks like a singleton, but I didn’t implement it with class methods. This is because you don’t have to limit the number of RingFingers to one, so it doesn’t have to be a strict singleton.

Name Server Using TupleSpace

Ring uses TupleSpace to make a name server. This isn’t a special TupleSpace, and it just uses the TupleSpace published by RingServer or RingFinger.

Here are the responsibilities of Ring’s name server:

This is the format of the tuple:

  ​[:name, :type, a DRbObject, "comment"]​
tuple[0]

Symbol. Represents that this is a tuple representing a name server entry. The value is always :name.

tuple[1]

Symbol. Represents a category (for example, :name_server, :place, :rwiki). This name should be decided per system.

tuple[2]

DRbObject. A reference to the object to be published.

tuple[3]

String. A comment to explain what this tuple is used for. If there’s nothing to comment, use an empty string or nil.

Let’s look at a few examples:

  ​# Register a name valid for 600sec​
  ​ts.write([:name, :rwiki, book.front, "RWiki2 front"], 600)​
  ​# Search​
  ​tuple = ts.read([:name, :rwiki, DRbObject, nil])​
  ​rwiki = tuple[2]​

There is a utility class to publish these objects. RingProvider supports publication of objects and has these responsibilities (you can still directly control tuples if you need more fine-grained control): it generates tuples and prepares renewer.

Here are the methods of RingProvider:

RingProvider.new(klass, front, desc, renewer = nil)

Prepares [:name, klass, front, desc] tuple and renewer. If renewer is not specified, generates SimpleRenewer.

RingProvider#provide

Searches tuplespace and writes the tuple and renewer into TupleSpace.

To publish a reference of your application to Ring, you need to instantiate RingProvider and then call provide. Here is the code:

ring01.rb
  ​require 'rinda/ring'
  class Hello​
  def greeting​
  "Hello, World."
  end
  end
  ​​
  ​hello = Hello.new​
  ​DRb.start_service(nil, hello)​
  ​provider = Rinda::RingProvider.new(:Hello, DRbObject.new(hello), 'Hello')​
  ​provider.provide​
  ​​
  ​DRb.thread.join​

The last few lines of this script are key. It first generates Rinda::RingProvider and then calls provide to publish the object to the network. The argument of the new constructor is in the order of the type of the object to publish, the reference to the object, and the explanation of the object.

  ​provider = Rinda::RingProvider.new(:Hello, DRbObject.new(hello), 'Hello')​
  ​provider.provide​

provide adds an entry into the name server. When this is called, it writes the following tuple:

  ​[:name, :Hello, DRbObject.new(hello), 'Hello']​

You’ll find out how to use this service within an application in the next section.