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).
Here are the advantages of Ring:
-
There’s no need to know the URI of services in advance.
-
There’s no need to know the URI of a name server in advance.
-
When a service goes down, it will be removed from the name server periodically.
-
The services can receive a notification when a new server joins Ring.
Ring consists of two elements:
-
A structure to search
TupleSpace
within a LAN -
A name server that utilizes
TupleSpace
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).
Use RingFinger.primary
to find only one RingServer
, and use RingFinger.to_a
to find all RingServer
s 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 searchesRingServer
usinglookup_ring
. -
RingFinger.primary
-
Returns a reference to the
TupleSpace
ofRingServer
found byRingFinger.finger
. -
RingFinger.to_a
-
Returns all references to the
TupleSpace
ofRingServer
found byRingFinger.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 atRingFinger#lookup_ring
with a reference toTupleSpace
. You should specify an action you want to take once theTupleSpace
is found in the code block. -
RingFinger#lookup_ring_any(timeout=5)
-
Simplified version of
lookup_ring
. Returns theTupleSpace
that responded to the first callback. RaisesRingNotFound
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 RingFinger
s 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:
-
Make a tuple consisting of name, type, and reference
-
Publish the object via
write
toTupleSpace
-
Search via
read
orall
with pattern matching
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 ornil
.
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 andrenewer
. Ifrenewer
is not specified, generatesSimpleRenewer
. -
RingProvider#provide
-
Searches tuplespace and writes the tuple and
renewer
intoTupleSpace
.
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 write
s 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.