7.6 Examples of Ring Applications
In this section, we’ll go through a simple example to explain how to use the Ring name server and then move to a more complex system that combines various components.
Various Ways to Wait
Let’s use the “Hello” example we used in the previous section to understand how to use the name server. The easiest way to search is to use read
.
ring02.rb | |
require 'rinda/ring' |
|
DRb.start_service |
|
|
|
ts = Rinda::RingFinger.primary |
|
tuple = ts.read([:name, :Hello, DRbObject, nil]) |
|
hello = tuple[2] |
|
puts hello.greeting |
This example uses RingFinger
to search a tuplespace and then read
with pattern matching of [:name, :Hello, DRbObject, nil]
(see Figure 42, read will be blocked until a service is registered).
Figure 42.
read
will be blocked until a service is registered.
The second read
call receives a response immediately.
If a specific service isn’t published, the process will be blocked until it becomes available. This is one of the advantages of utilizing TupleSpace
as a name server. Let’s do some experimentation. First, start RingServer
.
# terminal1 |
|
% ruby ring00.rb |
Next, start the client. This will be blocked because :Hello
isn’t registered yet.
# terminal2 |
|
% ruby ring02.rb |
Next, start ring01.rb
to publish :Hello
. Once :Hello
is published, the process at terminal 2 will be unblocked and continue its transaction.
# terminal3 |
|
% ruby ring01.rb |
# continued from terminal2 |
|
% ruby ring02.rb |
|
Hello, World. |
Let’s try ring02.rb
again. This time, it should start the transaction immediately, because :Hello
is already registered.
# terminal2 |
|
% ruby ring02.rb |
|
Hello, World. |
It’s good if you can wait for a service to be available through Ring, but you may sometimes want to quit immediately if a service you’re interested in isn’t available. In such a case, just use read
with a timeout or use read_all
.
ring03.rb | |
require 'rinda/ring' |
|
|
|
DRb.start_service |
|
|
|
ts = Rinda::RingFinger.primary |
|
begin |
|
tuple = ts.read([:name, :Hello, DRbObject, nil], 0) |
|
rescue Rinda::RequestExpiredError |
|
puts "Hello: not found." |
|
exit(1) |
|
end |
|
hello = tuple[2] |
|
puts hello.greeting |
read_all
may return multiple entries. In ring04.rb
, it responds to all the returning entries.
ring04.rb | |
require 'rinda/ring' |
|
|
|
DRb.start_service |
|
|
|
ts = Rinda::RingFinger.primary |
|
ary = ts.read_all([:name, :Hello, DRbObject, nil]) |
|
if ary.size == 0 |
|
puts "Hello: not found." |
|
exit(1) |
|
end |
|
ary.each do |tuple| |
|
hello = tuple[2] |
|
puts hello.greeting |
|
end |
Let’s see how it works:
# terminal1 |
|
% ruby ring00.rb |
# terminal2 |
|
% ruby ring04.rb |
|
Hello: not found. |
It should fail immediately because the service is not up yet. Next, run ring04.rb
after starting up two Hello
services.
# terminal3 |
|
% ruby ring01.rb |
# terminal4 |
|
% ruby ring01.rb |
# terminal2 |
|
% ruby ring04.rb |
|
Hello, World. |
|
Hello, World. |
You should see that “Hello, World.” is displayed twice. This is because the service is registered to two different services. If you want to use only one service, then use first
to take the first service, as shown in the following example, rather than iterating over using each
. The following is the modified version using first
:
ring05.rb | |
require 'rinda/ring' |
|
|
|
DRb.start_service |
|
|
|
ts = Rinda::RingFinger.primary |
|
tuple = ts.read_all([:name, :Hello, DRbObject, nil]).first |
|
if tuple.nil? |
|
puts "Hello: not found." |
|
exit(1) |
|
end |
|
hello = tuple[2] |
|
puts hello.greeting |
Next, let’s think about how to deal with newly registered services. As explained earlier, you can use read_all
to list all the registered services. However, what do you do when a new service is registered? The read
method can wait for only one service, so it isn’t a good way to keep track of all the new services. notify
seems like a good way to get notification, because service registration is simply done by write
to TupleSpace
.
Let’s write a class called RingNotify
. This will read already registered services and newly registered services as each event happens. We can instantiate RingNotify
by passing the type of services we want to watch and then take them out in the each
block.
ringnotify.rb | |
require 'thread' |
|
require 'rinda/ring' |
|
|
|
class RingNotify |
|
def initialize(ts, kind, desc=nil) |
|
@queue = Queue.new |
|
pattern = [:name, kind, DRbObject, desc] |
|
open_stream(ts, pattern) |
|
end |
|
|
|
def pop |
|
@queue.pop |
|
end |
|
|
|
def each |
|
while tuple = @queue.pop |
|
yield(tuple) |
|
end |
|
end |
|
|
|
private |
|
def open_stream(ts, pattern) |
|
@notifier = ts.notify('write', pattern) |
|
ts.read_all(pattern).each do |tuple| |
|
@queue.push(tuple) |
|
end |
|
@writer = writer_thread |
|
end |
|
|
|
def writer_thread |
|
Thread.start do |
|
begin |
|
@notifier.each do |event, tuple| |
|
@queue.push(tuple) |
|
end |
|
rescue |
|
@queue.push(nil) |
|
end |
|
end |
|
end |
|
end |
The open_stream
method calls notify
first and then calls read_all
. If it calls read_all
first and then notify
, you may miss the write
method that happened between read_all
and notify
. If you notify
first, then it won’t miss the write
method, even though there is a possibility of notifying twice.
ring06.rb
is a revised Hello
service client that handles both existing services and newly added services using RingNotify
.
ring06.rb | |
require 'rinda/ring' |
|
require './ringnotify' |
|
|
|
DRb.start_service |
|
|
|
ts = Rinda::RingFinger.primary |
|
ns = RingNotify.new(ts, :Hello) |
|
ns.each do |tuple| |
|
hello = tuple[2] |
|
puts hello.greeting |
|
end |
Let’s experiment now. First, start RingServer
.
# terminal1 |
|
% ruby ring00.rb |
Next, start the Hello
service.
# terminal2 |
|
% ruby ring01.rb |
Next, start the client ring06.rb
. Now that the Hello
service is already registered, it should immediately print out “Hello, World.” and then wait (being blocked) until the next Hello
is registered.
# terminal3 |
|
% ruby ring06.rb |
|
Hello, World. |
Let’s run another ring01.rb
and publish Hello
.
# terminal4 |
|
% ruby ring01.rb |
The process at terminal 2 will be now unblocked, continue the transaction, and then wait until the next Hello
is published.
# terminal2 (continues) |
|
% ruby ring06.rb |
|
Hello, World. |
|
Hello, World. |
As you just saw, you can write a script with read_all
and notify
to handle interservice dependencies no matter how often the dependent services restart. With this script, you aren’t bound to the restart timing of dependent services, and the re-registration of the service will become easier. If you have a complex system with many subsystems, this script will simplify the management of the start-up.
What if registered services finish? If you register an object with the renewer
option, then the timed-out service will be removed automatically. However, invalid tuples will remain in TupleSpace
for several minutes until the next round of housekeeping work happens. Let’s experiment with this.
# terminal1 |
|
% ruby ring00.rb |
|
|
|
# terminal2 |
|
% ruby ring01.rb |
|
## Halt this process with Ctrl-C |
|
|
|
# terminal3 |
|
% ruby ring06.rb |
|
/usr/local/lib/ruby/1.9/drb/drb.rb:706:in |
|
`open': druby://localhost:52180 - |
|
#<Errno::ECONNREFUSED: Connection refused - connect(2)> (DRb::DRbConnError) |
|
.... |
With Ring’s name service, an invalid entry will be deleted automatically, but this example shows that it will take a while until the invalid entry gets detected and deleted from the system. (Even if you can control it perfectly, a service could be terminated after you take out the service, so you still need to take special care.)
This is a common problem among dRuby systems. How to deal with referencing these invalid remote objects depends on each application.
ring07.rb | |
require 'rinda/ring' |
|
require './ringnotify' |
|
|
|
DRb.start_service |
|
|
|
ts = Rinda::RingFinger.primary |
|
ns = RingNotify.new(ts, :Hello) |
|
ns.each do |tuple| |
|
hello = tuple[2] |
|
begin |
|
puts hello.greeting |
|
rescue |
|
end |
|
end |
This time, we took a strategy of simply ignoring a failure to invoke methods.
In this section, we went through the following common patterns for registering and searching services:
-
Waiting for available services using
read
-
Timing out, or searching services with
read_all
-
Using
read_all
to deal with multiple services -
Dealing with service re-registration using
read_all
andnotify
-
Handling invalid services
Next, you’ll see subsets of complex systems actually in use.
Tiny “I Like Ruby”
“I like Ruby” is my Japanese website originally used to distribute dRuby and Rinda. It’s also the first practical system to use Ring. Rwiki (a dRuby-powered wiki) maintains the content, and Div (another library written by me, similar to Rail’s view helper) formats and indexes the content. It also has some sample applications.
For example, the following services are up and running:
-
A process with the WEBrick HTTP server and a session management system called Tofu
-
RWiki: A dRuby-powered wiki server
-
RWiki: An editor-only Div
-
RWiki: A formatting-only Div
-
A dRuby-based game called Hako Iri Musume
-
A simple database-backed expense reporting application called Saifu (meaning “wallet” in Japanese)
Multiple Div applications are running on top of a WEBrick-based HTTP server. Some of the Div applications are used outside WEBrick, such as within RWiki.
Thanks to Ring, you don’t have to restart all the services when you want to deploy part of your system. This is very handy when you want to do minor upgrades or small bug fixes.