The dRuby Book

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

images/ring1.png

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:

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:

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.